kuva 0.1.4

Scientific plotting library in Rust with various backends.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# Scatter Plot

A scatter plot renders individual (x, y) data points as markers. It supports trend lines, error bars, variable point sizes, per-point colors, and six marker shapes.

**Import path:** `kuva::plot::scatter::ScatterPlot`

---

## Basic usage

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

let data = vec![
    (0.5_f64, 1.2_f64),
    (1.4, 3.1),
    (2.1, 2.4),
    (3.3, 5.0),
    (4.0, 4.3),
    (5.2, 6.8),
    (6.1, 6.0),
    (7.0, 8.5),
    (8.4, 7.9),
    (9.1, 9.8),
];

let plot = ScatterPlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_size(5.0);

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Scatter Plot")
    .with_x_label("X")
    .with_y_label("Y");

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

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

### Layout options

`Layout::auto_from_plots()` automatically computes axis ranges from the data. You can also set ranges manually with `Layout::new((x_min, x_max), (y_min, y_max))`.

---

## Trend line

Add a linear trend line with `.with_trend(TrendLine::Linear)`. Optionally overlay the regression equation and the Pearson R² value.

```rust,no_run
use kuva::plot::scatter::{ScatterPlot, TrendLine};
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data = vec![
    (1.0_f64, 2.1_f64), (2.0, 3.9), (3.0, 6.2),
    (4.0, 7.8), (5.0, 10.1), (6.0, 12.3),
    (7.0, 13.9), (8.0, 16.2), (9.0, 17.8), (10.0, 19.7),
];

let plot = ScatterPlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_size(5.0)
    .with_trend(TrendLine::Linear)
    .with_trend_color("crimson")   // defaults to "black"
    .with_equation()               // show y = mx + b
    .with_correlation();           // show R²

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Linear Trend Line")
    .with_x_label("X")
    .with_y_label("Y");

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

<img src="../assets/scatter/trend.svg" alt="Scatter with linear trend line" width="560">

---

## Confidence band

Attach a shaded uncertainty region with `.with_band(y_lower, y_upper)`. Both slices must align with the x positions of the scatter data. The band color matches the point color.

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

let xs: Vec<f64> = (1..=10).map(|i| i as f64).collect();
let ys: Vec<f64> = xs.iter().map(|&x| x * 1.8 + 0.5).collect();
let lower: Vec<f64> = ys.iter().map(|&y| y - 1.2).collect();
let upper: Vec<f64> = ys.iter().map(|&y| y + 1.2).collect();

let data: Vec<(f64, f64)> = xs.into_iter().zip(ys).collect();

let plot = ScatterPlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_size(5.0)
    .with_band(lower, upper);

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Confidence Band")
    .with_x_label("X")
    .with_y_label("Y");

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

<img src="../assets/scatter/confidence_band.svg" alt="Scatter with confidence band" width="560">

---

## Error bars

Use `.with_x_err()` and `.with_y_err()` for symmetric error bars. Asymmetric variants accept `(negative, positive)` tuples.

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

let data = vec![
    (1.0_f64, 2.0_f64), (2.0, 4.5), (3.0, 5.8),
    (4.0, 8.2), (5.0, 10.1),
];
let x_err = vec![0.2_f64, 0.15, 0.3, 0.1, 0.25];  // symmetric
let y_err = vec![0.6_f64, 0.8, 0.4, 0.9, 0.5];     // symmetric

let plot = ScatterPlot::new()
    .with_data(data)
    .with_x_err(x_err)
    .with_y_err(y_err)
    .with_color("steelblue")
    .with_size(5.0);

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Error Bars")
    .with_x_label("X")
    .with_y_label("Y");

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

<img src="../assets/scatter/error_bars.svg" alt="Scatter with error bars" width="560">

### Asymmetric errors

Pass `(neg, pos)` tuples instead of scalar values:

```rust,no_run
# use kuva::plot::scatter::ScatterPlot;
let data = vec![(1.0_f64, 5.0_f64), (2.0, 6.0)];
let y_err = vec![(0.3_f64, 0.8_f64), (0.5, 1.2)];  // (neg, pos)

let plot = ScatterPlot::new()
    .with_data(data)
    .with_y_err_asymmetric(y_err);
```

---

## Marker shapes

Six marker shapes are available via `MarkerShape`. They are particularly useful when overlaying multiple series on the same axes.

```rust,no_run
use kuva::plot::scatter::{ScatterPlot, MarkerShape};
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plots = vec![
    Plot::Scatter(ScatterPlot::new()
        .with_data(vec![(1.0_f64, 1.0_f64), (2.0, 1.0), (3.0, 1.0)])
        .with_color("steelblue").with_size(7.0)
        .with_marker(MarkerShape::Circle).with_legend("Circle")),

    Plot::Scatter(ScatterPlot::new()
        .with_data(vec![(1.0_f64, 2.0_f64), (2.0, 2.0), (3.0, 2.0)])
        .with_color("crimson").with_size(7.0)
        .with_marker(MarkerShape::Square).with_legend("Square")),

    Plot::Scatter(ScatterPlot::new()
        .with_data(vec![(1.0_f64, 3.0_f64), (2.0, 3.0), (3.0, 3.0)])
        .with_color("seagreen").with_size(7.0)
        .with_marker(MarkerShape::Triangle).with_legend("Triangle")),
];

let layout = Layout::auto_from_plots(&plots)
    .with_title("Marker Shapes")
    .with_x_label("X")
    .with_y_label("");

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

Available variants: `Circle` (default), `Square`, `Triangle`, `Diamond`, `Cross`, `Plus`.

<img src="../assets/scatter/markers.svg" alt="Scatter marker shapes" width="560">

---

## Bubble plot

Encode a third dimension through point area using `.with_sizes()`. Values are point radii in pixels.

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

let data = vec![
    (1.0_f64, 3.0_f64), (2.5, 6.5), (4.0, 4.0),
    (5.5, 8.0), (7.0, 5.5), (8.5, 9.0),
];
let sizes = vec![5.0_f64, 14.0, 9.0, 18.0, 11.0, 7.0];

let plot = ScatterPlot::new()
    .with_data(data)
    .with_sizes(sizes)
    .with_color("steelblue");

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Bubble Plot")
    .with_x_label("X")
    .with_y_label("Y");

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

<img src="../assets/scatter/bubble.svg" alt="Bubble plot" width="560">

---

## Per-point colors

Encode a categorical grouping through color using `.with_colors()`. Colors are matched to points by index and fall back to the uniform `.with_color()` value for any point without an entry.

This is useful when your data already carries a group label and you want to avoid splitting into multiple `ScatterPlot` instances. The legend is **not** updated automatically — add `.with_legend()` on separate `ScatterPlot` instances when you need a labeled legend.

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

// Three clusters, colors assigned per point
let data = vec![
    (1.0_f64, 1.5_f64), (1.5, 2.0), (2.0, 1.8),  // cluster A
    (4.0, 4.5),         (4.5, 5.0), (5.0, 4.8),  // cluster B
    (7.0, 2.0),         (7.5, 2.5), (8.0, 2.2),  // cluster C
];
let colors = vec![
    "steelblue", "steelblue", "steelblue",
    "crimson",   "crimson",   "crimson",
    "seagreen",  "seagreen",  "seagreen",
];

let plot = ScatterPlot::new()
    .with_data(data)
    .with_colors(colors)
    .with_size(6.0);

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Per-Point Colors")
    .with_x_label("X")
    .with_y_label("Y");

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

<img src="../assets/scatter/per_point_colors.svg" alt="Scatter with per-point colors" width="560">

---

## Marker opacity and stroke

Two builders control the visual style of markers, enabling three distinct modes useful for dense datasets:

| Mode | Setting | Use case |
|------|---------|----------|
| **Solid** (default) | no calls needed | Small N or well-separated clusters |
| **Semi-transparent** | `opacity < 1` + stroke | Dense regions pool colour; individual points stay visible |
| **Hollow** | `opacity = 0.0` + stroke | Very large N; overlapping rings reveal density without blobs |

### Semi-transparent markers — overlapping clusters

Three Gaussian clusters of 200 points each share a region in the centre. Solid markers at this density merge into a single opaque mass. Reducing opacity to `0.25` lets the darker overlap region show where clusters share space, while the `0.7 px` stroke keeps each marker individually legible.

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

// Three clusters, 200 points each — defined as (center_x, center_y, color, label)
let series = [
    (3.0_f64, 4.0_f64, "steelblue", "Cluster A"),
    (5.0,     5.5,     "tomato",    "Cluster B"),
    (4.0,     3.0,     "seagreen",  "Cluster C"),
];

// (populate `data` from your source — each entry is 200 (x, y) points)

# let data: Vec<(f64,f64)> = vec![];
let plot = ScatterPlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_size(5.0)
    .with_marker_opacity(0.25)
    .with_marker_stroke_width(0.7)
    .with_legend("Cluster A");

let plots = vec![Plot::Scatter(plot) /* , ... */];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Overlapping Clusters — semi-transparent markers")
    .with_x_label("X")
    .with_y_label("Y");

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

<img src="../assets/scatter/marker_semi_transparent.svg" alt="Three overlapping Gaussian clusters with semi-transparent markers" width="560">

### Hollow open circles — dense annular data

800 points sampled uniformly along a noisy ring. With solid fill the ring becomes a uniform band; hollow circles (`opacity = 0.0`) make the denser arc sections visible through the accumulation of overlapping outlines.

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

// 800 points sampled along a noisy annulus of radius ≈ 3
# let data: Vec<(f64,f64)> = vec![];
let plot = ScatterPlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_size(4.0)
    .with_marker_opacity(0.0)       // fully hollow
    .with_marker_stroke_width(1.0); // only the outline is drawn

let plots = vec![Plot::Scatter(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Hollow open circles — 800 pts in a noisy annulus")
    .with_x_label("X")
    .with_y_label("Y");

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

<img src="../assets/scatter/marker_hollow.svg" alt="800 hollow open circles forming a noisy ring" width="560">

---

## Multiple series

Wrap multiple `ScatterPlot` structs in a `Vec<Plot>` and pass them to `render_multiple()`. Legends are shown when any series has a label attached via `.with_legend()`.

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

let series_a = ScatterPlot::new()
    .with_data(vec![(1.0_f64, 2.0_f64), (3.0, 4.0), (5.0, 3.5)])
    .with_color("steelblue")
    .with_legend("Series A");

let series_b = ScatterPlot::new()
    .with_data(vec![(1.0_f64, 5.0_f64), (3.0, 6.5), (5.0, 7.0)])
    .with_color("crimson")
    .with_legend("Series B");

let plots = vec![Plot::Scatter(series_a), Plot::Scatter(series_b)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Two Series")
    .with_x_label("X")
    .with_y_label("Y");

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

<img src="../assets/scatter/multiple_series.svg" alt="Multiple scatter series with legend" width="560">

---

## API reference

| Method | Description |
|--------|-------------|
| `ScatterPlot::new()` | Create a new scatter plot with defaults |
| `.with_data(iter)` | Set (x, y) data; accepts any `Into<f64>` numeric type |
| `.with_color(s)` | Set uniform point color (CSS color string, default `"black"`) |
| `.with_colors(iter)` | Set per-point colors; falls back to `.with_color` for out-of-range indices |
| `.with_size(r)` | Set uniform point radius in pixels (default 3.0) |
| `.with_sizes(iter)` | Set per-point radii (bubble plot); falls back to `.with_size` for out-of-range indices |
| `.with_marker(MarkerShape)` | Set marker shape (default `Circle`) |
| `.with_legend(s)` | Attach a legend label to this series |
| `.with_trend(TrendLine)` | Overlay a trend line |
| `.with_trend_color(s)` | Set trend line color |
| `.with_trend_width(w)` | Set trend line stroke width |
| `.with_equation()` | Annotate the plot with the regression equation |
| `.with_correlation()` | Annotate the plot with R² |
| `.with_x_err(iter)` | Symmetric X error bars |
| `.with_x_err_asymmetric(iter)` | Asymmetric X error bars: `(neg, pos)` tuples |
| `.with_y_err(iter)` | Symmetric Y error bars |
| `.with_y_err_asymmetric(iter)` | Asymmetric Y error bars: `(neg, pos)` tuples |
| `.with_band(lower, upper)` | Confidence band aligned to scatter x positions |
| `.with_marker_opacity(f)` | Fill alpha: `0.0` = hollow, `1.0` = solid (default: solid) |
| `.with_marker_stroke_width(w)` | Outline stroke at the fill color; `None` = no stroke (default) |

### `MarkerShape` variants

`Circle` · `Square` · `Triangle` · `Diamond` · `Cross` · `Plus`

### `TrendLine` variants

`Linear` — fits y = mx + b by ordinary least squares.