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
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
# Layout & Axes

`Layout` is the single configuration struct passed to every render function. It controls axis ranges, labels, tick marks, log scale, canvas size, annotations, and typography. Every plot type goes through a `Layout` before becoming an SVG.

**Import path:** `kuva::render::layout::Layout`

---

## Constructors

### `Layout::auto_from_plots()`

The recommended starting point. Inspects the data in a `Vec<Plot>` and automatically computes axis ranges, padding, legend visibility, and colorbar presence.

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];
let layout = Layout::auto_from_plots(&plots)
    .with_title("My Plot")
    .with_x_label("X")
    .with_y_label("Y");
```

### `Layout::new()`

Sets explicit axis ranges. Use this when you need precise control — for example, when comparing multiple plots that must share the same scale, or when the auto-range would include unwanted padding.

```rust,no_run
# use kuva::render::layout::Layout;
// x from 0 to 100, y from -1 to 1
let layout = Layout::new((0.0, 100.0), (-1.0, 1.0))
    .with_title("Fixed Range")
    .with_x_label("Time (ms)")
    .with_y_label("Amplitude");
```

---

## Labels and title

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];
let layout = Layout::auto_from_plots(&plots)
    .with_title("My Plot")          // text above the plot area
    .with_x_label("Concentration")  // label below the x-axis
    .with_y_label("Response (%)");  // label left of the y-axis
```

---

## Canvas size

The default canvas is `600 × 450` pixels for the plot area, with margins computed automatically from the title, tick labels, and legend. Override either dimension:

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];
let layout = Layout::auto_from_plots(&plots)
    .with_width(800.0)   // total SVG width in pixels
    .with_height(300.0); // total SVG height in pixels
```

---

## Ticks

The number of tick marks is chosen automatically based on the canvas size. Override it with `.with_ticks()`:

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];
let layout = Layout::auto_from_plots(&plots)
    .with_ticks(8);  // request approximately 8 tick intervals
```

### Tick formats

`TickFormat` controls how numeric tick labels are rendered. Import it from `kuva::TickFormat`.

| Variant | Example output | Use case |
|---------|----------------|----------|
| `Auto` *(default)* | `5`, `3.14`, `1.2e5` | General purpose — integers without `.0`, minimal decimals, sci notation for extremes |
| `Fixed(n)` | `3.14` (n=2) | Fixed decimal places |
| `Integer` | `5` | Round to nearest integer |
| `Sci` | `1.23e4` | Always scientific notation |
| `Percent` | `45.0%` | Multiply by 100 and append `%` — for data in the range 0–1 |
| `Custom(fn)` | anything | Provide your own `fn(f64) -> String` |

Apply the same format to both axes, or set them independently:

```rust,no_run
use kuva::TickFormat;
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];

// Same format on both axes
let layout = Layout::auto_from_plots(&plots)
    .with_tick_format(TickFormat::Fixed(2));

// Independent formats
let layout = Layout::auto_from_plots(&plots)
    .with_x_tick_format(TickFormat::Percent)
    .with_y_tick_format(TickFormat::Sci);

// Custom formatter — append a unit suffix
let layout = Layout::auto_from_plots(&plots)
    .with_y_tick_format(TickFormat::Custom(
        std::sync::Arc::new(|v| format!("{:.0} ms", v))
    ));
```

<table>
<tr>
<td><img src="../assets/layout/tick_auto.svg" alt="Auto tick format" width="240"></td>
<td><img src="../assets/layout/tick_fixed.svg" alt="Fixed(2) tick format" width="240"></td>
</tr>
<tr>
<td align="center"><code>Auto</code></td>
<td align="center"><code>Fixed(2)</code></td>
</tr>
<tr>
<td><img src="../assets/layout/tick_percent.svg" alt="Percent tick format" width="240"></td>
<td><img src="../assets/layout/tick_sci.svg" alt="Sci tick format" width="240"></td>
</tr>
<tr>
<td align="center"><code>Percent</code></td>
<td align="center"><code>Sci</code></td>
</tr>
</table>

### Tick rotation

Rotate x-axis tick labels when category names are long:

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];
let layout = Layout::auto_from_plots(&plots)
    .with_x_tick_rotate(45.0);  // degrees; 45 or 90 are common
```

---

## Log scale

Enable logarithmic axes for data spanning multiple orders of magnitude. Ticks are placed at powers of 10; narrow ranges add 2× and 5× sub-ticks automatically.

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];
// Both axes log
let layout = Layout::auto_from_plots(&plots).with_log_scale();

// X axis only
let layout = Layout::auto_from_plots(&plots).with_log_x();

// Y axis only
let layout = Layout::auto_from_plots(&plots).with_log_y();
```

<img src="../assets/layout/log_scale.svg" alt="Log-scale axes" width="560">

All data values must be positive when using a log axis. `auto_from_plots` uses the raw data range (before padding) to compute log-scale tick positions, so zero-inclusive ranges are handled safely.

---

## Grid

The grid is shown by default. Disable it with:

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];
let layout = Layout::auto_from_plots(&plots)
    .with_show_grid(false);
```

Some plot types (Manhattan, UpSet) suppress the grid automatically.

---

## Annotations

Three types of annotation are available, all added via the `Layout` builder. Any number of each can be chained.

### Text annotation

Places a text label at a data coordinate. Optionally draws an arrow pointing to a different coordinate.

```rust,no_run
use kuva::render::annotations::TextAnnotation;
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];

let layout = Layout::auto_from_plots(&plots)
    .with_annotation(
        TextAnnotation::new("Outlier", 5.0, 7.5)   // text at (5, 7.5)
            .with_arrow(6.0, 9.0)                   // arrow points to (6, 9)
            .with_color("crimson")
            .with_font_size(12),                    // optional, default 12
    );
```

<img src="../assets/layout/text_annotation.svg" alt="Text annotation with arrow" width="560">

### Reference line

Draws a dashed line across the full plot area at a fixed x or y value.

```rust,no_run
use kuva::render::annotations::ReferenceLine;
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];

let layout = Layout::auto_from_plots(&plots)
    .with_reference_line(
        ReferenceLine::horizontal(0.05)     // y = 0.05
            .with_color("crimson")
            .with_label("p = 0.05"),        // optional label at right edge
    )
    .with_reference_line(
        ReferenceLine::vertical(3.5)        // x = 3.5
            .with_color("steelblue")
            .with_label("cutoff")
            .with_stroke_width(1.5)         // optional, default 1.0
            .with_dasharray("8 4"),         // optional, default "6 4"
    );
```

<img src="../assets/layout/reference_line.svg" alt="Reference lines" width="560">

### Shaded region

Fills a horizontal or vertical band across the plot area.

```rust,no_run
use kuva::render::annotations::ShadedRegion;
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];

let layout = Layout::auto_from_plots(&plots)
    .with_shaded_region(
        ShadedRegion::horizontal(2.0, 4.0)  // y band from 2 to 4
            .with_color("gold")
            .with_opacity(0.2),
    )
    .with_shaded_region(
        ShadedRegion::vertical(10.0, 20.0)  // x band from 10 to 20
            .with_color("steelblue")
            .with_opacity(0.15),
    );
```

<img src="../assets/layout/shaded_region.svg" alt="Shaded regions" width="560">

---

## Scale

`with_scale(f)` applies a single multiplier to every piece of plot chrome — font sizes, margins, tick mark lengths, stroke widths, legend padding and swatch geometry, and annotation arrow sizes. The default is `1.0` (no change). The canvas `width` and `height` are **not** affected.

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Growth Rate")
    .with_x_label("Time (weeks)")
    .with_y_label("Count")
    .with_scale(2.0);  // everything twice as large
```

The four scale levels below all use the same default canvas size (`600 × 450` plot area). Notice how at `0.5×` the chrome feels cramped while at `2.0×` tick labels and the legend are clearly legible even when the SVG is scaled down in a browser:

<table>
<tr>
<td><img src="../assets/scale/scale_0_5x.svg" alt="scale 0.5x" width="280"></td>
<td><img src="../assets/scale/scale_1x.svg" alt="scale 1.0x" width="280"></td>
</tr>
<tr>
<td align="center"><code>.with_scale(0.5)</code></td>
<td align="center"><code>.with_scale(1.0)</code> — default</td>
</tr>
<tr>
<td><img src="../assets/scale/scale_1_5x.svg" alt="scale 1.5x" width="280"></td>
<td><img src="../assets/scale/scale_2x.svg" alt="scale 2.0x" width="280"></td>
</tr>
<tr>
<td align="center"><code>.with_scale(1.5)</code></td>
<td align="center"><code>.with_scale(2.0)</code></td>
</tr>
</table>

### Combining scale with canvas size

`with_scale` makes the chrome proportionally larger but keeps the plot area the same size. At `2.0×` the default canvas will feel tight because the margins (which scale) eat into the fixed-size plot area. To keep the same visual balance as the default, scale the canvas dimensions by the same factor:

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];
let scale = 2.0_f64;
let layout = Layout::auto_from_plots(&plots)
    .with_scale(scale)
    .with_width(1200.0)   // 600 * scale
    .with_height(900.0);  // 450 * scale
```

<img src="../assets/scale/scale_with_larger_canvas.svg" alt="scale 2x with larger canvas" width="560">

The result has the same data density as the default but every pixel measurement is doubled — useful for publication figures that will be embedded at a reduced size.

### Limitations — what you must adjust manually

Two categories of user-set values are **not** auto-scaled because they are specified explicitly when constructing the object, not derived from `Layout`:

#### 1. `TextAnnotation::font_size`

`TextAnnotation` has its own `font_size` field (default `12`). When you call `.with_scale(2.0)`, the annotation arrow and its stroke scale automatically, but the text does not. Scale it in the constructor:

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# use kuva::render::annotations::TextAnnotation;
# let plots: Vec<Plot> = vec![];
let scale = 2.0_f64;
let layout = Layout::auto_from_plots(&plots)
    .with_annotation(
        TextAnnotation::new("Peak", 9.0, 16.0)
            .with_arrow(9.0, 16.0)
            .with_font_size((11.0 * scale).round() as u32),  // scale manually
    )
    .with_scale(scale);
```

The two SVGs below use `with_scale(2.0)`. In the left one the annotation font is the default `11px` regardless of scale; in the right one it has been multiplied by `2.0`:

<table>
<tr>
<td><img src="../assets/scale/annotation_not_scaled.svg" alt="annotation not scaled" width="280"></td>
<td><img src="../assets/scale/annotation_scaled_manually.svg" alt="annotation scaled manually" width="280"></td>
</tr>
<tr>
<td align="center">annotation font unchanged (11 px)</td>
<td align="center">annotation font doubled (22 px)</td>
</tr>
</table>

#### 2. `ReferenceLine::stroke_width`

`ReferenceLine` stores its own `stroke_width` (default `1.0`). The line is drawn at exactly that pixel width regardless of `with_scale`. Multiply it manually:

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# use kuva::render::annotations::ReferenceLine;
# let plots: Vec<Plot> = vec![];
let scale = 2.0_f64;
let layout = Layout::auto_from_plots(&plots)
    .with_reference_line(
        ReferenceLine::horizontal(10.0)
            .with_stroke_width(1.0 * scale),  // scale manually
    )
    .with_scale(scale);
```

#### 3. PNG raster output — use DPI scale instead

For raster output, `PngBackend` already has its own DPI multiplier:

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# use kuva::render::render::render_multiple;
# let plots: Vec<Plot> = vec![];
# let layout = Layout::auto_from_plots(&plots);
#[cfg(feature = "raster")]
{
    use kuva::backend::png::PngBackend;
    let scene = render_multiple(plots, layout);
    // Render at 3× pixel density — no need for with_scale
    let png = PngBackend::new().with_scale(3.0).render_scene(&scene);
}
```

The two mechanisms are independent and can be combined, but doing so is rarely necessary. Use `Layout::with_scale` when you want a larger SVG; use `PngBackend::with_scale` when you want a higher-DPI PNG from an unchanged SVG layout.

---

## Typography

Font family and sizes for all text elements. Sizes are in pixels.

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];
let layout = Layout::auto_from_plots(&plots)
    .with_font_family("Arial, sans-serif")  // default: "DejaVu Sans, Liberation Sans, Arial, sans-serif"
    .with_title_size(20)                    // default: 18
    .with_label_size(14)                    // default: 14  (axis labels)
    .with_tick_size(11)                     // default: 12  (tick labels)
    .with_body_size(12);                    // default: 12  (legend, annotations)
```

These can also be set via a `Theme` — see the [Themes](./themes.md) reference.

---

## Quick reference

### Layout constructors

| Method | Description |
|--------|-------------|
| `Layout::new(x_range, y_range)` | Explicit axis ranges |
| `Layout::auto_from_plots(&plots)` | Auto-compute ranges and layout from data |

### Axes and labels

| Method | Description |
|--------|-------------|
| `.with_title(s)` | Plot title |
| `.with_x_label(s)` | X-axis label |
| `.with_y_label(s)` | Y-axis label |
| `.with_ticks(n)` | Approximate number of tick intervals |
| `.with_tick_format(fmt)` | Same `TickFormat` for both axes |
| `.with_x_tick_format(fmt)` | `TickFormat` for x-axis only |
| `.with_y_tick_format(fmt)` | `TickFormat` for y-axis only |
| `.with_x_tick_rotate(deg)` | Rotate x tick labels by `deg` degrees |
| `.with_log_x()` | Logarithmic x-axis |
| `.with_log_y()` | Logarithmic y-axis |
| `.with_log_scale()` | Logarithmic on both axes |
| `.with_show_grid(bool)` | Show or hide grid lines (default `true`) |

### Canvas and scale

| Method | Description |
|--------|-------------|
| `.with_width(px)` | Total SVG width in pixels |
| `.with_height(px)` | Total SVG height in pixels |
| `.with_scale(f)` | Uniform scale factor for all plot chrome (default `1.0`). Font sizes, margins, tick marks, legend geometry, and arrow sizes all multiply by `f`. Canvas size is unaffected. `TextAnnotation::font_size` and `ReferenceLine::stroke_width` must be scaled manually. |

### Annotations

| Method | Description |
|--------|-------------|
| `.with_annotation(TextAnnotation)` | Text label with optional arrow |
| `.with_reference_line(ReferenceLine)` | Horizontal or vertical dashed line |
| `.with_shaded_region(ShadedRegion)` | Horizontal or vertical filled band |

### Legend

| Method | Description |
|--------|-------------|
| `.with_legend_entries(Vec<LegendEntry>)` | Supply entries directly, bypassing auto-collection; auto-sizes `legend_width` |
| `.with_legend_at(x, y)` | Place legend at absolute SVG pixel coordinates (`Custom` variant); no margin reserved |
| `.with_legend_at_data(x, y)` | Place legend at data-space coordinates, mapped through axes at render time |
| `.with_legend_position(LegendPosition)` | Choose a preset legend placement |
| `.with_legend_box(bool)` | Show or hide the legend background and border box (default `true`) |
| `.with_legend_title(s)` | Render a bold title row above all legend entries |
| `.with_legend_group(title, entries)` | Add a labelled group of entries; multiple calls stack |
| `.with_legend_width(px)` | Override the auto-computed legend box width |
| `.with_legend_height(px)` | Override the auto-computed legend box height |

`LegendPosition` variants (grouped by placement zone):

**Inside the plot axes** — overlaid on the data area, 8 px inset from the axis edges:

| Variant | Corner |
|---------|--------|
| `InsideTopRight` | Upper-right |
| `InsideTopLeft` | Upper-left |
| `InsideBottomRight` | Lower-right |
| `InsideBottomLeft` | Lower-left |
| `InsideTopCenter` | Top edge, centred |
| `InsideBottomCenter` | Bottom edge, centred |

**Outside the plot axes** — placed in a margin; the canvas expands to accommodate:

| Variant | Placement |
|---------|-----------|
| `OutsideRightTop` *(default)* | Right margin, top-aligned |
| `OutsideRightMiddle` | Right margin, vertically centred |
| `OutsideRightBottom` | Right margin, bottom-aligned |
| `OutsideLeftTop` | Left margin, top-aligned |
| `OutsideLeftMiddle` | Left margin, vertically centred |
| `OutsideLeftBottom` | Left margin, bottom-aligned |
| `OutsideTopLeft` | Top margin, left-aligned |
| `OutsideTopCenter` | Top margin, centred |
| `OutsideTopRight` | Top margin, right-aligned |
| `OutsideBottomLeft` | Bottom margin, left-aligned |
| `OutsideBottomCenter` | Bottom margin, centred |
| `OutsideBottomRight` | Bottom margin, right-aligned |

**Freeform** — no margin change; you control the position:

| Variant | Placement |
|---------|-----------|
| `Custom(x, y)` | Absolute SVG canvas pixel coordinates |
| `DataCoords(x, y)` | Data-space coordinates mapped through `map_x`/`map_y` at render time |

### Legend sizing overrides

The legend box dimensions are computed automatically — width from the longest label (at ~8.5 px per character), height from the number of entries and groups. If the auto-sizing is off for your data, override either dimension explicitly:

```rust,no_run
# use kuva::render::layout::Layout;
# use kuva::render::plots::Plot;
# let plots: Vec<Plot> = vec![];
let layout = Layout::auto_from_plots(&plots)
    .with_legend_width(180.0)   // wider box for long labels
    .with_legend_height(120.0); // taller box for manual height control
```

### Typography

| Method | Default | Description |
|--------|---------|-------------|
| `.with_font_family(s)` | `"DejaVu Sans, Liberation Sans, Arial, sans-serif"` | CSS font-family string |
| `.with_title_size(n)` | `18` | Title font size (px) |
| `.with_label_size(n)` | `14` | Axis label font size (px) |
| `.with_tick_size(n)` | `12` | Tick label font size (px) |
| `.with_body_size(n)` | `12` | Body text font size (px) |

### `TickFormat` variants

| Variant | Output example |
|---------|----------------|
| `Auto` | `5`, `3.14`, `1.2e5` |
| `Fixed(n)` | `3.14` |
| `Integer` | `5` |
| `Sci` | `1.23e4` |
| `Percent` | `45.0%` |
| `Custom(Arc<dyn Fn(f64) -> String>)` | user-defined |