kuva
A lightweight scientific plotting library in Rust. Zero heavy dependencies — just build your plot, render to SVG, done.
Features
- Builder pattern API — chainable methods for constructing plots
- Generic numeric inputs — accepts
i32,u32,f32,f64seamlessly viaInto<f64> - SVG output — clean, scalable vector graphics
- Multi-plot support — overlay multiple plots on shared axes with automatic legends
- Subplot figures — grid layouts with shared axes, merged cells, panel labels, and shared legends
- Auto-layout — automatic axis scaling, tick generation, and margin computation
- Log-scale axes — logarithmic scaling for data spanning orders of magnitude, with 1-2-5 tick marks
- Annotations — text labels with arrows, reference lines (horizontal/vertical), and shaded regions
- Error bars & bands — symmetric/asymmetric error bars on scatter and line plots, confidence interval bands
- Built-in statistics — linear regression, KDE, percentiles, Pearson correlation
- Color palettes — named colorblind-safe palettes (Wong, Tol, IBM) and general-purpose palettes (Category10, Pastel, Bold), with auto-cycling via
Layout::with_palette() - Themes — light (default), dark, minimal, and solarized themes
- Tick formatting —
TickFormatenum for per-axis label control: smart auto, fixed decimals, integer, scientific notation, percentages, or fully custom - Terminal output —
--terminalrenders any plot directly in the terminal using Unicode braille, block characters, and ANSI colour; ideal for HPC and remote-server workflows with no display
Plot Types
| Type | Description |
|---|---|
| Scatter | Points with optional trend lines, error bars, bands, equation/correlation display |
| Line | Connected line segments with error bars, bands, and configurable stroke |
| Bar | Single, grouped, or stacked bars with categorical x-axis |
| Histogram | Binned frequency distribution with optional normalization |
| 2D Histogram | Bivariate density with colormaps and correlation |
| Box | Quartiles, median, whiskers (1.5×IQR) |
| Violin | Kernel density estimation shape |
| Pie | Slices with labels (inside/outside/auto), percentages, leader lines, donut charts, per-slice legend |
| Series | Index-based 1D data with line, point, or both styles |
| Heatmap | 2D matrix with colormaps (Viridis, Inferno, Grayscale, custom) |
| Band | Filled area between upper and lower curves (confidence intervals) |
| Brick | Character-level sequence visualization (DNA/RNA templates) |
| Waterfall | Floating bars showing cumulative change; Delta and Total bar kinds |
| Strip | Jitter/strip plots with center or swarm modes; overlayable on Box or Violin |
| Volcano | log2FC vs −log10(p) with threshold lines, up/down/NS colouring, gene labels |
| Manhattan | Genome-wide association plots with per-chromosome colouring, significance thresholds, gene labels |
| Dot | 2D grid of circles encoding two variables via size and colour; sparse/matrix input, stacked size-legend + colorbar |
| UpSet | Set-intersection visualisation: intersection-size bars, dot matrix, optional set-size bars |
| Stacked Area | Layered filled areas for part-to-whole time series; normalized (100%) variant |
| Candlestick | OHLC bars with optional volume overlay; up/down colors, continuous shading |
| Contour | Contour lines or filled contours from scattered (x, y, z) data via IDW interpolation |
| Chord | Flow matrix rendered as arc segments with cubic-Bézier ribbons; symmetric and directed |
| Sankey | Node-column flow diagram with tapered ribbons; source, gradient, or per-link coloring |
| Phylogenetic Tree | Rectangular, slanted, or circular dendrogram; Newick, edge-list, UPGMA, or linkage input; clade coloring |
| Synteny | Sequence bars connected by collinear-block ribbons; forward and inverted (bowtie) blocks |
Gallery
All 25 plot types in a single figure. See the full gallery for the detailed version with titles, axis labels, and legends.
Quick Start
use ScatterPlot;
use SvgBackend;
use render_scatter;
use Layout;
let plot = new
.with_data
.with_color
.with_size;
let layout = new
.with_title
.with_x_label
.with_y_label;
let scene = render_scatter.with_background;
let svg = SvgBackend.render_scene;
write.unwrap;
Multi-Plot Example
Overlay multiple plot types on the same axes with automatic legends:
use ;
use render_multiple;
use Layout;
use Plot;
use SvgBackend;
let line = new
.with_data
.with_color
.with_legend;
let points = new
.with_data
.with_color
.with_legend;
let plots = vec!;
let layout = auto_from_plots
.with_title
.with_x_label
.with_y_label;
let scene = render_multiple.with_background;
let svg = SvgBackend.render_scene;
Grouped Bar Chart Example
use BarPlot;
use render_multiple;
use Layout;
use Plot;
let bar = new
.with_group
.with_group
.with_legend;
let plots = vec!;
let layout = auto_from_plots
.with_title
.with_y_label;
let scene = render_multiple;
Log-Scale Example
use ScatterPlot;
use render_multiple;
use Layout;
use Plot;
use SvgBackend;
let scatter = new
.with_data
.with_color;
let plots = vec!;
let layout = auto_from_plots
.with_log_scale // both axes log, or use .with_log_x() / .with_log_y()
.with_title
.with_x_label
.with_y_label;
let scene = render_multiple;
let svg = SvgBackend.render_scene;
Subplot / Figure Layout Example
Arrange multiple independent subplots in a grid with shared axes, merged cells, and panel labels:
use ;
use Figure;
use Layout;
use Plot;
use SvgBackend;
let scatter = new
.with_data
.with_color;
let line = new
.with_data
.with_color;
let plots = vec!;
let layouts = vec!;
let figure = new
.with_title
.with_plots
.with_layouts
.with_shared_y_all
.with_labels;
let scene = figure.render;
let svg = SvgBackend.render_scene;
Features:
- Grid layout —
Figure::new(rows, cols)creates anrows x colsgrid - Merged cells —
.with_structure(vec![vec![0,1], vec![2], vec![3]])for spanning - Shared axes —
.with_shared_y_all(),.with_shared_x_all(), per-row/column/slice variants - Panel labels —
.with_labels()adds bold A, B, C labels (also numeric, lowercase, or custom) - Shared legends —
.with_shared_legend()or.with_shared_legend_bottom()for figure-wide legends - Figure title —
.with_title()adds a centered title above all subplots - Configurable sizing —
.with_cell_size(w, h),.with_spacing(px),.with_padding(px)
Color Palettes
Use named palettes for colorblind-safe or publication-ready color schemes. Colors auto-cycle across plots:
use Palette;
use ;
use render_multiple;
use Layout;
use Plot;
// Auto-cycle: palette colors assigned to each plot automatically
let s1 = new.with_data.with_legend;
let s2 = new.with_data.with_legend;
let s3 = new.with_data.with_legend;
let plots = vec!;
let layout = auto_from_plots
.with_palette // colorblind-safe
.with_title;
let scene = render_multiple;
Or index into a palette manually:
use Palette;
let pal = wong;
let color_a = &pal; // "#E69F00"
let color_b = &pal; // "#56B4E9"
// Wraps on overflow: pal[8] == pal[0]
Available palettes:
| Constructor | Colors | Notes |
|---|---|---|
Palette::wong() |
8 | Bang Wong, Nature Methods 2011 — colorblind-safe |
Palette::okabe_ito() |
8 | Alias for Wong |
Palette::tol_bright() |
7 | Paul Tol qualitative bright |
Palette::tol_muted() |
10 | Paul Tol qualitative muted |
Palette::tol_light() |
9 | Paul Tol qualitative light |
Palette::ibm() |
5 | IBM Design Language |
Palette::category10() |
10 | Tableau/D3 Category10 (default) |
Palette::pastel() |
10 | Softer pastel |
Palette::bold() |
10 | High-saturation vivid |
Condition-based aliases: deuteranopia(), protanopia() → Wong; tritanopia() → Tol Bright.
Custom palettes: Palette::custom("mine", vec!["red".into(), "green".into(), "blue".into()]).
Tick Formatting
Control how tick labels are rendered on each axis with TickFormat:
use TickFormat;
use Layout;
use Arc;
// Both axes: smart auto (integers as "5", not "5.0"; sci notation for extremes)
let layout = new;
// Per-axis: x as percentage, y as scientific notation
let layout = new
.with_x_tick_format // 0.5 → "50.0%"
.with_y_tick_format; // 12300 → "1.23e4"
// Fixed decimal places
let layout = new
.with_tick_format; // 3.1 → "3.10"
// Custom formatter
let layout = new
.with_tick_format;
| Variant | Example output |
|---|---|
Auto |
"5", "3.14", "1.23e4" (smart default) |
Fixed(2) |
"3.14", "0.00" |
Integer |
"5", "-3" |
Sci |
"1.23e4", "3.5e-3" |
Percent |
"45.0%" (value × 100) |
Custom(f) |
any string |
Log-scale axes retain their 1 / 10 / 100 style labels by default; specifying an explicit format overrides this.
Waterfall Chart Example
Visualise how an initial value evolves through a sequence of positive and negative increments:
use WaterfallPlot;
use render_multiple;
use Layout;
use Plot;
use SvgBackend;
let wf = new
.with_delta
.with_delta
.with_total // bar from zero to running total
.with_delta
.with_delta
.with_total
.with_connectors // dashed horizontal connector lines
.with_values; // value labels above/below each bar
let plots = vec!;
let layout = auto_from_plots
.with_title
.with_y_label;
let scene = render_multiple;
let svg = SvgBackend.render_scene;
Delta bars float from the running cumulative total; positive bars use color_positive (default green), negative bars use color_negative (default red). Total bars reach from zero to the current running total and use color_total (default steelblue). Override with .with_color_positive(), .with_color_negative(), .with_color_total().
UpSet Plot Example
Visualise set intersections with the standard UpSet layout — intersection-size bars on top, dot matrix in the centre, and optional set-size bars on the left:
use UpSetPlot;
use render_multiple;
use Layout;
use Plot;
use SvgBackend;
// Build directly from raw element sets — intersections are computed automatically.
let up = new.with_sets;
let plots = vec!;
let layout = auto_from_plots.with_title;
let scene = render_multiple;
let svg = SvgBackend.render_scene;
write.unwrap;
Or supply precomputed (mask, count) pairs for large datasets:
let up = new
.with_data
.with_sort
.with_max_visible;
Performance
kuva renders SVG directly without intermediate data structures or heavy runtimes. All benchmarks below use cargo bench --features full (release build, Criterion, AMD64 Linux).
| plot type | 10k points | 100k points | 1M points |
|---|---|---|---|
| Scatter | 3.1 ms | 34.5 ms | 414 ms |
| Line | 2.5 ms | 28.6 ms | 308 ms |
| Violin (3 groups) | 12.7 ms | 89 ms | — |
| Manhattan (22 chr) | 3.9 ms | 42 ms | 501 ms |
| Heatmap n×n | 4.9 ms (100²) | 24.6 ms (200²) | 154 ms (500²) |
| SVG emit only | 2.0 ms | 19.9 ms | 213 ms |
All plot types scale O(n). SVG string generation costs ~200 ns/element; violin time is dominated by KDE (~28 ms for 100k samples, ~8 ns/exp). Error bars add 4–6× cost over plain scatter. See docs/src/benchmarks.md for full tables, methodology, and how to reproduce the results.
Documentation
The docs are built with mdBook. Install it once with:
Regenerate SVG assets
Each plot type has a dedicated example that writes its SVG assets to docs/src/assets/. Regenerate all of them at once with:
Or regenerate a single plot type:
Build and preview
CLI (kuva)
The kuva binary lets you render plots directly from the shell — no Rust required.
Build
Quick start
These examples use the datasets in examples/data/ and work from the repo root immediately after building:
# Scatter plot — SVG to stdout
# Volcano plot — highlight top 20 genes
# Box plot — pipe from stdin, save to file
|
Subcommands
| Subcommand | Input format | Use case |
|---|---|---|
scatter |
x, y columns (+ optional group) | Scatter plot with optional trend line |
line |
x, y columns (+ optional group) | Line plot with optional fill |
bar |
label, value columns | Categorical bar chart |
histogram |
value column | Distribution histogram |
box |
group, value columns | Box-and-whisker by group |
violin |
group, value columns | Violin plot by group |
pie |
label, value columns | Pie / donut chart |
strip |
group, value columns | Strip / beeswarm plot by group |
waterfall |
label, value columns | Waterfall / bridge chart |
stacked-area |
x, group, y columns | Stacked area chart |
volcano |
name, log2fc, pvalue columns | Volcano plot for DE analysis |
manhattan |
chr, pos, pvalue columns | GWAS Manhattan plot |
candlestick |
label, open, high, low, close columns | OHLC candlestick chart |
heatmap |
row, col, value columns (long format) | Heatmap with optional clustering |
hist2d |
x, y columns | 2-D histogram / density grid |
contour |
x, y, z columns | Contour / filled-contour plot |
dot |
x, y columns with size/color | Dot plot with size and color encoding |
upset |
set membership TSV | UpSet intersection plot |
chord |
matrix TSV | Chord diagram |
sankey |
source, target, value columns | Sankey flow diagram |
phylo |
Newick string or edge list | Phylogenetic tree |
synteny |
sequence definitions + block file | Genome synteny ribbons |
Input and output
Input is auto-detected TSV or CSV (by extension, then content sniff). Columns are selectable by 0-based index or header name — pass an integer (--x-col 2) or a name (--x-col log2fc). Pipe from stdin by omitting the file argument or passing -.
Output defaults to SVG on stdout; use -o file.svg/png/pdf to write a file. PNG and PDF output require the png/pdf feature flags at build time.
Examples
# Scatter plot from a TSV, SVG to stdout
| |
# Render directly in the terminal — no file, no display

# With explicit terminal dimensions
# Colour by a group column, write PNG
# Box plot with swarm overlay
# Histogram with 40 bins, dark theme
# Pie chart with percentages and outside labels
# Volcano plot, label top 20 genes
# Manhattan with hg38 base-pair positions
# Waterfall with connectors and value labels
# Stacked area, normalized
# UpSet intersection plot
# Sankey flow diagram, gradient links
# Synteny ribbons
See docs/src/cli/index.md for the complete flag reference for every subcommand, and examples/data/ for ready-to-use example datasets.
Development note
kuva was initially built by hand, with a working library and several plot types already in place before AI tooling was introduced. From that point, development was heavily assisted by Claude (Anthropic) — accelerating the addition of new plot types, the CLI binary, tests, and documentation. The architecture, domain knowledge, and direction remain the author's own; Claude was used as an accelerant, not an author.
This disclaimer was written by Claude as an honest assessment of its own role in the project.
License
MIT