From 35f622b61ebd0e502d1f7b0aed38f8e9e5f579c7 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Thu, 19 Mar 2026 11:32:26 +0100 Subject: [PATCH 01/11] add ridge parameter --- src/plot/layer/geom/violin.rs | 4 ++++ src/writer/vegalite/layer.rs | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/plot/layer/geom/violin.rs b/src/plot/layer/geom/violin.rs index 60192d98..5575d0a1 100644 --- a/src/plot/layer/geom/violin.rs +++ b/src/plot/layer/geom/violin.rs @@ -63,6 +63,10 @@ impl GeomTrait for Violin { name: "width", default: DefaultParamValue::Number(0.9), }, + DefaultParam { + name: "ridge", + default: DefaultParamValue::String("both"), + }, ] } diff --git a/src/writer/vegalite/layer.rs b/src/writer/vegalite/layer.rs index 92cdc261..bf986609 100644 --- a/src/writer/vegalite/layer.rs +++ b/src/writer/vegalite/layer.rs @@ -1311,7 +1311,18 @@ impl GeomRenderer for ViolinRenderer { let offset_col = naming::aesthetic_column("offset"); // It'll be implemented as an offset. - let violin_offset = format!("[datum.{offset}, -datum.{offset}]", offset = offset_col); + let mut violin_offset = format!("[datum.{offset}, -datum.{offset}]", offset = offset_col); + if let Some(ParameterValue::String(ridge)) = layer.parameters.get("ridge") { + match ridge.as_str() { + "left" | "top" => { + violin_offset = format!("[-datum.{offset}]", offset = offset_col); + } + "right" | "bottom" => { + violin_offset = format!("[datum.{offset}]", offset = offset_col); + } + _ => {} + } + } // Read orientation from layer (already resolved during execution) let is_horizontal = is_transposed(layer); From f3c11be703c8e1b571a662b9701db3a64ce31d98 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Thu, 19 Mar 2026 12:09:14 +0100 Subject: [PATCH 02/11] add docs --- doc/syntax/layer/type/violin.qmd | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/syntax/layer/type/violin.qmd b/doc/syntax/layer/type/violin.qmd index 0d63d8e7..9bbb2500 100644 --- a/doc/syntax/layer/type/violin.qmd +++ b/doc/syntax/layer/type/violin.qmd @@ -34,6 +34,10 @@ The following aesthetics are recognised by the violin layer. * `'biweight'` or `'quartic'` * `'cosine'` * `width`: Relative width of the violins. Defaults to `0.9`. +* `ridge`: Determines the sides of the midline where the density is displayed. One of the following: + * `'both'` (default) displays a complete violin both sides of the midline. + * `'left'` or `'top'` only displays half a violin at the left side or top side. + * `'right'` or `'bottom'` only displays half a violin at the right side or bottom side. ## Data transformation A violin layer uses the same computation as a density layer. See the [density data transformation](density.qmd#data-transformation) section for details. @@ -93,3 +97,11 @@ VISUALISE species AS y, bill_dep AS x FROM ggsql:penguins DRAW violin ``` +A ridgeline plot (or joy plot) can be seen as a horizontal half-violin plot, or like a density plot with vertical offsets for every category. +To achieve this outcome, you can set the `ridge` setting and adjust `width` to taste. + +```{ggsql} +VISUALISE Temp AS x, Month AS y FROM ggsql:airquality +DRAW violin SETTING width => 4, ridge => 'top' +SCALE ORDINAL y +``` \ No newline at end of file From 9b06c4c7b29a0e18e917f946fa27abba88d9fcd7 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Thu, 19 Mar 2026 12:16:29 +0100 Subject: [PATCH 03/11] add test --- src/writer/vegalite/layer.rs | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/writer/vegalite/layer.rs b/src/writer/vegalite/layer.rs index bf986609..c427baae 100644 --- a/src/writer/vegalite/layer.rs +++ b/src/writer/vegalite/layer.rs @@ -3217,6 +3217,52 @@ mod tests { ); } + #[test] + fn test_violin_ridge_parameter() { + use crate::naming; + use crate::plot::ParameterValue; + + let offset_col = naming::aesthetic_column("offset"); + + fn get_violin_offset_expr(ridge: Option<&str>) -> String { + let mut layer = Layer::new(crate::plot::Geom::violin()); + if let Some(r) = ridge { + layer.parameters.insert("ridge".to_string(), ParameterValue::String(r.to_string())); + } + + let mut layer_spec = json!({ + "mark": {"type": "line"}, + "encoding": { + "x": {"field": "species", "type": "nominal"}, + "y": {"field": naming::aesthetic_column("pos2"), "type": "quantitative"} + } + }); + + ViolinRenderer.modify_spec(&mut layer_spec, &layer, &RenderContext::new(&[])).unwrap(); + + layer_spec["transform"].as_array().unwrap() + .iter() + .find(|t| t.get("as").and_then(|a| a.as_str()) == Some("violin_offsets")) + .unwrap()["calculate"].as_str().unwrap().to_string() + } + + // Default "both" - mirrors on both sides + let expr = get_violin_offset_expr(None); + assert!( + expr.contains(&format!("[datum.{}, -datum.{}]", offset_col, offset_col)) + || expr.contains(&format!("[-datum.{}, datum.{}]", offset_col, offset_col)), + "Default should mirror both sides: {}", expr + ); + + // "left" and "top" - only negative offset + assert_eq!(get_violin_offset_expr(Some("left")), format!("[-datum.{}]", offset_col)); + assert_eq!(get_violin_offset_expr(Some("top")), format!("[-datum.{}]", offset_col)); + + // "right" and "bottom" - only positive offset + assert_eq!(get_violin_offset_expr(Some("right")), format!("[datum.{}]", offset_col)); + assert_eq!(get_violin_offset_expr(Some("bottom")), format!("[datum.{}]", offset_col)); + } + #[test] fn test_render_context_get_extent() { use crate::plot::{ArrayElement, Scale}; From 9297edf50be5077c42262574b03c77fb8c7788d7 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Thu, 19 Mar 2026 13:02:44 +0100 Subject: [PATCH 04/11] cargo fmt --- src/writer/vegalite/layer.rs | 40 ++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/writer/vegalite/layer.rs b/src/writer/vegalite/layer.rs index 8ff56eca..fde3e14a 100644 --- a/src/writer/vegalite/layer.rs +++ b/src/writer/vegalite/layer.rs @@ -3219,7 +3219,9 @@ mod tests { fn get_violin_offset_expr(ridge: Option<&str>) -> String { let mut layer = Layer::new(crate::plot::Geom::violin()); if let Some(r) = ridge { - layer.parameters.insert("ridge".to_string(), ParameterValue::String(r.to_string())); + layer + .parameters + .insert("ridge".to_string(), ParameterValue::String(r.to_string())); } let mut layer_spec = json!({ @@ -3230,12 +3232,19 @@ mod tests { } }); - ViolinRenderer.modify_spec(&mut layer_spec, &layer, &RenderContext::new(&[])).unwrap(); + ViolinRenderer + .modify_spec(&mut layer_spec, &layer, &RenderContext::new(&[])) + .unwrap(); - layer_spec["transform"].as_array().unwrap() + layer_spec["transform"] + .as_array() + .unwrap() .iter() .find(|t| t.get("as").and_then(|a| a.as_str()) == Some("violin_offsets")) - .unwrap()["calculate"].as_str().unwrap().to_string() + .unwrap()["calculate"] + .as_str() + .unwrap() + .to_string() } // Default "both" - mirrors on both sides @@ -3243,16 +3252,29 @@ mod tests { assert!( expr.contains(&format!("[datum.{}, -datum.{}]", offset_col, offset_col)) || expr.contains(&format!("[-datum.{}, datum.{}]", offset_col, offset_col)), - "Default should mirror both sides: {}", expr + "Default should mirror both sides: {}", + expr ); // "left" and "top" - only negative offset - assert_eq!(get_violin_offset_expr(Some("left")), format!("[-datum.{}]", offset_col)); - assert_eq!(get_violin_offset_expr(Some("top")), format!("[-datum.{}]", offset_col)); + assert_eq!( + get_violin_offset_expr(Some("left")), + format!("[-datum.{}]", offset_col) + ); + assert_eq!( + get_violin_offset_expr(Some("top")), + format!("[-datum.{}]", offset_col) + ); // "right" and "bottom" - only positive offset - assert_eq!(get_violin_offset_expr(Some("right")), format!("[datum.{}]", offset_col)); - assert_eq!(get_violin_offset_expr(Some("bottom")), format!("[datum.{}]", offset_col)); + assert_eq!( + get_violin_offset_expr(Some("right")), + format!("[datum.{}]", offset_col) + ); + assert_eq!( + get_violin_offset_expr(Some("bottom")), + format!("[datum.{}]", offset_col) + ); } #[test] From 0c9a38c4211aea4a50435d67788ffb96685da8dd Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Tue, 7 Apr 2026 10:09:35 +0200 Subject: [PATCH 05/11] rename `ridge` to `align` --- doc/syntax/layer/type/violin.qmd | 4 ++-- src/plot/layer/geom/violin.rs | 2 +- src/writer/vegalite/layer.rs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/syntax/layer/type/violin.qmd b/doc/syntax/layer/type/violin.qmd index ac80f7dc..396938b9 100644 --- a/doc/syntax/layer/type/violin.qmd +++ b/doc/syntax/layer/type/violin.qmd @@ -34,8 +34,8 @@ The following aesthetics are recognised by the violin layer. * `'biweight'` or `'quartic'` * `'cosine'` * `width`: Relative width of the violins (0 to 1). Defaults to `0.9`. -* `ridge`: Determines the sides of the midline where the density is displayed. One of the following: - * `'both'` (default) displays a complete violin both sides of the midline. +* `align`: Determines the sides of the midline where the density is displayed. One of the following: + * `'center'` or `'centre'` (default) displays a complete violin both sides of the midline. * `'left'` or `'top'` only displays half a violin at the left side or top side. * `'right'` or `'bottom'` only displays half a violin at the right side or bottom side. * `tails`: Expansion rule for drawing the tails (must be >= 0 if numeric). One of the following: diff --git a/src/plot/layer/geom/violin.rs b/src/plot/layer/geom/violin.rs index 270ec682..f5b64b6a 100644 --- a/src/plot/layer/geom/violin.rs +++ b/src/plot/layer/geom/violin.rs @@ -84,7 +84,7 @@ impl GeomTrait for Violin { constraint: ParamConstraint::number_range(0.0, 1.0), }, ParamDefinition { - name: "ridge", + name: "align", default: DefaultParamValue::String("center"), constraint: ParamConstraint::string_option(ALIGN_VALUES), }, diff --git a/src/writer/vegalite/layer.rs b/src/writer/vegalite/layer.rs index 77769663..99482849 100644 --- a/src/writer/vegalite/layer.rs +++ b/src/writer/vegalite/layer.rs @@ -1313,8 +1313,8 @@ impl GeomRenderer for ViolinRenderer { // It'll be implemented as an offset. let mut violin_offset = format!("[datum.{offset}, -datum.{offset}]", offset = offset_col); - if let Some(ParameterValue::String(ridge)) = layer.parameters.get("ridge") { - match ridge.as_str() { + if let Some(ParameterValue::String(align)) = layer.parameters.get("align") { + match align.as_str() { "left" | "top" => { violin_offset = format!("[-datum.{offset}]", offset = offset_col); } @@ -3237,7 +3237,7 @@ mod tests { if let Some(r) = ridge { layer .parameters - .insert("ridge".to_string(), ParameterValue::String(r.to_string())); + .insert("align".to_string(), ParameterValue::String(r.to_string())); } let mut layer_spec = json!({ From fe6555e15a665a8bb7d11b76a04d13b6fed2aa7a Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Tue, 7 Apr 2026 10:11:17 +0200 Subject: [PATCH 06/11] fix example --- doc/syntax/layer/type/violin.qmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/syntax/layer/type/violin.qmd b/doc/syntax/layer/type/violin.qmd index 396938b9..eb2fa47e 100644 --- a/doc/syntax/layer/type/violin.qmd +++ b/doc/syntax/layer/type/violin.qmd @@ -111,10 +111,10 @@ DRAW violin ``` A ridgeline plot (or joy plot) can be seen as a horizontal half-violin plot, or like a density plot with vertical offsets for every category. -To achieve this outcome, you can set the `ridge` setting and adjust `width` to taste. +To achieve this outcome, you can set the `align` setting and adjust `width` to taste. ```{ggsql} VISUALISE Temp AS x, Month AS y FROM ggsql:airquality -DRAW violin SETTING width => 4, ridge => 'top' +DRAW violin SETTING width => 4, align => 'top' SCALE ORDINAL y ``` \ No newline at end of file From 6276aa614c324c6a1251e1702285cb0684b014a0 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Tue, 7 Apr 2026 10:22:55 +0200 Subject: [PATCH 07/11] Allow `width` to exceed 1 for ridgeline purposes --- src/plot/layer/geom/violin.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plot/layer/geom/violin.rs b/src/plot/layer/geom/violin.rs index f5b64b6a..a544d8d4 100644 --- a/src/plot/layer/geom/violin.rs +++ b/src/plot/layer/geom/violin.rs @@ -81,7 +81,8 @@ impl GeomTrait for Violin { ParamDefinition { name: "width", default: DefaultParamValue::Number(0.9), - constraint: ParamConstraint::number_range(0.0, 1.0), + // We allow >1 width to make ridgeline plots + constraint: ParamConstraint::number_min_exclusive(0.0), }, ParamDefinition { name: "align", From 067a587ef120815b2d3f193b73fab781698b6d58 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Tue, 7 Apr 2026 12:05:00 +0200 Subject: [PATCH 08/11] rename the parameter to 'side' --- doc/syntax/layer/type/violin.qmd | 8 ++++---- src/plot/layer/geom/violin.rs | 8 ++++---- src/writer/vegalite/layer.rs | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/syntax/layer/type/violin.qmd b/doc/syntax/layer/type/violin.qmd index eb2fa47e..f03c7b66 100644 --- a/doc/syntax/layer/type/violin.qmd +++ b/doc/syntax/layer/type/violin.qmd @@ -34,8 +34,8 @@ The following aesthetics are recognised by the violin layer. * `'biweight'` or `'quartic'` * `'cosine'` * `width`: Relative width of the violins (0 to 1). Defaults to `0.9`. -* `align`: Determines the sides of the midline where the density is displayed. One of the following: - * `'center'` or `'centre'` (default) displays a complete violin both sides of the midline. +* `side`: Determines the sides of the midline where the density is displayed. One of the following: + * `'both'` (default) displays a complete violin both sides of the midline. * `'left'` or `'top'` only displays half a violin at the left side or top side. * `'right'` or `'bottom'` only displays half a violin at the right side or bottom side. * `tails`: Expansion rule for drawing the tails (must be >= 0 if numeric). One of the following: @@ -111,10 +111,10 @@ DRAW violin ``` A ridgeline plot (or joy plot) can be seen as a horizontal half-violin plot, or like a density plot with vertical offsets for every category. -To achieve this outcome, you can set the `align` setting and adjust `width` to taste. +To achieve this outcome, you can set the `side` setting and adjust `width` to taste. ```{ggsql} VISUALISE Temp AS x, Month AS y FROM ggsql:airquality -DRAW violin SETTING width => 4, align => 'top' +DRAW violin SETTING width => 4, side => 'top' SCALE ORDINAL y ``` \ No newline at end of file diff --git a/src/plot/layer/geom/violin.rs b/src/plot/layer/geom/violin.rs index a544d8d4..06d2239c 100644 --- a/src/plot/layer/geom/violin.rs +++ b/src/plot/layer/geom/violin.rs @@ -25,7 +25,7 @@ const KERNEL_VALUES: &[&str] = &[ "cosine", ]; -const ALIGN_VALUES: &[&str] = &["center", "centre", "left", "top", "right", "bottom"]; +const SIDE_VALUES: &[&str] = &["both", "left", "top", "right", "bottom"]; /// Violin geom - violin plots (mirrored density) #[derive(Debug, Clone, Copy)] @@ -85,9 +85,9 @@ impl GeomTrait for Violin { constraint: ParamConstraint::number_min_exclusive(0.0), }, ParamDefinition { - name: "align", - default: DefaultParamValue::String("center"), - constraint: ParamConstraint::string_option(ALIGN_VALUES), + name: "side", + default: DefaultParamValue::String("both"), + constraint: ParamConstraint::string_option(SIDE_VALUES), }, ParamDefinition { name: "tails", diff --git a/src/writer/vegalite/layer.rs b/src/writer/vegalite/layer.rs index 99482849..994e9ce0 100644 --- a/src/writer/vegalite/layer.rs +++ b/src/writer/vegalite/layer.rs @@ -1313,8 +1313,8 @@ impl GeomRenderer for ViolinRenderer { // It'll be implemented as an offset. let mut violin_offset = format!("[datum.{offset}, -datum.{offset}]", offset = offset_col); - if let Some(ParameterValue::String(align)) = layer.parameters.get("align") { - match align.as_str() { + if let Some(ParameterValue::String(side)) = layer.parameters.get("side") { + match side.as_str() { "left" | "top" => { violin_offset = format!("[-datum.{offset}]", offset = offset_col); } @@ -3237,7 +3237,7 @@ mod tests { if let Some(r) = ridge { layer .parameters - .insert("align".to_string(), ParameterValue::String(r.to_string())); + .insert("side".to_string(), ParameterValue::String(r.to_string())); } let mut layer_spec = json!({ From ed54f1683525ccec5447d793d2628e2fa13a95dc Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Wed, 8 Apr 2026 10:26:17 +0200 Subject: [PATCH 09/11] offset variable doesn't need encoding channel, we already have xOffset/yOffset channels --- src/writer/vegalite/layer.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/writer/vegalite/layer.rs b/src/writer/vegalite/layer.rs index 994e9ce0..82f61757 100644 --- a/src/writer/vegalite/layer.rs +++ b/src/writer/vegalite/layer.rs @@ -1394,6 +1394,8 @@ impl GeomRenderer for ViolinRenderer { // Read orientation from layer (already resolved during execution) let is_horizontal = is_transposed(layer); + encoding.remove("offset"); + // Categorical axis for detail encoding: // - Vertical: x channel (categorical groups on x-axis) // - Horizontal: y channel (categorical groups on y-axis) From 92fb952c241f3a08379e2195dc678f6c44f5d08f Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Wed, 8 Apr 2026 11:37:26 +0200 Subject: [PATCH 10/11] relative-to-axis consistency of synonymous `side` --- doc/syntax/layer/type/violin.qmd | 4 ++-- src/writer/vegalite/layer.rs | 25 +++++++++++++------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/doc/syntax/layer/type/violin.qmd b/doc/syntax/layer/type/violin.qmd index f03c7b66..37a59560 100644 --- a/doc/syntax/layer/type/violin.qmd +++ b/doc/syntax/layer/type/violin.qmd @@ -36,8 +36,8 @@ The following aesthetics are recognised by the violin layer. * `width`: Relative width of the violins (0 to 1). Defaults to `0.9`. * `side`: Determines the sides of the midline where the density is displayed. One of the following: * `'both'` (default) displays a complete violin both sides of the midline. - * `'left'` or `'top'` only displays half a violin at the left side or top side. - * `'right'` or `'bottom'` only displays half a violin at the right side or bottom side. + * `'left'` or `'bottom'` only displays half a violin at the left side or bottom side. + * `'right'` or `'top'` only displays half a violin at the right side or top side. * `tails`: Expansion rule for drawing the tails (must be >= 0 if numeric). One of the following: * A number setting a multiple of adjusted bandwidths to expand each group's range. Defaults to 3. * `null` to use the whole data range rather than group ranges. diff --git a/src/writer/vegalite/layer.rs b/src/writer/vegalite/layer.rs index 82f61757..5a75ae35 100644 --- a/src/writer/vegalite/layer.rs +++ b/src/writer/vegalite/layer.rs @@ -1311,23 +1311,24 @@ impl GeomRenderer for ViolinRenderer { }); let offset_col = naming::aesthetic_column("offset"); + // Read orientation from layer (already resolved during execution) + let is_horizontal = is_transposed(layer); + // It'll be implemented as an offset. let mut violin_offset = format!("[datum.{offset}, -datum.{offset}]", offset = offset_col); if let Some(ParameterValue::String(side)) = layer.parameters.get("side") { - match side.as_str() { - "left" | "top" => { - violin_offset = format!("[-datum.{offset}]", offset = offset_col); - } - "right" | "bottom" => { - violin_offset = format!("[datum.{offset}]", offset = offset_col); - } - _ => {} - } + let positive = if is_horizontal { + matches!(side.as_str(), "bottom" | "left") + } else { + matches!(side.as_str(), "top" | "right") + }; + violin_offset = if positive { + format!("[datum.{offset}]", offset = offset_col) + } else { + format!("[-datum.{offset}]", offset = offset_col) + }; } - // Read orientation from layer (already resolved during execution) - let is_horizontal = is_transposed(layer); - // Continuous axis column for order calculation: // - Vertical: pos2 (y-axis has continuous density values) // - Horizontal: pos1 (x-axis has continuous density values) From 103cc95c11aef9ba1e29c5a5ca617a90c4bd634a Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Wed, 8 Apr 2026 11:57:02 +0200 Subject: [PATCH 11/11] cover all cases in tests --- src/writer/vegalite/layer.rs | 72 ++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/src/writer/vegalite/layer.rs b/src/writer/vegalite/layer.rs index 5a75ae35..f467645e 100644 --- a/src/writer/vegalite/layer.rs +++ b/src/writer/vegalite/layer.rs @@ -3235,7 +3235,7 @@ mod tests { let offset_col = naming::aesthetic_column("offset"); - fn get_violin_offset_expr(ridge: Option<&str>) -> String { + fn get_violin_offset_expr(ridge: Option<&str>, is_horizontal: bool) -> String { let mut layer = Layer::new(crate::plot::Geom::violin()); if let Some(r) = ridge { layer @@ -3243,13 +3243,31 @@ mod tests { .insert("side".to_string(), ParameterValue::String(r.to_string())); } - let mut layer_spec = json!({ - "mark": {"type": "line"}, - "encoding": { - "x": {"field": "species", "type": "nominal"}, - "y": {"field": naming::aesthetic_column("pos2"), "type": "quantitative"} - } - }); + // Set orientation parameter for horizontal case + if is_horizontal { + layer.parameters.insert( + "orientation".to_string(), + ParameterValue::String("transposed".to_string()), + ); + } + + let mut layer_spec = if is_horizontal { + json!({ + "mark": {"type": "line"}, + "encoding": { + "x": {"field": naming::aesthetic_column("pos2"), "type": "quantitative"}, + "y": {"field": "species", "type": "nominal"} + } + }) + } else { + json!({ + "mark": {"type": "line"}, + "encoding": { + "x": {"field": "species", "type": "nominal"}, + "y": {"field": naming::aesthetic_column("pos2"), "type": "quantitative"} + } + }) + }; ViolinRenderer .modify_spec(&mut layer_spec, &layer, &RenderContext::new(&[])) @@ -3266,8 +3284,8 @@ mod tests { .to_string() } - // Default "both" - mirrors on both sides - let expr = get_violin_offset_expr(None); + // Default "both" - mirrors on both sides (vertical orientation) + let expr = get_violin_offset_expr(None, false); assert!( expr.contains(&format!("[datum.{}, -datum.{}]", offset_col, offset_col)) || expr.contains(&format!("[-datum.{}, datum.{}]", offset_col, offset_col)), @@ -3275,25 +3293,47 @@ mod tests { expr ); - // "left" and "top" - only negative offset + // Vertical orientation (default): x=nominal, y=quantitative + // "left" and "bottom" - only negative offset assert_eq!( - get_violin_offset_expr(Some("left")), + get_violin_offset_expr(Some("left"), false), format!("[-datum.{}]", offset_col) ); assert_eq!( - get_violin_offset_expr(Some("top")), + get_violin_offset_expr(Some("bottom"), false), format!("[-datum.{}]", offset_col) ); - // "right" and "bottom" - only positive offset + // "right" and "top" - only positive offset assert_eq!( - get_violin_offset_expr(Some("right")), + get_violin_offset_expr(Some("right"), false), format!("[datum.{}]", offset_col) ); assert_eq!( - get_violin_offset_expr(Some("bottom")), + get_violin_offset_expr(Some("top"), false), format!("[datum.{}]", offset_col) ); + + // Horizontal orientation: x=quantitative, y=nominal + // "bottom" and "left" - only positive offset + assert_eq!( + get_violin_offset_expr(Some("bottom"), true), + format!("[datum.{}]", offset_col) + ); + assert_eq!( + get_violin_offset_expr(Some("left"), true), + format!("[datum.{}]", offset_col) + ); + + // "top" and "right" - only negative offset + assert_eq!( + get_violin_offset_expr(Some("top"), true), + format!("[-datum.{}]", offset_col) + ); + assert_eq!( + get_violin_offset_expr(Some("right"), true), + format!("[-datum.{}]", offset_col) + ); } #[test]