Skip to content
Open
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
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. 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.
Expand Down Expand Up @@ -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
```
4 changes: 4 additions & 0 deletions src/plot/layer/geom/violin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
59 changes: 58 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(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);
Expand Down Expand Up @@ -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};
Expand Down
Loading