Skip to content

Commit 2c15821

Browse files
authored
Fix #184 (#186)
1 parent 10890cb commit 2c15821

2 files changed

Lines changed: 202 additions & 0 deletions

File tree

src/plot/main.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

src/reader/mod.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,4 +1044,76 @@ mod tests {
10441044
"Identity position should not have xOffset encoding"
10451045
);
10461046
}
1047+
1048+
#[test]
1049+
fn test_label_with_flipped_project() {
1050+
// End-to-end test: LABEL x/y with PROJECT y, x TO cartesian
1051+
// Labels should be correctly applied to the flipped axes
1052+
let reader = DuckDBReader::from_connection_string("duckdb://memory").unwrap();
1053+
let query = r#"
1054+
SELECT * FROM (VALUES (1, 10), (2, 20)) AS t(x, y)
1055+
VISUALISE
1056+
DRAW bar MAPPING x AS y, y AS x
1057+
PROJECT y, x TO cartesian
1058+
LABEL x => 'Value', y => 'Category'
1059+
"#;
1060+
1061+
let spec = reader.execute(query).unwrap();
1062+
let writer = VegaLiteWriter::new();
1063+
let result = writer.render(&spec).unwrap();
1064+
1065+
let json: serde_json::Value = serde_json::from_str(&result).unwrap();
1066+
let layer = json["layer"].as_array().unwrap().first().unwrap();
1067+
let encoding = &layer["encoding"];
1068+
1069+
// With PROJECT y, x TO cartesian:
1070+
// - y is pos1 (first positional), renders to VL x-axis in cartesian
1071+
// - x is pos2 (second positional), renders to VL y-axis in cartesian
1072+
// So LABEL y => 'Category' should appear on VL x-axis, LABEL x => 'Value' on VL y-axis
1073+
let x_title = encoding["x"]["title"].as_str();
1074+
let y_title = encoding["y"]["title"].as_str();
1075+
1076+
assert_eq!(
1077+
x_title,
1078+
Some("Category"),
1079+
"x-axis should have 'Category' title (from LABEL y). Got encoding: {}",
1080+
serde_json::to_string_pretty(encoding).unwrap()
1081+
);
1082+
assert_eq!(
1083+
y_title,
1084+
Some("Value"),
1085+
"y-axis should have 'Value' title (from LABEL x). Got encoding: {}",
1086+
serde_json::to_string_pretty(encoding).unwrap()
1087+
);
1088+
}
1089+
1090+
#[test]
1091+
fn test_label_with_polar_project() {
1092+
// End-to-end test: LABEL theta/radius with PROJECT TO polar
1093+
let reader = DuckDBReader::from_connection_string("duckdb://memory").unwrap();
1094+
let query = r#"
1095+
SELECT * FROM (VALUES ('A', 10), ('B', 20)) AS t(category, value)
1096+
VISUALISE value AS theta, category AS fill
1097+
DRAW bar
1098+
PROJECT TO polar
1099+
LABEL theta => 'Angle', radius => 'Distance'
1100+
"#;
1101+
1102+
let spec = reader.execute(query).unwrap();
1103+
let writer = VegaLiteWriter::new();
1104+
let result = writer.render(&spec).unwrap();
1105+
1106+
let json: serde_json::Value = serde_json::from_str(&result).unwrap();
1107+
let layer = json["layer"].as_array().unwrap().first().unwrap();
1108+
let encoding = &layer["encoding"];
1109+
1110+
// Verify theta encoding has the label
1111+
let theta_title = encoding["theta"]["title"].as_str();
1112+
assert_eq!(
1113+
theta_title,
1114+
Some("Angle"),
1115+
"theta encoding should have 'Angle' title. Got encoding: {}",
1116+
serde_json::to_string_pretty(encoding).unwrap()
1117+
);
1118+
}
10471119
}

0 commit comments

Comments
 (0)