@@ -169,6 +169,7 @@ impl Plot {
169169 /// - Layer aesthetics
170170 /// - Layer remappings
171171 /// - Scale aesthetics
172+ /// - Label keys
172173 pub fn transform_aesthetics_to_internal ( & mut self ) {
173174 let ctx = self . get_aesthetic_context ( ) ;
174175
@@ -187,6 +188,19 @@ impl Plot {
187188 scale. aesthetic = internal. to_string ( ) ;
188189 }
189190 }
191+
192+ // Transform label keys
193+ if let Some ( labels) = & mut self . labels {
194+ let mut transformed = HashMap :: new ( ) ;
195+ for ( key, value) in labels. labels . drain ( ) {
196+ let internal_key = ctx
197+ . map_user_to_internal ( & key)
198+ . map ( |s| s. to_string ( ) )
199+ . unwrap_or ( key) ;
200+ transformed. insert ( internal_key, value) ;
201+ }
202+ labels. labels = transformed;
203+ }
190204 }
191205
192206 /// Check if the spec has any layers
@@ -716,4 +730,120 @@ mod tests {
716730 "Non-color aesthetic should keep its name"
717731 ) ;
718732 }
733+
734+ // ========================================
735+ // Label Transformation Tests
736+ // ========================================
737+
738+ #[ test]
739+ fn test_label_transform_with_default_project ( ) {
740+ // LABEL x/y with default cartesian should transform to pos1/pos2
741+ use crate :: plot:: projection:: { Coord , Projection } ;
742+
743+ let mut spec = Plot :: new ( ) ;
744+ spec. project = Some ( Projection {
745+ coord : Coord :: cartesian ( ) ,
746+ aesthetics : vec ! [ "x" . to_string( ) , "y" . to_string( ) ] ,
747+ properties : HashMap :: new ( ) ,
748+ } ) ;
749+ spec. labels = Some ( Labels {
750+ labels : HashMap :: from ( [
751+ ( "x" . to_string ( ) , "X Axis" . to_string ( ) ) ,
752+ ( "y" . to_string ( ) , "Y Axis" . to_string ( ) ) ,
753+ ] ) ,
754+ } ) ;
755+
756+ spec. initialize_aesthetic_context ( ) ;
757+ spec. transform_aesthetics_to_internal ( ) ;
758+
759+ let labels = spec. labels . as_ref ( ) . unwrap ( ) ;
760+ assert_eq ! ( labels. labels. get( "pos1" ) , Some ( & "X Axis" . to_string( ) ) ) ;
761+ assert_eq ! ( labels. labels. get( "pos2" ) , Some ( & "Y Axis" . to_string( ) ) ) ;
762+ assert ! ( labels. labels. get( "x" ) . is_none( ) ) ;
763+ assert ! ( labels. labels. get( "y" ) . is_none( ) ) ;
764+ }
765+
766+ #[ test]
767+ fn test_label_transform_with_flipped_project ( ) {
768+ // LABEL x/y with PROJECT y, x TO cartesian should swap the mappings
769+ use crate :: plot:: projection:: { Coord , Projection } ;
770+
771+ let mut spec = Plot :: new ( ) ;
772+ // PROJECT y, x TO cartesian means y maps to pos1, x maps to pos2
773+ spec. project = Some ( Projection {
774+ coord : Coord :: cartesian ( ) ,
775+ aesthetics : vec ! [ "y" . to_string( ) , "x" . to_string( ) ] ,
776+ properties : HashMap :: new ( ) ,
777+ } ) ;
778+ spec. labels = Some ( Labels {
779+ labels : HashMap :: from ( [
780+ ( "x" . to_string ( ) , "Category" . to_string ( ) ) ,
781+ ( "y" . to_string ( ) , "Value" . to_string ( ) ) ,
782+ ] ) ,
783+ } ) ;
784+
785+ spec. initialize_aesthetic_context ( ) ;
786+ spec. transform_aesthetics_to_internal ( ) ;
787+
788+ let labels = spec. labels . as_ref ( ) . unwrap ( ) ;
789+ // x maps to pos2 (second positional), y maps to pos1 (first positional)
790+ assert_eq ! ( labels. labels. get( "pos1" ) , Some ( & "Value" . to_string( ) ) ) ;
791+ assert_eq ! ( labels. labels. get( "pos2" ) , Some ( & "Category" . to_string( ) ) ) ;
792+ }
793+
794+ #[ test]
795+ fn test_label_transform_with_polar_project ( ) {
796+ // LABEL theta/radius with polar should transform to pos1/pos2
797+ use crate :: plot:: projection:: { Coord , Projection } ;
798+
799+ let mut spec = Plot :: new ( ) ;
800+ spec. project = Some ( Projection {
801+ coord : Coord :: polar ( ) ,
802+ aesthetics : vec ! [ "theta" . to_string( ) , "radius" . to_string( ) ] ,
803+ properties : HashMap :: new ( ) ,
804+ } ) ;
805+ spec. labels = Some ( Labels {
806+ labels : HashMap :: from ( [
807+ ( "theta" . to_string ( ) , "Angle" . to_string ( ) ) ,
808+ ( "radius" . to_string ( ) , "Distance" . to_string ( ) ) ,
809+ ] ) ,
810+ } ) ;
811+
812+ spec. initialize_aesthetic_context ( ) ;
813+ spec. transform_aesthetics_to_internal ( ) ;
814+
815+ let labels = spec. labels . as_ref ( ) . unwrap ( ) ;
816+ assert_eq ! ( labels. labels. get( "pos1" ) , Some ( & "Angle" . to_string( ) ) ) ;
817+ assert_eq ! ( labels. labels. get( "pos2" ) , Some ( & "Distance" . to_string( ) ) ) ;
818+ }
819+
820+ #[ test]
821+ fn test_label_transform_preserves_non_positional ( ) {
822+ // LABEL title/color should be preserved unchanged
823+ use crate :: plot:: projection:: { Coord , Projection } ;
824+
825+ let mut spec = Plot :: new ( ) ;
826+ spec. project = Some ( Projection {
827+ coord : Coord :: cartesian ( ) ,
828+ aesthetics : vec ! [ "x" . to_string( ) , "y" . to_string( ) ] ,
829+ properties : HashMap :: new ( ) ,
830+ } ) ;
831+ spec. labels = Some ( Labels {
832+ labels : HashMap :: from ( [
833+ ( "title" . to_string ( ) , "My Chart" . to_string ( ) ) ,
834+ ( "color" . to_string ( ) , "Category" . to_string ( ) ) ,
835+ ( "x" . to_string ( ) , "X Axis" . to_string ( ) ) ,
836+ ] ) ,
837+ } ) ;
838+
839+ spec. initialize_aesthetic_context ( ) ;
840+ spec. transform_aesthetics_to_internal ( ) ;
841+
842+ let labels = spec. labels . as_ref ( ) . unwrap ( ) ;
843+ // Non-positional labels should remain unchanged
844+ assert_eq ! ( labels. labels. get( "title" ) , Some ( & "My Chart" . to_string( ) ) ) ;
845+ assert_eq ! ( labels. labels. get( "color" ) , Some ( & "Category" . to_string( ) ) ) ;
846+ // Positional label should be transformed
847+ assert_eq ! ( labels. labels. get( "pos1" ) , Some ( & "X Axis" . to_string( ) ) ) ;
848+ }
719849}
0 commit comments