Skip to content
12 changes: 12 additions & 0 deletions doc/syntax/layer/type/violin.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ 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`.
* `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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't left and 'bottom' be synonyms? those are the two that pointing towards the low end of the scale...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vegalite calculations just worked out this way, but I kinda agree that such consistency would be nice.

* `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.
Expand Down Expand Up @@ -106,3 +110,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 `side` setting and adjust `width` to taste.

```{ggsql}
VISUALISE Temp AS x, Month AS y FROM ggsql:airquality
DRAW violin SETTING width => 4, side => 'top'
SCALE ORDINAL y
```
10 changes: 9 additions & 1 deletion src/plot/layer/geom/violin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const KERNEL_VALUES: &[&str] = &[
"cosine",
];

const SIDE_VALUES: &[&str] = &["both", "left", "top", "right", "bottom"];

/// Violin geom - violin plots (mirrored density)
#[derive(Debug, Clone, Copy)]
pub struct Violin;
Expand Down Expand Up @@ -79,7 +81,13 @@ 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: "side",
default: DefaultParamValue::String("both"),
constraint: ParamConstraint::string_option(SIDE_VALUES),
},
ParamDefinition {
name: "tails",
Expand Down
81 changes: 80 additions & 1 deletion src/writer/vegalite/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(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);
}
_ => {}
}
}

// Read orientation from layer (already resolved during execution)
let is_horizontal = is_transposed(layer);
Expand Down Expand Up @@ -3214,6 +3225,74 @@ 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("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"}
}
});

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};
Expand Down
Loading