Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/parser/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1124,12 +1124,13 @@ fn build_labels(node: &Node, source: &SourceTree) -> Result<Labels> {
// Parse label type (name)
let label_type = source.get_text(&name_node);

// Parse label value (must be a string)
// Parse label value (string or null)
let label_value = match value_node.kind() {
"string" => parse_string_node(&value_node, source),
"string" => Some(parse_string_node(&value_node, source)),
"null_literal" => None,
_ => {
return Err(GgsqlError::ParseError(format!(
"Label '{}' must have a string value, got: {}",
"Label '{}' must have a string or null value, got: {}",
label_type,
value_node.kind()
)));
Expand Down
64 changes: 38 additions & 26 deletions src/plot/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ pub struct Plot {
/// Text labels (from LABELS clause)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Labels {
/// Label assignments (label type → text)
pub labels: HashMap<String, String>,
/// Label assignments (label type → text, None = suppress)
pub labels: HashMap<String, Option<String>>,
}

/// Theme styling (from THEME clause)
Expand Down Expand Up @@ -301,7 +301,7 @@ impl Plot {
label_source.to_string()
};

labels.labels.insert(primary.to_string(), column_name);
labels.labels.insert(primary.to_string(), Some(column_name));
}
}
}
Expand Down Expand Up @@ -635,7 +635,7 @@ mod tests {
};
labels
.labels
.insert("pos1".to_string(), "Custom X Label".to_string());
.insert("pos1".to_string(), Some("Custom X Label".to_string()));
spec.labels = Some(labels);

spec.compute_aesthetic_labels();
Expand All @@ -644,7 +644,7 @@ mod tests {
// User-specified label should be preserved
assert_eq!(
labels.labels.get("pos1"),
Some(&"Custom X Label".to_string())
Some(&Some("Custom X Label".to_string()))
);
// pos2 should still be computed from variants
assert!(labels.labels.contains_key("pos2"));
Expand Down Expand Up @@ -684,7 +684,7 @@ mod tests {

let labels = spec.labels.as_ref().unwrap();
// First layer's pos1 mapping should win
assert_eq!(labels.labels.get("pos1"), Some(&"date".to_string()));
assert_eq!(labels.labels.get("pos1"), Some(&Some("date".to_string())));
}

#[test]
Expand All @@ -711,7 +711,7 @@ mod tests {
// The stroke label should be "stroke" (extracted from __ggsql_aes_stroke__)
assert_eq!(
labels.labels.get("stroke"),
Some(&"stroke".to_string()),
Some(&Some("stroke".to_string())),
"Stroke aesthetic should use 'stroke' as label"
);
}
Expand All @@ -736,7 +736,7 @@ mod tests {
// The size label should be "size", not "color"
assert_eq!(
labels.labels.get("size"),
Some(&"size".to_string()),
Some(&Some("size".to_string())),
"Non-color aesthetic should keep its name"
);
}
Expand All @@ -758,17 +758,17 @@ mod tests {
});
spec.labels = Some(Labels {
labels: HashMap::from([
("x".to_string(), "X Axis".to_string()),
("y".to_string(), "Y Axis".to_string()),
("x".to_string(), Some("X Axis".to_string())),
("y".to_string(), Some("Y Axis".to_string())),
]),
});

spec.initialize_aesthetic_context();
spec.transform_aesthetics_to_internal();

let labels = spec.labels.as_ref().unwrap();
assert_eq!(labels.labels.get("pos1"), Some(&"X Axis".to_string()));
assert_eq!(labels.labels.get("pos2"), Some(&"Y Axis".to_string()));
assert_eq!(labels.labels.get("pos1"), Some(&Some("X Axis".to_string())));
assert_eq!(labels.labels.get("pos2"), Some(&Some("Y Axis".to_string())));
assert!(!labels.labels.contains_key("x"));
assert!(!labels.labels.contains_key("y"));
}
Expand All @@ -787,8 +787,8 @@ mod tests {
});
spec.labels = Some(Labels {
labels: HashMap::from([
("x".to_string(), "Category".to_string()),
("y".to_string(), "Value".to_string()),
("x".to_string(), Some("Category".to_string())),
("y".to_string(), Some("Value".to_string())),
]),
});

Expand All @@ -797,8 +797,11 @@ mod tests {

let labels = spec.labels.as_ref().unwrap();
// x maps to pos2 (second position), y maps to pos1 (first position)
assert_eq!(labels.labels.get("pos1"), Some(&"Value".to_string()));
assert_eq!(labels.labels.get("pos2"), Some(&"Category".to_string()));
assert_eq!(labels.labels.get("pos1"), Some(&Some("Value".to_string())));
assert_eq!(
labels.labels.get("pos2"),
Some(&Some("Category".to_string()))
);
}

#[test]
Expand All @@ -814,17 +817,20 @@ mod tests {
});
spec.labels = Some(Labels {
labels: HashMap::from([
("angle".to_string(), "Angle".to_string()),
("radius".to_string(), "Distance".to_string()),
("angle".to_string(), Some("Angle".to_string())),
("radius".to_string(), Some("Distance".to_string())),
]),
});

spec.initialize_aesthetic_context();
spec.transform_aesthetics_to_internal();

let labels = spec.labels.as_ref().unwrap();
assert_eq!(labels.labels.get("pos1"), Some(&"Angle".to_string()));
assert_eq!(labels.labels.get("pos2"), Some(&"Distance".to_string()));
assert_eq!(labels.labels.get("pos1"), Some(&Some("Angle".to_string())));
assert_eq!(
labels.labels.get("pos2"),
Some(&Some("Distance".to_string()))
);
}

#[test]
Expand All @@ -840,9 +846,9 @@ mod tests {
});
spec.labels = Some(Labels {
labels: HashMap::from([
("title".to_string(), "My Chart".to_string()),
("color".to_string(), "Category".to_string()),
("x".to_string(), "X Axis".to_string()),
("title".to_string(), Some("My Chart".to_string())),
("color".to_string(), Some("Category".to_string())),
("x".to_string(), Some("X Axis".to_string())),
]),
});

Expand All @@ -851,9 +857,15 @@ mod tests {

let labels = spec.labels.as_ref().unwrap();
// Material labels should remain unchanged
assert_eq!(labels.labels.get("title"), Some(&"My Chart".to_string()));
assert_eq!(labels.labels.get("color"), Some(&"Category".to_string()));
assert_eq!(
labels.labels.get("title"),
Some(&Some("My Chart".to_string()))
);
assert_eq!(
labels.labels.get("color"),
Some(&Some("Category".to_string()))
);
// Position label should be transformed
assert_eq!(labels.labels.get("pos1"), Some(&"X Axis".to_string()));
assert_eq!(labels.labels.get("pos1"), Some(&Some("X Axis".to_string())));
}
}
22 changes: 18 additions & 4 deletions src/writer/vegalite/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,15 @@ fn apply_title_to_encoding(
.as_ref()
.and_then(|labels| labels.labels.get(primary));

if let Some(label) = explicit_label {
encoding["title"] = super::split_label_on_newlines(label);
if let Some(label_opt) = explicit_label {
match label_opt {
Some(label) => {
encoding["title"] = super::split_label_on_newlines(label);
}
None => {
encoding["title"] = Value::Null;
}
}
titled_families.insert(primary.to_string());
} else if let Some(orig) = original_name {
// Use original column name as default title when available
Expand All @@ -428,8 +435,15 @@ fn apply_title_to_encoding(
} else if !is_primary && !primary_exists && !titled_families.contains(primary) {
// Variant without primary: allow first variant to claim title (for explicit labels)
if let Some(ref labels) = spec.labels {
if let Some(label) = labels.labels.get(primary) {
encoding["title"] = super::split_label_on_newlines(label);
if let Some(label_opt) = labels.labels.get(primary) {
match label_opt {
Some(label) => {
encoding["title"] = super::split_label_on_newlines(label);
}
None => {
encoding["title"] = Value::Null;
}
}
titled_families.insert(primary.to_string());
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/writer/vegalite/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1110,8 +1110,8 @@ impl Writer for VegaLiteWriter {
}

if let Some(labels) = &spec.labels {
let title = labels.labels.get("title");
let subtitle = labels.labels.get("subtitle");
let title = labels.labels.get("title").and_then(|v| v.as_ref());
let subtitle = labels.labels.get("subtitle").and_then(|v| v.as_ref());
match (title, subtitle) {
(Some(t), Some(st)) => {
// Vega-Lite uses an object for title + subtitle
Expand Down Expand Up @@ -1564,7 +1564,7 @@ mod tests {
};
labels
.labels
.insert("title".to_string(), "My Chart".to_string());
.insert("title".to_string(), Some("My Chart".to_string()));
spec.labels = Some(labels);

let df = df! {
Expand Down
2 changes: 1 addition & 1 deletion tree-sitter-ggsql/grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,7 @@ module.exports = grammar({
label_assignment: $ => seq(
field('name', $.label_type),
'=>',
field('value', $.string)
field('value', choice($.string, $.null_literal))
),

label_type: $ => $.identifier,
Expand Down
Loading