kuva 0.2.0

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

A parallel coordinates plot displays multivariate data by drawing one vertical axis per dimension and connecting each observation as a polyline that passes through its value on each axis. Groups of observations that share a similar pattern appear as bundles of lines with similar trajectories; divergent groups cross each other clearly.

Parallel coordinates are useful for exploring high-dimensional datasets, comparing groups across many measured attributes, and identifying which dimensions best separate groups.

**Import path:** `kuva::plot::parallel::{ParallelPlot, ParallelRow}`

---

## Basic usage

Set axis names with `.with_axis_names()`, then add rows with `.with_row_group(group, values)`. Each row is one observation; values must be in the same order as the axis names.

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

let plot = ParallelPlot::new()
    .with_axis_names(["Sepal L", "Sepal W", "Petal L", "Petal W"])
    .with_row_group("setosa",     vec![5.1, 3.5, 1.4, 0.2])
    .with_row_group("setosa",     vec![4.9, 3.0, 1.4, 0.2])
    .with_row_group("setosa",     vec![4.7, 3.2, 1.3, 0.2])
    .with_row_group("versicolor", vec![7.0, 3.2, 4.7, 1.4])
    .with_row_group("versicolor", vec![6.4, 3.2, 4.5, 1.5])
    .with_row_group("versicolor", vec![6.9, 3.1, 4.9, 1.5])
    .with_row_group("virginica",  vec![6.3, 3.3, 6.0, 2.5])
    .with_row_group("virginica",  vec![5.8, 2.7, 5.1, 1.9])
    .with_row_group("virginica",  vec![7.1, 3.0, 5.9, 2.1])
    .with_legend("Species");

let plots = vec![Plot::Parallel(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Iris Dataset");

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

<img src="../assets/parallel/basic.svg" alt="Parallel coordinates plot of the Iris dataset" width="560">

Each axis is normalised to `[0, 1]` by default so that differently-scaled dimensions are comparable. Disable with `.with_normalize(false)` when all axes share a common unit.

---

## Smooth curves

`.with_curved(true)` draws S-shaped cubic Bézier curves instead of straight polylines, which reduces visual clutter in dense plots.

```rust,no_run
use kuva::plot::parallel::ParallelPlot;
use kuva::render::plots::Plot;
# use kuva::render::layout::Layout;
# use kuva::render::render::render_multiple;

let plot = ParallelPlot::new()
    .with_axis_names(["Sepal L", "Sepal W", "Petal L", "Petal W"])
    .with_row_group("setosa",     vec![5.1, 3.5, 1.4, 0.2])
    .with_row_group("setosa",     vec![4.9, 3.0, 1.4, 0.2])
    .with_row_group("versicolor", vec![7.0, 3.2, 4.7, 1.4])
    .with_row_group("versicolor", vec![6.4, 3.2, 4.5, 1.5])
    .with_row_group("virginica",  vec![6.3, 3.3, 6.0, 2.5])
    .with_row_group("virginica",  vec![7.1, 3.0, 5.9, 2.1])
    .with_curved(true)
    .with_opacity(0.7)
    .with_legend("Species");

let plots = vec![Plot::Parallel(plot)];
```

<img src="../assets/parallel/curved.svg" alt="Parallel coordinates with smooth Bézier curves" width="560">

---

## Group mean overlay

`.with_mean(true)` draws a bold polyline at the per-group mean for each axis. This makes the group-level pattern visible even when individual lines are dense.

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

let plot = ParallelPlot::new()
    .with_axis_names(["Recall", "Precision", "F1", "AUC", "Inference ms"])
    // Multiple runs per model
    .with_group_rows("BERT",  [
        vec![0.82, 0.85, 0.83, 0.91, 120.0],
        vec![0.80, 0.87, 0.83, 0.90, 115.0],
        vec![0.83, 0.84, 0.83, 0.92, 125.0],
    ])
    .with_group_rows("DistilBERT", [
        vec![0.78, 0.80, 0.79, 0.87,  55.0],
        vec![0.77, 0.82, 0.79, 0.86,  52.0],
        vec![0.79, 0.81, 0.80, 0.88,  58.0],
    ])
    .with_group_rows("LSTM", [
        vec![0.72, 0.75, 0.73, 0.82,  30.0],
        vec![0.71, 0.76, 0.73, 0.81,  28.0],
        vec![0.73, 0.74, 0.73, 0.83,  32.0],
    ])
    .with_mean(true)
    .with_opacity(0.35)
    .with_curved(true)
    .with_legend("Model");

let plots = vec![Plot::Parallel(plot)];
let layout = Layout::auto_from_plots(&plots).with_title("NLP Model Comparison");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
```

<img src="../assets/parallel/mean_overlay.svg" alt="Parallel coordinates with group mean overlay" width="560">

---

## Axis inversion

Some axes are naturally "better when low" (e.g., error rate, latency). `.with_inverted_axis(i)` inverts axis `i` so that high values plot near the bottom — a visual triangle at the bottom of the axis label indicates inversion.

```rust,no_run
use kuva::plot::parallel::ParallelPlot;
use kuva::render::plots::Plot;
# use kuva::render::layout::Layout;
# use kuva::render::render::render_multiple;

// Axes: Accuracy (higher = better), Error rate (lower = better), Speed ms (lower = better), F1 (higher = better)
let plot = ParallelPlot::new()
    .with_axis_names(["Accuracy", "Error rate", "Speed (ms)", "F1"])
    .with_row_group("Model A", vec![0.92, 0.08, 120.0, 0.90])
    .with_row_group("Model A", vec![0.91, 0.09, 115.0, 0.89])
    .with_row_group("Model B", vec![0.87, 0.13,  45.0, 0.86])
    .with_row_group("Model B", vec![0.88, 0.12,  48.0, 0.87])
    .with_inverted_axes([1, 2])  // invert Error rate and Speed: down = better
    .with_mean(true)
    .with_legend("Model");

let plots = vec![Plot::Parallel(plot)];
```

---

## ParallelPlot API reference

### `ParallelPlot` builders

| Method | Default | Description |
|--------|---------|-------------|
| `ParallelPlot::new()` || Create a parallel coordinates plot |
| `.with_axis_names(iter)` || Set axis (column) names |
| `.with_row(values)` || Add an ungrouped row |
| `.with_row_group(group, values)` || Add a row assigned to a named group |
| `.with_rows(iter)` || Add multiple ungrouped rows |
| `.with_group_rows(group, iter)` || Add multiple rows to the same group |
| `.with_normalize(bool)` | `true` | Normalise each axis independently to `[0, 1]` |
| `.with_curved(bool)` | `false` | Draw smooth S-shaped Bézier curves |
| `.with_stroke_width(px)` | `1.2` | Polyline stroke width |
| `.with_opacity(f)` | `0.6` | Polyline opacity |
| `.with_color(css)` | `"steelblue"` | Fallback color for ungrouped rows |
| `.with_group_colors(iter)` | palette | Explicit per-group CSS colors |
| `.with_mean(bool)` | `false` | Draw a bold mean line for each group |
| `.with_inverted_axis(i)` || Invert axis `i` (high values at bottom) |
| `.with_inverted_axes(iter)` || Invert multiple axes |
| `.with_legend(label)` || Legend title (one entry per group) |