diff --git a/doc/syntax/layer/type/violin.qmd b/doc/syntax/layer/type/violin.qmd index acc55eb8..af5b3243 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. * `tails`: Expansion rule for drawing the tails. 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. @@ -103,3 +107,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 diff --git a/src/plot/layer/geom/violin.rs b/src/plot/layer/geom/violin.rs index b213ebb7..4b842482 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"), + }, DefaultParam { name: "tails", default: DefaultParamValue::Number(3.0), diff --git a/src/writer/vegalite/layer.rs b/src/writer/vegalite/layer.rs index 3a82cf19..8ff56eca 100644 --- a/src/writer/vegalite/layer.rs +++ b/src/writer/vegalite/layer.rs @@ -1312,7 +1312,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); @@ -3198,6 +3209,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};