From 04fae3b84ae81828c6a1b7c3f0e05cbc8ee93b09 Mon Sep 17 00:00:00 2001 From: Carson Date: Thu, 14 May 2026 12:26:27 -0500 Subject: [PATCH] fix(writer): boxplot produces invalid Vega-Lite spec Skip the boxplot stat's internal "type" aesthetic when building Vega-Lite encoding channels and strip the y2 encoding on whisker/box layers down to just the field reference. Add a schema-validated boxplot test. Closes #448 --- src/writer/vegalite/layer.rs | 5 ++--- src/writer/vegalite/mod.rs | 30 +++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/writer/vegalite/layer.rs b/src/writer/vegalite/layer.rs index 2ac08834a..1331a0aaf 100644 --- a/src/writer/vegalite/layer.rs +++ b/src/writer/vegalite/layer.rs @@ -2009,9 +2009,8 @@ impl BoxplotRenderer { // Build encoding templates for y and y2 fields let mut y_encoding = summary_prototype["encoding"][value_var1].clone(); y_encoding["field"] = json!(value_col); - let mut y2_encoding = summary_prototype["encoding"][value_var1].clone(); - y2_encoding["field"] = json!(value2_col); - y2_encoding["title"] = Value::Null; // Suppress y2 title to prevent "y, y2" axis label + // y2 is a secondary position channel — Vega-Lite only allows field/datum/value + let y2_encoding = json!({"field": value2_col}); // Lower whiskers (rule from y to y2, where y=q1 and y2=lower) let mut lower_whiskers = create_layer( diff --git a/src/writer/vegalite/mod.rs b/src/writer/vegalite/mod.rs index 727526761..fb1644fbb 100644 --- a/src/writer/vegalite/mod.rs +++ b/src/writer/vegalite/mod.rs @@ -296,9 +296,9 @@ fn build_layer_encoding( continue; } - // Skip geometry aesthetic - it is structural (consumed by SpatialRenderer - // to build GeoJSON Features), not a visual encoding channel. - if aesthetic == "geometry" { + // Skip structural aesthetics that are consumed by renderers during data + // preparation but are not visual encoding channels in Vega-Lite. + if aesthetic == "geometry" || aesthetic == "type" { continue; } @@ -3105,4 +3105,28 @@ mod tests { ); } } + + #[test] + #[cfg(feature = "duckdb")] + fn test_boxplot_schema_validation() { + use crate::reader::{DuckDBReader, Reader}; + + let reader = DuckDBReader::from_connection_string("duckdb://memory").unwrap(); + reader + .execute_sql( + "CREATE TABLE box_data AS + SELECT 'A' AS grp, generate_series * 1.0 AS value FROM GENERATE_SERIES(1, 10) + UNION ALL + SELECT 'B' AS grp, generate_series * 1.0 + 4.0 AS value FROM GENERATE_SERIES(1, 10)", + ) + .unwrap(); + + let spec = reader + .execute("SELECT * FROM box_data VISUALISE grp AS x, value AS y DRAW boxplot") + .unwrap(); + + let writer = VegaLiteWriter::new(); + let json_str = writer.render(&spec).unwrap(); + assert_valid_vegalite(&json_str); + } }