From fcff9f67afea70a2ab6e398f3172ed24a9a06f7a Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Wed, 17 Jun 2026 14:35:11 +0200 Subject: [PATCH 1/3] flatten/unnest logic --- src/writer/vegalite/encoding.rs | 59 +++++++++++++-------------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/src/writer/vegalite/encoding.rs b/src/writer/vegalite/encoding.rs index 4c796e60..b4325176 100644 --- a/src/writer/vegalite/encoding.rs +++ b/src/writer/vegalite/encoding.rs @@ -725,44 +725,31 @@ fn apply_label_mapping_to_encoding( _ => None, }); - // Build the mapping and null_key based on legend style - let (filtered_mapping, null_key) = if is_binned_legend { - let legend_style = determine_legend_style(aesthetic, spec); - - if legend_style == LegendStyle::Symbol { - // Symbol legend: map VL's range-style labels to our labels - let closed = scale - .properties - .get("closed") - .and_then(|v| { - if let ParameterValue::String(s) = v { - Some(s.as_str()) - } else { - None - } - }) - .unwrap_or("left"); + let is_symbol = + is_binned_legend && determine_legend_style(aesthetic, spec) == LegendStyle::Symbol; - if let Some(ParameterValue::Array(breaks)) = scale.properties.get("breaks") { - let symbol_mapping = - build_symbol_legend_label_mapping(breaks, label_mapping, closed); - (symbol_mapping, None) - } else { - (label_mapping.clone(), None) - } - } else { - // Gradient legend: use null_key for first terminal - let first_key = scale.properties.get("breaks").and_then(|b| { - if let ParameterValue::Array(breaks) = b { - breaks.first().map(|e| e.to_key_string()) - } else { - None - } - }); - (label_mapping.clone(), first_key) - } + let breaks = match scale.properties.get("breaks") { + Some(ParameterValue::Array(b)) => Some(b.as_slice()), + _ => None, + }; + + // Symbol legends compare VL's predicted range labels (e.g. "-20 – 0") + // as strings via datum.label, not as numeric datum.value. + let filtered_mapping = if let (true, Some(breaks)) = (is_symbol, breaks) { + let closed = match scale.properties.get("closed") { + Some(ParameterValue::String(s)) => s.as_str(), + _ => "left", + }; + build_symbol_legend_label_mapping(breaks, label_mapping, closed) + } else { + label_mapping.clone() + }; + + // Gradient legends use null for the first terminal's label + let null_key = if is_binned_legend && !is_symbol { + breaks.and_then(|b| b.first().map(|e| e.to_key_string())) } else { - (label_mapping.clone(), None) + None }; let label_expr = build_label_expr( From ebe79d30ace673616ef865e9e463496f4679f83c Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Wed, 17 Jun 2026 14:38:05 +0200 Subject: [PATCH 2/3] fix bug --- src/writer/vegalite/encoding.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/writer/vegalite/encoding.rs b/src/writer/vegalite/encoding.rs index b4325176..23512d7f 100644 --- a/src/writer/vegalite/encoding.rs +++ b/src/writer/vegalite/encoding.rs @@ -752,11 +752,13 @@ fn apply_label_mapping_to_encoding( None }; + let effective_field_type = if is_symbol { "nominal" } else { field_type }; + let label_expr = build_label_expr( &filtered_mapping, time_format, null_key.as_deref(), - field_type, + effective_field_type, ); if is_position_aesthetic(aesthetic) { From 2ce7065407ba01b4bf62b54f6d886158149092f7 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Wed, 17 Jun 2026 14:39:44 +0200 Subject: [PATCH 3/3] add test --- src/writer/vegalite/encoding.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/writer/vegalite/encoding.rs b/src/writer/vegalite/encoding.rs index 23512d7f..c409de8b 100644 --- a/src/writer/vegalite/encoding.rs +++ b/src/writer/vegalite/encoding.rs @@ -1203,6 +1203,35 @@ mod tests { ); } + #[test] + fn test_symbol_legend_label_expr_uses_datum_label() { + use crate::plot::ArrayElement; + + // Breaks: -20, 0, 20 → VL predicts labels "-20 – 0" and "≥ 0" + let breaks = vec![ + ArrayElement::Number(-20.0), + ArrayElement::Number(0.0), + ArrayElement::Number(20.0), + ]; + let mut label_mapping = HashMap::new(); + label_mapping.insert("-20".to_string(), Some("cold".to_string())); + label_mapping.insert("0".to_string(), Some("hot".to_string())); + + let symbol_mapping = build_symbol_legend_label_mapping(&breaks, &label_mapping, "left"); + + // The resulting mapping uses VL's range-style label strings as keys + let expr = build_label_expr(&symbol_mapping, None, None, "nominal"); + + assert!( + expr.contains("datum.label =="), + "symbol legend labelExpr must use datum.label (string comparison), got: {expr}" + ); + assert!( + !expr.contains("datum.value =="), + "symbol legend labelExpr must not use datum.value (keys contain en-dashes), got: {expr}" + ); + } + #[test] fn test_literal_shape_converts_to_svg_path() { let lit = ParameterValue::String("square".to_string());