Skip to content

Commit de829d1

Browse files
authored
Improve legends when using line/path with linewidth (#308)
* display legends again * fix color legend * cargo fmt
1 parent 0f8ffff commit de829d1

1 file changed

Lines changed: 95 additions & 3 deletions

File tree

src/writer/vegalite/layer.rs

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ impl GeomRenderer for PathRenderer {
510510

511511
// Handle varying linewidth: switch to trail mark and translate encodings
512512
if varying_aesthetics.contains(&"linewidth") {
513-
layer_spec["mark"] = json!({"type": "trail", "clip": true, "stroke": null});
513+
layer_spec["mark"] = json!({"type": "trail", "clip": true, "strokeWidth": 0});
514514

515515
// Translate line encodings to trail encodings
516516
if let Some(encoding_obj) = layer_spec.get_mut("encoding") {
@@ -521,7 +521,18 @@ impl GeomRenderer for PathRenderer {
521521
}
522522

523523
// stroke → fill
524-
if let Some(stroke) = encoding_map.remove("stroke") {
524+
if let Some(mut stroke) = encoding_map.remove("stroke") {
525+
// Add symbolStrokeColor to legend so symbols display with color
526+
if let Some(stroke_obj) = stroke.as_object_mut() {
527+
if let Some(legend) = stroke_obj.get_mut("legend") {
528+
if let Some(legend_obj) = legend.as_object_mut() {
529+
legend_obj.insert(
530+
"symbolStrokeColor".to_string(),
531+
json!({"expr": "scale('fill', datum.value)"}),
532+
);
533+
}
534+
}
535+
}
525536
encoding_map.insert("fill".to_string(), stroke);
526537
}
527538

@@ -4359,7 +4370,7 @@ mod tests {
43594370

43604371
// Check mark type is trail
43614372
assert_eq!(spec["mark"]["type"], "trail");
4362-
assert_eq!(spec["mark"]["stroke"], json!(null));
4373+
assert_eq!(spec["mark"]["strokeWidth"], 0);
43634374

43644375
// Check encoding translations
43654376
let encoding = spec["encoding"].as_object().unwrap();
@@ -4372,6 +4383,87 @@ mod tests {
43724383
assert!(!encoding.contains_key("stroke"), "stroke should be removed");
43734384
}
43744385

4386+
#[test]
4387+
fn test_path_renderer_trail_mark_with_stroke_legend() {
4388+
use crate::plot::{AestheticValue, Geom, Layer};
4389+
use polars::prelude::*;
4390+
4391+
let renderer = PathRenderer;
4392+
let mut layer = Layer::new(Geom::line());
4393+
4394+
// Create DataFrame with varying linewidth and stroke
4395+
let df = df! {
4396+
naming::aesthetic_column("pos1").as_str() => &[1.0, 2.0, 3.0],
4397+
naming::aesthetic_column("pos2").as_str() => &[10.0, 20.0, 30.0],
4398+
naming::aesthetic_column("linewidth").as_str() => &[1.0, 3.0, 5.0],
4399+
naming::aesthetic_column("stroke").as_str() => &["A", "A", "B"],
4400+
}
4401+
.unwrap();
4402+
4403+
// Map linewidth and stroke to columns
4404+
layer.mappings.insert(
4405+
"linewidth".to_string(),
4406+
AestheticValue::standard_column(naming::aesthetic_column("linewidth")),
4407+
);
4408+
layer.mappings.insert(
4409+
"stroke".to_string(),
4410+
AestheticValue::standard_column(naming::aesthetic_column("stroke")),
4411+
);
4412+
4413+
// Prepare data
4414+
let prepared = renderer
4415+
.prepare_data(&df, &layer, "test", &HashMap::new())
4416+
.unwrap();
4417+
4418+
// Create a mock layer spec with stroke legend
4419+
let layer_spec = json!({
4420+
"mark": {"type": "line", "clip": true},
4421+
"encoding": {
4422+
"x": {"field": naming::aesthetic_column("pos1"), "type": "quantitative"},
4423+
"y": {"field": naming::aesthetic_column("pos2"), "type": "quantitative"},
4424+
"strokeWidth": {"field": naming::aesthetic_column("linewidth"), "type": "quantitative"},
4425+
"stroke": {
4426+
"field": naming::aesthetic_column("stroke"),
4427+
"type": "nominal",
4428+
"legend": {
4429+
"title": "direction"
4430+
}
4431+
}
4432+
}
4433+
});
4434+
4435+
// Finalize should switch to trail mark and translate encodings
4436+
let result = renderer
4437+
.finalize(layer_spec.clone(), &layer, "test", &prepared)
4438+
.unwrap();
4439+
4440+
assert_eq!(result.len(), 1);
4441+
let spec = &result[0];
4442+
4443+
// Check mark type is trail
4444+
assert_eq!(spec["mark"]["type"], "trail");
4445+
assert_eq!(spec["mark"]["strokeWidth"], 0);
4446+
4447+
// Check encoding translations
4448+
let encoding = spec["encoding"].as_object().unwrap();
4449+
assert!(encoding.contains_key("size"), "Should have size encoding");
4450+
assert!(encoding.contains_key("fill"), "Should have fill encoding");
4451+
assert!(!encoding.contains_key("stroke"), "stroke should be removed");
4452+
4453+
// Check that fill legend has symbolStrokeColor
4454+
let fill = &encoding["fill"];
4455+
assert!(fill["legend"].is_object(), "fill should have legend");
4456+
let legend = fill["legend"].as_object().unwrap();
4457+
assert!(
4458+
legend.contains_key("symbolStrokeColor"),
4459+
"fill legend should have symbolStrokeColor"
4460+
);
4461+
assert_eq!(
4462+
legend["symbolStrokeColor"]["expr"], "scale('fill', datum.value)",
4463+
"symbolStrokeColor should use fill scale"
4464+
);
4465+
}
4466+
43754467
#[test]
43764468
fn test_path_renderer_segmentation_for_varying_stroke() {
43774469
use crate::plot::{AestheticValue, Geom, Layer};

0 commit comments

Comments
 (0)