kuva 0.1.4

Scientific plotting library in Rust with various backends.
Documentation
# Violin Plot

A violin plot estimates the probability density of each group using kernel density estimation (KDE) and renders it as a symmetric shape — widest where data is most dense. Unlike box plots, violins reveal multi-modal and skewed distributions that a five-number summary would obscure.

**Import path:** `kuva::plot::ViolinPlot`

---

## Basic usage

Add one group per category with `.with_group(label, values)`. Groups are rendered left-to-right in the order they are added.

```rust,no_run
use kuva::plot::ViolinPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plot = ViolinPlot::new()
    .with_group("Normal",  normal_data)   // unimodal
    .with_group("Bimodal", bimodal_data)  // two peaks — invisible in a box plot
    .with_group("Skewed",  skewed_data)   // long tail
    .with_color("steelblue")
    .with_width(30.0);

let plots = vec![Plot::Violin(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Violin Plot")
    .with_y_label("Value");

let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("violin.svg", svg).unwrap();
```

<img src="../assets/violin/basic.svg" alt="Basic violin plot" width="560">

The bimodal group shows two distinct bulges — structure that a box plot would represent as a single median and IQR, losing all information about the separation.

---

## Violin width

`.with_width(px)` sets the maximum half-width of each violin in pixels. The default is `30.0`. Unlike bar width, this is an absolute pixel value, not a fraction of the category slot.

```rust,no_run
# use kuva::plot::ViolinPlot;
let plot = ViolinPlot::new()
    .with_group("A", data)
    .with_width(20.0);   // narrower violins
```

---

## KDE bandwidth

Bandwidth controls how smooth the density estimate is. The default uses Silverman's rule-of-thumb, which works well for unimodal, roughly normal data. Set it manually with `.with_bandwidth(h)` when the automatic choice is too smooth (hides modes) or too rough (noisy).

```rust,no_run
use kuva::plot::ViolinPlot;
# use kuva::render::plots::Plot;

// Too narrow — jagged, noisy
let plot = ViolinPlot::new().with_group("", data.clone()).with_bandwidth(0.15);

// Automatic — Silverman's rule (default, no call needed)
let plot = ViolinPlot::new().with_group("", data.clone());

// Too wide — over-smoothed, modes blend together
let plot = ViolinPlot::new().with_group("", data.clone()).with_bandwidth(2.0);
```

<table>
<tr>
<td><img src="../assets/violin/bandwidth_narrow.svg" alt="Narrow bandwidth" width="186"></td>
<td><img src="../assets/violin/bandwidth_auto.svg" alt="Auto bandwidth" width="186"></td>
<td><img src="../assets/violin/bandwidth_wide.svg" alt="Wide bandwidth" width="186"></td>
</tr>
<tr>
<td align="center"><code>h = 0.15</code> (too narrow)</td>
<td align="center">Auto — Silverman</td>
<td align="center"><code>h = 2.0</code> (too wide)</td>
</tr>
</table>

`.with_kde_samples(n)` sets the number of points at which the density is evaluated (default `200`). Increase it for a smoother rendered curve; the default is adequate for most datasets.

---

## Point overlays

Adding individual points on top of the violin makes the sample size visible and helps readers judge the reliability of the density estimate.

### Beeswarm overlay

`.with_swarm_overlay()` spreads points horizontally to avoid overlap. Recommended for smaller datasets (roughly N < 200 per group).

```rust,no_run
use kuva::plot::ViolinPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plot = ViolinPlot::new()
    .with_group("Normal",  normal_data)
    .with_group("Bimodal", bimodal_data)
    .with_group("Skewed",  skewed_data)
    .with_color("steelblue")
    .with_width(30.0)
    .with_swarm_overlay()
    .with_overlay_color("rgba(0,0,0,0.35)")
    .with_overlay_size(2.5);

let plots = vec![Plot::Violin(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Violin + Swarm Overlay")
    .with_y_label("Value");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
```

<img src="../assets/violin/swarm_overlay.svg" alt="Violin with swarm overlay" width="560">

### Jittered strip

`.with_strip(jitter)` places points with random horizontal offsets. More appropriate for large datasets where beeswarm layout becomes slow.

```rust,no_run
# use kuva::plot::ViolinPlot;
let plot = ViolinPlot::new()
    .with_group("A", data)
    .with_color("steelblue")
    .with_strip(0.15)                       // jitter spread in data units
    .with_overlay_color("rgba(0,0,0,0.4)") // semi-transparent recommended
    .with_overlay_size(3.0);
```

---

## Per-group colors

Color each group independently within a single `ViolinPlot` using `.with_group_colors()`. Colors are matched to groups by position — the first color applies to the first group added, and so on. The uniform `.with_color()` value is used as a fallback for any group without an entry.

```rust,no_run
use kuva::plot::ViolinPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plot = ViolinPlot::new()
    .with_group("Normal",  normal_samples(0.0, 1.0, 300, 1))
    .with_group("Bimodal", bimodal_samples(-2.0, 2.0, 0.6, 300, 2))
    .with_group("Skewed",  skewed_samples(300, 3))
    .with_group_colors(["steelblue", "tomato", "seagreen"])
    .with_width(30.0);

let plots = vec![Plot::Violin(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Violin Plot — Per-group Colors")
    .with_y_label("Value");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
```

<img src="../assets/violin/group_colors.svg" alt="Violin plot with per-group colors" width="560">

A partial list is also valid — groups beyond the list length fall back to the uniform `.with_color()` value:

```rust,no_run
# use kuva::plot::ViolinPlot;
let plot = ViolinPlot::new()
    .with_group("Normal",  vec![/* values */])
    .with_group("Bimodal", vec![/* values */])
    .with_group("Skewed",  vec![/* values */])
    .with_color("steelblue")        // fallback for groups 1 and 2
    .with_group_colors(["tomato"]); // only group 0 gets this color
```

> **Legend note:** the legend entry uses the uniform `.with_color()` value. For a fully labeled per-group legend, create one `ViolinPlot` per group and attach `.with_legend()` to each, then use a `Layout::with_palette()` to auto-assign colors.

---

## API reference

| Method | Description |
|--------|-------------|
| `ViolinPlot::new()` | Create a violin plot with defaults |
| `.with_group(label, values)` | Add a group; accepts any `Into<f64>` iterable |
| `.with_color(s)` | Violin fill color (CSS color string) |
| `.with_group_colors(iter)` | Per-group fill colors; falls back to `.with_color` for out-of-range indices |
| `.with_width(px)` | Maximum half-width of each violin in pixels (default `30.0`) |
| `.with_legend(s)` | Attach a legend label |
| `.with_bandwidth(h)` | KDE bandwidth; omit for Silverman's rule (recommended default) |
| `.with_kde_samples(n)` | KDE evaluation points (default `200`) |
| `.with_strip(jitter)` | Overlay jittered strip; `jitter` is spread width in data units |
| `.with_swarm_overlay()` | Overlay beeswarm points (spread to avoid overlap) |
| `.with_overlay_color(s)` | Overlay point color (default `"rgba(0,0,0,0.45)"`) |
| `.with_overlay_size(r)` | Overlay point radius in pixels (default `3.0`) |