@@ -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