kuva

kuva is a scientific plotting library for Rust that renders plots to SVG. It targets bioinformatics use cases and ships with 25 specialised plot types — from standard scatter and bar charts to Manhattan plots, UpSet plots, phylogenetic trees, and synteny diagrams. A kuva CLI binary lets you render plots directly from the shell without writing any Rust.

Design

The API follows a builder pattern. Every plot type is constructed with ::new(), configured with method chaining, and rendered through a single pipeline:

plot struct  →  Plot enum  →  Layout  →  SVG / PNG / PDF

Quick start

#![allow(unused)]
fn main() {
use kuva::prelude::*;

let data = vec![(1.0_f64, 2.0_f64), (3.0, 5.0), (5.0, 4.0)];

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

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

let svg = render_to_svg(plots, layout);
std::fs::write("my_plot.svg", svg).unwrap();
}

Prelude

use kuva::prelude::* brings all 25 plot structs, Plot, Layout, Figure, Theme, Palette, render_to_svg, and everything else you typically need into scope in one line.

Every plot struct implements Into<Plot>, so you can write plot.into() instead of Plot::Scatter(plot).

For PNG or PDF output, use render_to_png and render_to_pdf (require feature flags png and pdf respectively):

#![allow(unused)]
fn main() {
use kuva::prelude::*;

let plots: Vec<Plot> = vec![/* ... */];
let layout = Layout::auto_from_plots(&plots);

// SVG — always available
let svg: String = render_to_svg(plots, layout);

// PNG — feature = "png"
let png: Vec<u8> = render_to_png(plots, layout, 2.0).unwrap();

// PDF — feature = "pdf"
let pdf: Vec<u8> = render_to_pdf(plots, layout).unwrap();
}

Regenerating documentation assets

The SVG images embedded in these docs are generated by standalone example programs in the examples/ directory. Regenerate all assets at once with:

bash scripts/gen_docs.sh

Or regenerate a single plot type:

cargo run --example scatter
cargo run --example histogram
# etc. — one example per plot type in examples/

Building the book

Install mdBook, then:

mdbook build docs    # produce docs/book/
mdbook serve docs    # live-reload preview at http://localhost:3000

Gallery

All plot types at a glance

25 plot types, one figure — generated by running cargo run --example all_plots_simple and cargo run --example all_plots_complex.

Simple overview (compact, minimal data):

All 25 plot types — simple

Full-featured (larger datasets, titles, axis labels, legends):

All 25 plot types — complex


Individual plot types

Click any image to go to the full documentation page.

Origins

Before kuva had axes, themes, or 25 plot types, it had three blue circles. (It was also called visus back then)

Three blue circles on a transparent background

The first output: three hardcoded circles, no background, no axes.

The very first proof-of-concept was just three blue dots on a transparent 500×500 canvas. No coordinate system, no margins, no layout. Just enough to confirm that the SVG pipeline worked end-to-end.

The next milestone was getting the library to automatically compute a layout from data:

  • margins
  • tick marks
  • grid lines
  • axis labels

all derived from the data bounds with no manual positioning.

Three blue circles with auto-computed axes, ticks, and grid

The first auto-sized plot: real ticks and labels computed from data.

scatter_autosized.svg was the moment the coordinate mapping started making sense. Data values mapped to pixel positions, axes drawn automatically, tick intervals chosen from the data range. The red x-axis and green y-axis are relics of the earliest rendering code, colour-coded for debugging. That was the first plot that felt like a real plot. The rush I felt when this plot first rendered...wow.

Everything since has been built on that foundation.

Before LLM assistance

The first Claude Code session was opened on 20 February 2026, just to run /init and generate a CLAUDE.md file. At that point the library already had 11 plot types, all written from scratch:

ScatterPlot, LinePlot, BarPlot, Histogram, Histogram2D, BoxPlot, ViolinPlot, PiePlot, SeriesPlot, Heatmap, BrickPlot

(Technically 10 compiled. BrickPlot had a syntax error on day one, because I was still figuring out STRIGAR processing from bladerunner, an STR calling tool I wrote)

The 14 plot types added with AI assistance: BandPlot, WaterfallPlot, StripPlot, VolcanoPlot, ManhattanPlot, DotPlot, UpSetPlot, StackedAreaPlot, CandlestickPlot, ContourPlot, ChordPlot, SankeyPlot, PhyloTree, SyntenyPlot

The architecture, the rendering pipeline, the builder pattern API, and the core plotting logic were all designed and written before any LLM was involved.

On using LLM assistance

I want to be honest about this, because I think the discourse around AI and software is often dishonest in one direction or the other.

I am perfectly capable of writing everything in this library myself. I've been doing bioinformatics software development for years. The architecture here (the pipeline, the builder pattern, the primitive-based renderer) is all mine. I designed it, and I would have made the same design choices with or without an LLM. The deep domain knowledge of what a bioinformatician actually needs from a plotting library, the aesthetics, the decisions about which plot types to include and how their APIs should feel. None of that came from an LLM.

What LLM assistance changed was pace. Going from 11 plot types to 25, adding a full CLI, a terminal backend, a documentation book, smoke tests, and integration tests, all in roughly a week, that would have taken me much longer alone. Not because I couldn't do it, but because translating a clear mental model into working, tested code takes time, and that's where the acceleration is real.

It isn't a one-way process either. The LLM gets things wrong, proposes approaches that don't fit the existing code, or misses the intent entirely. Every feature required my involvement at every step: reviewing, redirecting, and often rejecting what was produced. A good example is the x-axis label positioning in the terminal backend. Getting tick labels to sit correctly in braille character cells, snapping to the right row, spacing correctly across different terminal heights, went through several wrong approaches before landing on the solution that actually works. That kind of problem requires someone who understands both what the output should look like and why a given implementation is producing the wrong result. The LLMs can generate absolute shit sometimes, and it's up to us to decide what is correct, and take responsibility for any of the output.

The honest summary: this is my library, reflecting my vision and expertise. The LLM is a fast, capable collaborator that I direct. I would not be publishing this so soon without that acceleration, but the work is mine. Your opinion of this may differ, and that's fine. But I really don't care and kuva exists, rather than not. That's all the validation I need.

Scatter Plot

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

Import path: kuva::plot::scatter::ScatterPlot


Basic usage

#![allow(unused)]
fn main() {
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),
    (2.1, 2.4),
    (4.0, 4.3),
    (6.1, 6.0),
    (8.4, 7.9),
];

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();
}
Basic scatter plot

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.

#![allow(unused)]
fn main() {
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));
}
Scatter with linear trend line

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.

#![allow(unused)]
fn main() {
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));
}
Scatter with confidence band

Error bars

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

#![allow(unused)]
fn main() {
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));
}
Scatter with error bars

Asymmetric errors

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

#![allow(unused)]
fn main() {
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.

#![allow(unused)]
fn main() {
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.

Scatter marker shapes

Bubble plot

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

#![allow(unused)]
fn main() {
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));
}
Bubble plot

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().

#![allow(unused)]
fn main() {
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));
}

API reference

MethodDescription
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 point color (CSS color string)
.with_size(r)Set uniform point radius in pixels (default 3.0)
.with_sizes(iter)Set per-point radii (bubble plot)
.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

MarkerShape variants

Circle · Square · Triangle · Diamond · Cross · Plus

TrendLine variants

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

Line Plot

A line plot connects (x, y) data points with a continuous path. It supports four built-in stroke styles, area fills, step interpolation, confidence bands, and error bars.

Import path: kuva::plot::LinePlot


Basic usage

#![allow(unused)]
fn main() {
use kuva::plot::LinePlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data: Vec<(f64, f64)> = (0..=100)
    .map(|i| { let x = i as f64 * 0.1; (x, x.sin()) })
    .collect();

let plot = LinePlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_stroke_width(2.0);

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

let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("line.svg", svg).unwrap();
}
Basic line plot

Line styles

Four built-in stroke styles are available. Use the shorthand methods or pass a LineStyle variant directly.

#![allow(unused)]
fn main() {
use kuva::plot::{LinePlot, LineStyle};
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> = (0..=80).map(|i| i as f64 * 0.125).collect();

let plots = vec![
    Plot::Line(LinePlot::new()
        .with_data(xs.iter().map(|&x| (x, x.sin())))
        .with_color("steelblue").with_stroke_width(2.0)
        .with_line_style(LineStyle::Solid).with_legend("Solid")),

    Plot::Line(LinePlot::new()
        .with_data(xs.iter().map(|&x| (x, x.cos())))
        .with_color("crimson").with_stroke_width(2.0)
        .with_dashed().with_legend("Dashed")),

    Plot::Line(LinePlot::new()
        .with_data(xs.iter().map(|&x| (x, (x * 0.7).sin())))
        .with_color("seagreen").with_stroke_width(2.0)
        .with_dotted().with_legend("Dotted")),

    Plot::Line(LinePlot::new()
        .with_data(xs.iter().map(|&x| (x, (x * 0.7).cos())))
        .with_color("darkorange").with_stroke_width(2.0)
        .with_dashdot().with_legend("Dash-dot")),
];

let layout = Layout::auto_from_plots(&plots)
    .with_title("Line Styles")
    .with_x_label("X")
    .with_y_label("Y");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Line styles

Custom dasharray

For patterns not covered by the built-in variants, use LineStyle::Custom with an SVG stroke-dasharray string:

#![allow(unused)]
fn main() {
use kuva::plot::{LinePlot, LineStyle};
let plot = LinePlot::new()
    .with_data(vec![(0.0_f64, 0.0_f64), (1.0, 1.0), (2.0, 0.5)])
    .with_line_style(LineStyle::Custom("12 3 3 3".into()));
}

Area plot

Call .with_fill() to shade the region between the line and the x-axis. The fill uses the line color at 0.3 opacity by default.

#![allow(unused)]
fn main() {
use kuva::plot::LinePlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data: Vec<(f64, f64)> = (0..=100)
    .map(|i| { let x = i as f64 * 0.1; (x, x.sin()) })
    .collect();

let plot = LinePlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_stroke_width(2.0)
    .with_fill()
    .with_fill_opacity(0.3);   // optional, 0.3 is the default

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

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Area plot

Step plot

.with_step() draws horizontal-then-vertical transitions between consecutive points rather than diagonal segments. This is the standard rendering for histograms and discrete time-series data.

#![allow(unused)]
fn main() {
use kuva::plot::LinePlot;
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.0_f64, 2.0_f64), (1.0, 5.0), (2.0, 3.0), (3.0, 7.0),
    (4.0, 4.0), (5.0, 8.0), (6.0, 5.0), (7.0, 9.0),
];

let plot = LinePlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_stroke_width(2.0)
    .with_step();

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

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Step plot

Step and fill can be combined: .with_step().with_fill() produces a filled step area.


Confidence band

.with_band(y_lower, y_upper) draws a shaded region between two boundary series aligned to the line's x positions. The band color inherits from the line color.

#![allow(unused)]
fn main() {
use kuva::plot::LinePlot;
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> = (0..=80).map(|i| i as f64 * 0.125).collect();
let ys: Vec<f64> = xs.iter().map(|&x| x.sin()).collect();
let lower: Vec<f64> = ys.iter().map(|&y| y - 0.3).collect();
let upper: Vec<f64> = ys.iter().map(|&y| y + 0.3).collect();

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

let plot = LinePlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_stroke_width(2.0)
    .with_band(lower, upper);

let plots = vec![Plot::Line(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));
}
Line with confidence band

Error bars

.with_y_err() and .with_x_err() attach symmetric error bars. Asymmetric variants take (negative, positive) tuples.

#![allow(unused)]
fn main() {
use kuva::plot::LinePlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data: Vec<(f64, f64)> = (0..=8)
    .map(|i| (i as f64, (i as f64 * 0.8).sin()))
    .collect();
let y_err = vec![0.15, 0.20, 0.12, 0.18, 0.22, 0.14, 0.19, 0.16, 0.21_f64];

let plot = LinePlot::new()
    .with_data(data)
    .with_y_err(y_err)
    .with_color("steelblue")
    .with_stroke_width(2.0);

let plots = vec![Plot::Line(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));
}
Line with error bars

Asymmetric errors

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

#![allow(unused)]
fn main() {
use kuva::plot::LinePlot;
let data: Vec<(f64, f64)> = (0..=4).map(|i| (i as f64, i as f64)).collect();
let y_err = vec![(0.1_f64, 0.3_f64), (0.2, 0.5), (0.1, 0.4), (0.3, 0.2), (0.2, 0.6)];

let plot = LinePlot::new()
    .with_data(data)
    .with_y_err_asymmetric(y_err);
}

Multiple series

Pass multiple LinePlot structs in a Vec<Plot>. Legends appear automatically when any series has a label.

#![allow(unused)]
fn main() {
use kuva::plot::LinePlot;
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> = (0..=60).map(|i| i as f64 * 0.1).collect();

let plots = vec![
    Plot::Line(LinePlot::new()
        .with_data(xs.iter().map(|&x| (x, x.sin())))
        .with_color("steelblue").with_legend("sin(x)")),
    Plot::Line(LinePlot::new()
        .with_data(xs.iter().map(|&x| (x, x.cos())))
        .with_color("crimson").with_legend("cos(x)")),
];

let layout = Layout::auto_from_plots(&plots)
    .with_title("Multiple Series")
    .with_x_label("X")
    .with_y_label("Y");

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

API reference

MethodDescription
LinePlot::new()Create a new line plot with defaults
.with_data(iter)Set (x, y) data; accepts any Into<f64> numeric type
.with_color(s)Set line color (CSS color string)
.with_stroke_width(w)Set stroke width in pixels (default 2.0)
.with_legend(s)Attach a legend label to this series
.with_line_style(LineStyle)Set stroke style explicitly
.with_dashed()Shorthand for LineStyle::Dashed
.with_dotted()Shorthand for LineStyle::Dotted
.with_dashdot()Shorthand for LineStyle::DashDot
.with_step()Use step interpolation
.with_fill()Fill area under the line
.with_fill_opacity(f)Set fill opacity (default 0.3)
.with_band(lower, upper)Confidence band aligned to line x positions
.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

LineStyle variants

Solid (default) · Dashed (8 4) · Dotted (2 4) · DashDot (8 4 2 4) · Custom(String)

Bar Chart

A bar chart renders categorical data as vertical bars. It has three modes — simple, grouped, and stacked — all built from the same BarPlot struct.

Import path: kuva::plot::BarPlot


Simple bar chart

Use .with_bar() or .with_bars() to add one bar per category, then .with_color() to set a uniform fill.

#![allow(unused)]
fn main() {
use kuva::plot::BarPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plot = BarPlot::new()
    .with_bars(vec![
        ("Apples",     42.0),
        ("Bananas",    58.0),
        ("Cherries",   31.0),
        ("Dates",      47.0),
        ("Elderberry", 25.0),
    ])
    .with_color("steelblue");

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

let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("bar.svg", svg).unwrap();
}
Simple bar chart

Adding bars individually

.with_bar(label, value) adds one bar at a time, which is useful when constructing data programmatically:

#![allow(unused)]
fn main() {
use kuva::plot::BarPlot;
let plot = BarPlot::new()
    .with_bar("A", 3.2)
    .with_bar("B", 4.7)
    .with_bar("C", 2.8)
    .with_color("steelblue");
}

Grouped bar chart

Use .with_group(label, values) to add a category with multiple side-by-side bars. Each item in values is a (value, color) pair — one per series. Call .with_legend() to label each series.

#![allow(unused)]
fn main() {
use kuva::plot::BarPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plot = BarPlot::new()
    .with_group("Q1", vec![(18.0, "steelblue"), (12.0, "crimson"), (9.0,  "seagreen")])
    .with_group("Q2", vec![(22.0, "steelblue"), (17.0, "crimson"), (14.0, "seagreen")])
    .with_group("Q3", vec![(19.0, "steelblue"), (21.0, "crimson"), (11.0, "seagreen")])
    .with_group("Q4", vec![(25.0, "steelblue"), (15.0, "crimson"), (18.0, "seagreen")])
    .with_legend(vec!["Product A", "Product B", "Product C"]);

let plots = vec![Plot::Bar(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Grouped Bar Chart")
    .with_y_label("Sales (units)");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Grouped bar chart

Stacked bar chart

Add .with_stacked() to the same grouped structure to stack segments vertically instead of placing them side-by-side.

#![allow(unused)]
fn main() {
use kuva::plot::BarPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plot = BarPlot::new()
    .with_group("Q1", vec![(18.0, "steelblue"), (12.0, "crimson"), (9.0,  "seagreen")])
    .with_group("Q2", vec![(22.0, "steelblue"), (17.0, "crimson"), (14.0, "seagreen")])
    .with_group("Q3", vec![(19.0, "steelblue"), (21.0, "crimson"), (11.0, "seagreen")])
    .with_group("Q4", vec![(25.0, "steelblue"), (15.0, "crimson"), (18.0, "seagreen")])
    .with_legend(vec!["Product A", "Product B", "Product C"])
    .with_stacked();

let plots = vec![Plot::Bar(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Stacked Bar Chart")
    .with_y_label("Sales (units)");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Stacked bar chart

Bar width

.with_width() controls how much of each category slot the bar fills. The default is 0.8; 1.0 means bars touch.

#![allow(unused)]
fn main() {
use kuva::plot::BarPlot;
let plot = BarPlot::new()
    .with_bars(vec![("A", 3.0), ("B", 5.0), ("C", 4.0)])
    .with_color("steelblue")
    .with_width(0.5);   // narrower bars with more whitespace
}

API reference

MethodDescription
BarPlot::new()Create a bar plot with defaults
.with_bar(label, value)Add a single bar (simple mode)
.with_bars(vec)Add multiple bars at once (simple mode)
.with_color(s)Set a uniform color across all existing bars
.with_group(label, values)Add a category with one bar per series (grouped / stacked mode)
.with_legend(vec)Set series labels; one label per bar within a group
.with_stacked()Stack bars vertically instead of side-by-side
.with_width(f)Bar width as a fraction of slot width (default 0.8)

Choosing a mode

GoalMethods to use
One color, one bar per category.with_bars() + .with_color()
Multiple series, side-by-side.with_group() × N + .with_legend()
Multiple series, stacked.with_group() × N + .with_legend() + .with_stacked()

Histogram

A histogram bins a 1-D dataset into equal-width intervals and renders each bin as a vertical bar. It supports explicit ranges, normalization, and overlapping distributions.

Import path: kuva::plot::Histogram


Basic usage

.with_range((min, max)) is required — without it Layout::auto_from_plots cannot determine the axis extent and will produce an empty chart. Compute the range from your data before building the histogram:

#![allow(unused)]
fn main() {
use kuva::plot::Histogram;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data: Vec<f64> = vec![/* your samples */];

// Compute range from data first.
let min = data.iter().cloned().fold(f64::INFINITY, f64::min);
let max = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);

let hist = Histogram::new()
    .with_data(data)
    .with_bins(20)
    .with_range((min, max))   // required for auto_from_plots
    .with_color("steelblue");

let plots = vec![Plot::Histogram(hist)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Histogram")
    .with_x_label("Value")
    .with_y_label("Count");

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

Bin count

.with_bins(n) sets the number of equal-width bins (default 10). Fewer bins smooth out noise; more bins reveal finer structure at the cost of per-bin counts. The same range is used in both cases so the x-axis stays comparable.

#![allow(unused)]
fn main() {
use kuva::plot::Histogram;
// Coarse — few bins, clear shape
let hist = Histogram::new().with_data(data.clone()).with_bins(5).with_range(range);

// Fine — many bins, more detail
let hist = Histogram::new().with_data(data).with_bins(40).with_range(range);
}
5 bins 40 bins

Fixed range

Pass an explicit (min, max) to .with_range() to fix the bin edges regardless of the data. Values outside the range are silently ignored. This is useful when you want to focus on a sub-range, exclude outliers, or ensure two independent histograms cover the same x-axis scale.

#![allow(unused)]
fn main() {
use kuva::plot::Histogram;
let hist = Histogram::new()
    .with_data(data)
    .with_bins(20)
    .with_range((-3.0, 3.0))   // bins fixed to [-3, 3]; outliers ignored
    .with_color("steelblue");
}

Normalized histogram

.with_normalize() rescales bar heights so the tallest bar equals 1.0. This is peak-normalization — useful for comparing the shape of distributions with different sample sizes. The y-axis shows relative frequency, not counts or probability density.

#![allow(unused)]
fn main() {
use kuva::plot::Histogram;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let min = data.iter().cloned().fold(f64::INFINITY, f64::min);
let max = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);

let hist = Histogram::new()
    .with_data(data)
    .with_bins(20)
    .with_range((min, max))
    .with_color("steelblue")
    .with_normalize();

let plots = vec![Plot::Histogram(hist)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Normalized Histogram")
    .with_x_label("Value")
    .with_y_label("Relative frequency");
}
Normalized histogram

Overlapping distributions

Place multiple Histogram structs in the same Vec<Plot>. Since bars have no built-in opacity setting, use 8-digit hex colors (#RRGGBBAA) to make each series semi-transparent so the overlap is visible.

When overlapping, compute a shared range from the combined data so both histograms use the same bin edges and x-axis scale:

#![allow(unused)]
fn main() {
use kuva::plot::Histogram;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

// Shared range across both groups so x-axes align.
let combined_min = group_a.iter().chain(group_b.iter())
    .cloned().fold(f64::INFINITY, f64::min);
let combined_max = group_a.iter().chain(group_b.iter())
    .cloned().fold(f64::NEG_INFINITY, f64::max);
let range = (combined_min, combined_max);

// #4682b480 = steelblue at ~50% opacity
// #dc143c80 = crimson  at ~50% opacity
let hist_a = Histogram::new()
    .with_data(group_a)
    .with_bins(20)
    .with_range(range)
    .with_color("#4682b480")
    .with_legend("Group A");

let hist_b = Histogram::new()
    .with_data(group_b)
    .with_bins(20)
    .with_range(range)
    .with_color("#dc143c80")
    .with_legend("Group B");

let plots = vec![Plot::Histogram(hist_a), Plot::Histogram(hist_b)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Overlapping Distributions")
    .with_x_label("Value")
    .with_y_label("Count");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Overlapping histograms

The AA byte in the hex color controls opacity: ff = fully opaque, 80 ≈ 50%, 40 ≈ 25%.


API reference

MethodDescription
Histogram::new()Create a histogram with defaults (10 bins, color "black")
.with_data(iter)Set input values; accepts any Into<f64> numeric type
.with_bins(n)Number of equal-width bins (default 10)
.with_range((min, max))Required. Sets bin edges and the layout axis extent
.with_color(s)Bar fill color; use 8-digit hex (#RRGGBBAA) for alpha transparency
.with_normalize()Scale heights so peak bar = 1.0 (relative frequency)
.with_legend(s)Attach a legend label to this series

2D Histogram

A 2D histogram (density map) bins scatter points (x, y) into a rectangular grid and colors each cell by its count. The colorbar labeled "Count" is added to the right margin automatically. Use it to visualize the joint distribution of two continuous variables.

Import path: kuva::plot::Histogram2D, kuva::plot::histogram2d::ColorMap


Basic usage

Pass (x, y) scatter points along with explicit axis ranges and bin counts to .with_data(). Points outside the specified ranges are silently discarded.

#![allow(unused)]
fn main() {
use kuva::plot::Histogram2D;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

// (x, y) scatter points — e.g. from a 2D measurement
let data: Vec<(f64, f64)> = vec![];  // ...your data here

let hist = Histogram2D::new()
    .with_data(data, (0.0, 30.0), (0.0, 30.0), 30, 30);

let plots = vec![Plot::Histogram2d(hist)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("2D Histogram — Viridis")
    .with_x_label("X")
    .with_y_label("Y");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("hist2d.svg", svg).unwrap();
}
2D histogram — single Gaussian cluster, Viridis colormap

A single bivariate Gaussian cluster binned into a 30×30 grid. The Viridis colorbar on the right shows the count scale from zero (dark blue) to the maximum (yellow).


Correlation annotation

.with_correlation() computes the Pearson r coefficient from the raw scatter points and prints it in the top-right corner.

#![allow(unused)]
fn main() {
use kuva::plot::Histogram2D;
use kuva::render::plots::Plot;
let hist = Histogram2D::new()
    .with_data(data, (0.0, 20.0), (0.0, 20.0), 25, 25)
    .with_correlation();
}
2D histogram with Pearson r = 0.85 annotation

The diagonal density ridge reflects a strong positive correlation (r ≈ 0.85). The coefficient is computed from all input points, including those clipped outside the plot range.


Bimodal data — Inferno colormap

ColorMap::Inferno maps low counts to near-black and high counts to bright yellow. It is effective for high-contrast visualization of structured or multi-modal data.

#![allow(unused)]
fn main() {
use kuva::plot::Histogram2D;
use kuva::plot::histogram2d::ColorMap;
use kuva::render::plots::Plot;

let hist = Histogram2D::new()
    .with_data(data, (0.0, 30.0), (0.0, 30.0), 30, 30)
    .with_color_map(ColorMap::Inferno);
}
2D histogram — bimodal distribution, Inferno colormap

Two Gaussian clusters in opposite corners of the grid, visible as bright islands against the dark background. Empty bins are not drawn, preserving the black background of Inferno.


Bin resolution

Bin count controls the trade-off between noise and detail.

ColorMap::Grayscale maps zero to white and the maximum to black — useful for printing or publication figures.

2D histogram — 10×10 coarse bins, Grayscale

10×10 bins smooth the distribution and make the Gaussian shape immediately obvious, but lose fine-grained density structure.

ColorMap::Viridis (the default) uses a perceptually uniform blue → green → yellow scale, making density gradients easy to read at high resolution.

2D histogram — 50×50 fine bins, Viridis

50×50 bins reveal the internal shape of the distribution, though individual cells become noisier at lower sample counts.


Range convention

The axis is calibrated directly to the physical x_range / y_range values you supply, so tick labels always show real data units regardless of bin count. Any (min, max) pair works.

Rangebins_xBin width
(0.0, 30.0)301.0
(0.0, 20.0)250.8
(5.0, 25.0)201.0

Colormaps

ColorMap variantDescription
ColorMap::ViridisBlue → green → yellow. Perceptually uniform, colorblind-safe. (default)
ColorMap::InfernoBlack → orange → yellow. High contrast.
ColorMap::GrayscaleWhite → black. Print-friendly.
ColorMap::Custom(f)User-supplied Arc<dyn Fn(f64) -> String>.

API reference

MethodDescription
Histogram2D::new()Create with defaults (10×10 bins, Viridis)
.with_data(data, x_range, y_range, bins_x, bins_y)Load (x, y) points and bin them
.with_color_map(cmap)Set the colormap (default ColorMap::Viridis)
.with_correlation()Print Pearson r in the top-right corner

Series Plot

A series plot displays an ordered sequence of y-values against their sequential index on the x-axis. It is the simplest way to visualise a time series, signal trace, or any 1D ordered measurement. Three rendering styles are available: line, point, or both.

Import path: kuva::plot::SeriesPlot


Basic usage

Pass an iterable of numeric values to .with_data(). The x-axis is assigned automatically as 0, 1, 2, ….

#![allow(unused)]
fn main() {
use kuva::plot::SeriesPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data: Vec<f64> = (0..80)
    .map(|i| (i as f64 * std::f64::consts::TAU / 80.0).sin())
    .collect();

let series = SeriesPlot::new()
    .with_data(data)
    .with_color("steelblue")
    .with_line_style();

let plots = vec![Plot::Series(series)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Series Plot — Line Style")
    .with_x_label("Sample")
    .with_y_label("Amplitude");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("series.svg", svg).unwrap();
}
Series plot — single sine wave, line style

Display styles

Three styles control how values are rendered. Call the corresponding method instead of setting a field directly.

MethodStyleRenders
.with_line_style()LinePolyline connecting consecutive points
.with_point_style()PointCircle at each value (default)
.with_line_point_style()BothPolyline and circles
Line style Point style Both style

Multiple series

Place multiple SeriesPlot instances in the same plots vector to overlay them on one canvas. All series share the same axes; they align automatically when they have the same number of values.

#![allow(unused)]
fn main() {
use kuva::plot::SeriesPlot;
use kuva::render::plots::Plot;
let s1 = SeriesPlot::new()
    .with_data(sin_data)
    .with_color("steelblue")
    .with_line_style()
    .with_legend("sin(t)");

let s2 = SeriesPlot::new()
    .with_data(cos_data)
    .with_color("firebrick")
    .with_line_style()
    .with_legend("cos(t)");

let s3 = SeriesPlot::new()
    .with_data(damped)
    .with_color("seagreen")
    .with_line_style()
    .with_stroke_width(1.5)
    .with_legend("e^(−t/2)·sin(t)");

let plots = vec![Plot::Series(s1), Plot::Series(s2), Plot::Series(s3)];
}
Multiple series — sin, cos, and damped oscillation

Each series has its own color and legend entry. The legend is drawn automatically when any series has a legend_label.


Custom stroke and point size

.with_stroke_width(f) sets the line thickness; .with_point_radius(f) sets the circle size. These only affect the relevant style — stroke_width applies to Line and Both; point_radius applies to Point and Both.

#![allow(unused)]
fn main() {
use kuva::plot::SeriesPlot;
use kuva::render::plots::Plot;
let series = SeriesPlot::new()
    .with_data(data)
    .with_color("darkorchid")
    .with_line_point_style()
    .with_stroke_width(1.5)
    .with_point_radius(4.0)
    .with_legend("signal");
}
Series with custom stroke width 1.5 and point radius 4.0

API reference

MethodDescription
SeriesPlot::new()Create with defaults (Point style, "black", radius 3.0, width 2.0)
.with_data(iter)Load y-values; x positions are sequential indices
.with_color(s)CSS color for lines and points (default "black")
.with_line_style()Polyline only
.with_point_style()Circles only (default)
.with_line_point_style()Polyline and circles
.with_stroke_width(f)Line thickness in pixels (default 2.0)
.with_point_radius(f)Circle radius in pixels (default 3.0)
.with_legend(s)Add a legend entry with this label

Band Plot

A band plot fills the region between two y-curves over a shared x-axis. It is used to display confidence intervals, prediction bands, IQR envelopes, or any shaded range around a central estimate.

Import path: kuva::plot::BandPlot


Standalone band

BandPlot::new(x, y_lower, y_upper) creates a filled area independently. Pair it with a LinePlot or ScatterPlot by placing both in the same plots vector — the band is drawn behind the line.

#![allow(unused)]
fn main() {
use kuva::plot::{BandPlot, LinePlot};
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let x: Vec<f64> = (0..60).map(|i| i as f64 * 0.2).collect();
let y: Vec<f64> = x.iter().map(|&v| v.sin()).collect();
let lower: Vec<f64> = y.iter().map(|&v| v - 0.35).collect();
let upper: Vec<f64> = y.iter().map(|&v| v + 0.35).collect();

let band = BandPlot::new(x.clone(), lower, upper)
    .with_color("steelblue")
    .with_opacity(0.25);

let line = LinePlot::new()
    .with_data(x.iter().copied().zip(y.iter().copied()))
    .with_color("steelblue");

// Band must come before the line so it renders behind it
let plots = vec![Plot::Band(band), Plot::Line(line)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Band Plot — Standalone")
    .with_x_label("x")
    .with_y_label("y");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("band.svg", svg).unwrap();
}
Standalone band paired with a line

The band fills between y_lower and y_upper. Placing Plot::Band before Plot::Line in the vector ensures it is drawn first and appears behind the line.


Band attached to a line

LinePlot::with_band(y_lower, y_upper) is a one-call shorthand. It creates the BandPlot internally, using the line's x positions and inheriting its color automatically.

#![allow(unused)]
fn main() {
use kuva::plot::LinePlot;
use kuva::render::plots::Plot;

let line = LinePlot::new()
    .with_data(x.iter().copied().zip(y.iter().copied()))
    .with_color("firebrick")
    .with_band(lower, upper);  // band color = "firebrick", opacity = 0.2

let plots = vec![Plot::Line(line)];
}
Band attached to a line — damped cosine with confidence envelope

The band is always rendered behind the line. No ordering in the plots vector is needed.


Band attached to a scatter plot

ScatterPlot::with_band(y_lower, y_upper) works identically: the band inherits the scatter color and renders behind the points.

#![allow(unused)]
fn main() {
use kuva::plot::ScatterPlot;
use kuva::render::plots::Plot;

let scatter = ScatterPlot::new()
    .with_data(x.iter().copied().zip(y.iter().copied()))
    .with_color("seagreen")
    .with_band(lower, upper);

let plots = vec![Plot::Scatter(scatter)];
}
Band attached to a scatter plot — linear trend with prediction band

Multiple series with bands

Each LinePlot carries its own independent band. Combine all series in one plots vector — they share the same axes automatically.

#![allow(unused)]
fn main() {
use kuva::plot::LinePlot;
use kuva::render::plots::Plot;

let line1 = LinePlot::new()
    .with_data(x.iter().copied().zip(y1.iter().copied()))
    .with_color("steelblue")
    .with_band(y1.iter().map(|&v| v - 0.25), y1.iter().map(|&v| v + 0.25))
    .with_legend("sin(x)");

let line2 = LinePlot::new()
    .with_data(x.iter().copied().zip(y2.iter().copied()))
    .with_color("darkorange")
    .with_band(y2.iter().map(|&v| v - 0.25), y2.iter().map(|&v| v + 0.25))
    .with_legend("0.8 · cos(0.5x)");

let plots = vec![Plot::Line(line1), Plot::Line(line2)];
}
Two series each with their own confidence band

Opacity

.with_opacity(f) sets the fill transparency. The default 0.2 is deliberately light so that overlapping bands and the underlying line remain readable.

OpacityEffect
0.10.2Light; line and overlapping bands visible (default 0.2)
0.30.5Moderate; band is prominent
1.0Fully opaque; hides anything behind it

API reference

MethodDescription
BandPlot::new(x, y_lower, y_upper)Create a standalone band from three parallel iterables
.with_color(s)Fill color (default "steelblue")
.with_opacity(f)Fill opacity in [0.0, 1.0] (default 0.2)
.with_legend(s)Add a legend entry
LinePlot::with_band(lower, upper)Attach a band to a line (inherits color)
ScatterPlot::with_band(lower, upper)Attach a band to a scatter plot (inherits color)

Brick Plot

A brick plot displays sequences as rows of colored rectangles — one brick per character. It is designed for bioinformatics workflows involving DNA/RNA sequence visualization and tandem-repeat structure analysis. Each character maps to a color defined by a template; rows are labeled on the y-axis.

Import paths: kuva::plot::BrickPlot, kuva::plot::brick::BrickTemplate


DNA sequences

BrickTemplate::dna() provides a standard A / C / G / T color scheme. Pass the .template field to BrickPlot::with_template(). Use with_x_offset(n) to skip a common flanking region so the region of interest starts at x = 0.

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use kuva::plot::BrickPlot;
use kuva::plot::brick::BrickTemplate;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let tmpl = BrickTemplate::new().dna();

let plot = BrickPlot::new()
    .with_sequences(vec![
        "CGGCGATCAGGCCGCACTCATCATCATCATCATCATCAT",
        "CGGCGATCAGGCCGCACTCATCATCATCATCATCATCATCAT",
    ])
    .with_names(vec!["read_1", "read_2"])
    .with_template(tmpl.template)
    .with_x_offset(18.0);  // skip 18-base common prefix

let plots = vec![Plot::Brick(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("DNA Repeat Region");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("brick.svg", svg).unwrap();
}
Brick plot — DNA sequences with x-offset alignment

The 18-character flanking prefix is hidden by with_x_offset(18.0). All rows start at the same x = 0, aligning the CAT repeat region across reads.


Per-row offsets

When reads begin at different positions, with_x_offsets accepts one offset per row. Pass None for any row that should fall back to the global x_offset.

#![allow(unused)]
fn main() {
use kuva::plot::BrickPlot;
use kuva::plot::brick::BrickTemplate;
use kuva::render::plots::Plot;
let plot = BrickPlot::new()
    .with_sequences(sequences)
    .with_names(names)
    .with_template(BrickTemplate::new().dna().template)
    .with_x_offset(12.0)                       // global fallback
    .with_x_offsets(vec![
        Some(18.0_f64),  // read 1: skip 18
        Some(10.0),      // read 2: skip 10
        Some(16.0),      // read 3: skip 16
        Some(5.0),       // read 4: skip 5
        None,            // read 5: use global (12)
    ]);
}
Brick plot — per-row x offsets

Each row is shifted independently, aligning the repeat boundary across reads with different flanking lengths.


Custom template with value overlay

with_template accepts any HashMap<char, String>. Here a protein secondary-structure alphabet (H, E, C, T) gets custom colors. with_values() prints the character label inside each brick.

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use kuva::plot::BrickPlot;
use kuva::render::plots::Plot;

let mut tmpl: HashMap<char, String> = HashMap::new();
tmpl.insert('H', "steelblue".into());   // α-helix
tmpl.insert('E', "firebrick".into());   // β-strand
tmpl.insert('C', "#aaaaaa".into());     // coil
tmpl.insert('T', "seagreen".into());    // turn

let plot = BrickPlot::new()
    .with_sequences(vec!["CCCHHHHHHHHHHCCCCEEEEEECCC"])
    .with_names(vec!["prot_1"])
    .with_template(tmpl)
    .with_values();  // show letter labels inside bricks
}
Brick plot — custom secondary-structure alphabet with value overlay

Any single-character alphabet can be used — amino acids, repeat unit categories, chromatin states, etc.


Strigar mode (tandem-repeat motifs)

with_strigars switches to strigar mode for structured tandem-repeat data produced by BLADERUNNER. Each read is described by two strings:

  • motif string — maps local letters to k-mers: "CAT:A,C:B,T:C"
  • strigar string — run-length encoding of those letters: "10A1B4A1C1A"

with_strigars normalises k-mers across all reads by canonical rotation, assigns global letters (A, B, C, …) by frequency, auto-generates colors, and renders variable-width bricks proportional to each motif's nucleotide length.

#![allow(unused)]
fn main() {
use kuva::plot::BrickPlot;
use kuva::render::plots::Plot;
let strigars: Vec<(String, String)> = vec![
    ("CAT:A,C:B,T:C".to_string(),   "10A1B4A1C1A".to_string()),
    ("CAT:A,T:B".to_string(),        "14A1B1A".to_string()),
    ("CAT:A,C:B,GGT:C".to_string(), "10A1B8A1C5A".to_string()),
    // ...
];

let plot = BrickPlot::new()
    .with_names(names)
    .with_strigars(strigars);  // sequences not needed — derived from strigars
}
Brick plot — strigar mode showing CAT tandem repeats with interruptions

Bricks are proportional to motif length (CAT = 3 bp wide; single-nucleotide interruptions are narrower). The dominant repeat unit (CAT) is assigned letter A and the first color; rarer motifs receive subsequent letters and colors.


Built-in templates

MethodAlphabetColors
BrickTemplate::new().dna()A C G Tgreen / blue / orange / red
BrickTemplate::new().rna()A C G Ugreen / blue / orange / red

Access the populated map via .template and pass it to with_template().


API reference

MethodDescription
BrickPlot::new()Create with defaults
.with_sequences(iter)Load character sequences (one string per row)
.with_names(iter)Load row labels (one per sequence)
.with_template(map)Set HashMap<char, CSS color>
.with_x_offset(f)Global x-offset applied to all rows
.with_x_offsets(iter)Per-row offsets (f64 or Option<f64>; None → global fallback)
.with_values()Draw character labels inside bricks
.with_strigars(iter)Load strigar data and switch to strigar mode
BrickTemplate::new().dna()Pre-built DNA (A/C/G/T) color template
BrickTemplate::new().rna()Pre-built RNA (A/C/G/U) color template

Box Plot

A box plot (box-and-whisker plot) displays the five-number summary of one or more groups of values. Boxes show the interquartile range (Q1–Q3) with a median line; whiskers extend to the most extreme values within 1.5×IQR of the box edges (Tukey style). Individual data points can optionally be overlaid as a jittered strip or beeswarm.

Import path: kuva::plot::BoxPlot


Basic usage

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

#![allow(unused)]
fn main() {
use kuva::plot::BoxPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plot = BoxPlot::new()
    .with_group("Control",     vec![4.1, 5.0, 5.3, 5.8, 6.2, 7.0, 5.5, 4.8])
    .with_group("Treatment A", vec![5.5, 6.1, 6.4, 7.2, 7.8, 8.5, 6.9, 7.0])
    .with_group("Treatment B", vec![3.2, 4.0, 4.5, 4.8, 5.1, 5.9, 4.3, 4.7])
    .with_group("Treatment C", vec![6.0, 7.2, 7.5, 8.1, 8.8, 9.5, 7.9, 8.2])
    .with_color("steelblue");

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

let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("boxplot.svg", svg).unwrap();
}
Basic box plot

What the box shows

ElementMeaning
Bottom of boxQ1 — 25th percentile
Line in boxQ2 — median (50th percentile)
Top of boxQ3 — 75th percentile
Lower whiskerSmallest value ≥ Q1 − 1.5×IQR
Upper whiskerLargest value ≤ Q3 + 1.5×IQR

Values outside the whisker range are not drawn automatically — use an overlay to show them.


Point overlays

Overlaying the raw data on top of each box makes the sample size and distribution shape immediately visible. Both modes accept an optional color and point size.

Jittered strip

.with_strip(jitter) scatters points randomly within a horizontal band. The jitter argument controls the spread width (in data-axis units; 0.2 is a reasonable starting value).

#![allow(unused)]
fn main() {
use kuva::plot::BoxPlot;
use kuva::render::plots::Plot;

let plot = BoxPlot::new()
    .with_group("Control",     vec![/* values */])
    .with_group("Treatment A", vec![/* values */])
    .with_color("steelblue")
    .with_strip(0.2)
    .with_overlay_color("rgba(0,0,0,0.4)")
    .with_overlay_size(3.0);
}
Box plot with strip overlay

Beeswarm

.with_swarm_overlay() uses a beeswarm algorithm to spread points horizontally to avoid overlap. This gives a clearer picture of data density and is particularly useful for smaller datasets (roughly N < 200 per group).

#![allow(unused)]
fn main() {
use kuva::plot::BoxPlot;
use kuva::render::plots::Plot;

let plot = BoxPlot::new()
    .with_group("Control",     vec![/* values */])
    .with_group("Treatment A", vec![/* values */])
    .with_color("steelblue")
    .with_swarm_overlay()
    .with_overlay_color("rgba(0,0,0,0.4)")
    .with_overlay_size(3.0);
}
Box plot with swarm overlay

A semi-transparent overlay_color is recommended so the box remains visible beneath the points.


API reference

MethodDescription
BoxPlot::new()Create a box plot with defaults
.with_group(label, values)Add a group; accepts any Into<f64> iterable
.with_color(s)Box fill color (CSS color string)
.with_width(f)Box width as a fraction of the category slot (default 0.8)
.with_legend(s)Attach a legend label
.with_strip(jitter)Overlay jittered strip points; jitter is horizontal spread width
.with_swarm_overlay()Overlay beeswarm points (spread to avoid overlap)
.with_overlay_color(s)Color for overlay points (default "rgba(0,0,0,0.45)")
.with_overlay_size(r)Radius of overlay points in pixels (default 3.0)

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.

#![allow(unused)]
fn main() {
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();
}
Basic violin plot

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.

#![allow(unused)]
fn main() {
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).

#![allow(unused)]
fn main() {
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);
}
Narrow bandwidth Auto bandwidth Wide bandwidth
h = 0.15 (too narrow) Auto — Silverman h = 2.0 (too wide)

.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).

#![allow(unused)]
fn main() {
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));
}
Violin with swarm overlay

Jittered strip

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

#![allow(unused)]
fn main() {
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);
}

API reference

MethodDescription
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_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)

Pie Chart

A pie chart divides a circle into slices proportional to each category's value. Each slice has its own explicit color. Slice labels can be placed automatically (inside large slices, outside small ones), forced to one side, or replaced with a legend.

Import path: kuva::plot::PiePlot


Basic usage

Add slices with .with_slice(label, value, color). Slices are drawn clockwise from 12 o'clock in the order they are added.

#![allow(unused)]
fn main() {
use kuva::plot::PiePlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_pie;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let pie = PiePlot::new()
    .with_slice("Rust",   40.0, "steelblue")
    .with_slice("Python", 30.0, "tomato")
    .with_slice("R",      20.0, "seagreen")
    .with_slice("Other",  10.0, "gold");

let plots = vec![Plot::Pie(pie.clone())];
let layout = Layout::auto_from_plots(&plots).with_title("Pie Chart");

let scene = render_pie(&pie, &layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("pie.svg", svg).unwrap();
}
Basic pie chart

Only the ratio between slice values matters — absolute magnitudes are irrelevant. .with_slice("A", 1.0, ...) and .with_slice("A", 100.0, ...) produce the same slice if all others scale identically.


Donut chart

.with_inner_radius(r) cuts a hollow centre, converting the pie into a donut. r is the inner radius in pixels; the outer radius is computed from the canvas size. Values in the range 40.080.0 work well at the default canvas size.

#![allow(unused)]
fn main() {
use kuva::plot::PiePlot;
let pie = PiePlot::new()
    .with_slice("Rust",   40.0, "steelblue")
    .with_slice("Python", 30.0, "tomato")
    .with_slice("R",      20.0, "seagreen")
    .with_slice("Other",  10.0, "gold")
    .with_inner_radius(60.0);   // donut hole radius in pixels
}
Donut chart

Percentage labels

.with_percent() appends each slice's percentage of the total to its label, formatted to one decimal place (e.g. "Rust 40.0%").

#![allow(unused)]
fn main() {
use kuva::plot::PiePlot;
let pie = PiePlot::new()
    .with_slice("Rust",   40.0, "steelblue")
    .with_slice("Python", 30.0, "tomato")
    .with_slice("R",      20.0, "seagreen")
    .with_slice("Other",  10.0, "gold")
    .with_percent();
}
Pie chart with percentage labels

Label positioning

.with_label_position(PieLabelPosition) controls where labels appear.

VariantBehaviour
AutoInside large slices; outside (with leader line) for small ones. Default.
InsideAll labels placed at mid-radius, regardless of slice size.
OutsideAll labels outside with leader lines. Labels are spaced to avoid overlap.
NoneNo slice labels. Combine with a legend (see below).

Outside labels

Outside is recommended when slices vary widely in size or when many slices are present, since leader lines prevent labels from overlapping.

#![allow(unused)]
fn main() {
use kuva::plot::{PiePlot, PieLabelPosition};
use kuva::render::plots::Plot;

let pie = PiePlot::new()
    .with_slice("Apples",  30.0, "seagreen")
    .with_slice("Oranges", 25.0, "darkorange")
    .with_slice("Bananas", 20.0, "gold")
    .with_slice("Grapes",  12.0, "mediumpurple")
    .with_slice("Mango",    8.0, "coral")
    .with_slice("Kiwi",     5.0, "olivedrab")
    .with_label_position(PieLabelPosition::Outside);
}
Pie chart with outside labels

Minimum label fraction

By default, slices smaller than 5 % of the total are not labelled (to avoid cramped text). Adjust this with .with_min_label_fraction(f).

#![allow(unused)]
fn main() {
use kuva::plot::PiePlot;
// Label every slice, even slices below 5 %
let pie = PiePlot::new()
    .with_slice("Big",  90.0, "steelblue")
    .with_slice("Tiny",  1.0, "tomato")
    .with_min_label_fraction(0.0);   // default is 0.05
}

Legend

.with_legend("") enables a per-slice legend in the right margin. Each slice gets an entry (colored square + slice label); the slice labels come from .with_slice(), not from the string passed to .with_legend(). Use render_multiple instead of render_pie so the legend is rendered.

Combine with PieLabelPosition::None to use the legend as the sole means of identification.

#![allow(unused)]
fn main() {
use kuva::plot::{PiePlot, PieLabelPosition};
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let pie = PiePlot::new()
    .with_slice("Apples",  40.0, "seagreen")
    .with_slice("Oranges", 35.0, "darkorange")
    .with_slice("Grapes",  25.0, "mediumpurple")
    .with_legend("Fruit")
    .with_percent()
    .with_label_position(PieLabelPosition::None);  // legend replaces labels

let plots = vec![Plot::Pie(pie)];
let layout = Layout::auto_from_plots(&plots).with_title("Pie with Legend");

// render_multiple (not render_pie) so the legend is drawn
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("pie_legend.svg", svg).unwrap();
}
Pie chart with legend

API reference

MethodDescription
PiePlot::new()Create a pie chart with defaults
.with_slice(label, value, color)Add a slice; value is proportional
.with_inner_radius(r)Inner radius in pixels; > 0 makes a donut (default 0.0)
.with_percent()Append percentage to each slice label
.with_label_position(pos)Label placement: Auto, Inside, Outside, None (default Auto)
.with_min_label_fraction(f)Minimum slice fraction to receive a label (default 0.05)
.with_legend("")Enable the per-slice legend; entry labels are the slice names from .with_slice(). Use render_multiple to render it

Heatmap

A heatmap renders a two-dimensional grid where each cell's color encodes a numeric value. Values are normalized to the data range and passed through a color map. A colorbar is always shown in the right margin.

Import path: kuva::plot::Heatmap


Basic usage

Pass a 2-D array to .with_data(). The outer dimension is rows (top to bottom) and the inner dimension is columns (left to right).

#![allow(unused)]
fn main() {
use kuva::plot::Heatmap;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data = vec![
    vec![0.8, 0.3, 0.9, 0.2, 0.6],
    vec![0.4, 0.7, 0.1, 0.8, 0.3],
    vec![0.5, 0.9, 0.4, 0.6, 0.1],
    vec![0.2, 0.5, 0.8, 0.3, 0.7],
];

let heatmap = Heatmap::new().with_data(data);

let plots = vec![Plot::Heatmap(heatmap)];
let layout = Layout::auto_from_plots(&plots).with_title("Heatmap");

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

.with_data() accepts any iterable of iterables of numeric values — Vec<Vec<f64>>, slices, or any type implementing Into<f64>.


Axis labels

Axis labels are set on the Layout, not on the Heatmap struct. Pass column labels to .with_x_categories() and row labels to .with_y_categories().

#![allow(unused)]
fn main() {
use kuva::plot::Heatmap;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data = vec![
    vec![2.1, 0.4, 3.2, 1.1, 2.8],
    vec![0.9, 3.5, 0.3, 2.7, 1.2],
    vec![1.8, 2.9, 1.5, 0.6, 3.1],
    vec![3.3, 1.1, 2.0, 3.8, 0.5],
];

let col_labels = vec!["Ctrl", "T1", "T2", "T3", "T4"]
    .into_iter().map(String::from).collect::<Vec<_>>();
let row_labels = vec!["GeneA", "GeneB", "GeneC", "GeneD"]
    .into_iter().map(String::from).collect::<Vec<_>>();

let heatmap = Heatmap::new().with_data(data);
let plots = vec![Plot::Heatmap(heatmap)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Gene Expression Heatmap")
    .with_x_categories(col_labels)   // column labels on x-axis
    .with_y_categories(row_labels);  // row labels on y-axis

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Heatmap with axis labels

Value overlay

.with_values() prints each cell's raw numeric value (formatted to two decimal places) centered inside the cell. Most useful for small grids where the text remains legible.

#![allow(unused)]
fn main() {
use kuva::plot::Heatmap;
let heatmap = Heatmap::new()
    .with_data(vec![
        vec![10.0, 20.0, 30.0, 15.0],
        vec![45.0, 55.0, 25.0, 60.0],
        vec![70.0, 35.0, 80.0, 40.0],
        vec![50.0, 90.0, 65.0, 20.0],
    ])
    .with_values();
}
Heatmap with value overlay

Color maps

.with_color_map(ColorMap) selects the color encoding. The default is Viridis.

VariantScaleNotes
ViridisBlue → green → yellowPerceptually uniform; colorblind-safe. Default.
InfernoBlack → purple → yellowHigh-contrast; works in greyscale print
GrayscaleBlack → whiteClean publication style
Custom(Arc<Fn>)User-definedFull control
#![allow(unused)]
fn main() {
use kuva::plot::{Heatmap, ColorMap};
use kuva::render::plots::Plot;

let heatmap = Heatmap::new()
    .with_data(vec![vec![1.0, 2.0], vec![3.0, 4.0]])
    .with_color_map(ColorMap::Inferno);
}
Viridis Inferno Greyscale
Viridis Inferno Grayscale

Custom color map

For diverging scales or other custom encodings, use ColorMap::Custom with a closure that maps a normalized [0.0, 1.0] value to a CSS color string.

#![allow(unused)]
fn main() {
use std::sync::Arc;
use kuva::plot::{Heatmap, ColorMap};

// Blue-to-red diverging scale
let cmap = ColorMap::Custom(Arc::new(|t: f64| {
    let r = (t * 255.0) as u8;
    let b = ((1.0 - t) * 255.0) as u8;
    format!("rgb({r},0,{b})")
}));

let heatmap = Heatmap::new()
    .with_data(vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]])
    .with_color_map(cmap);
}

API reference

MethodDescription
Heatmap::new()Create a heatmap with defaults
.with_data(rows)Set grid data; accepts any numeric iterable of iterables
.with_color_map(map)Color encoding: Viridis, Inferno, Grayscale, or Custom (default Viridis)
.with_values()Print each cell's value as text inside the cell
.with_labels(rows, cols)Store label strings in the struct (pass to Layout to render them)
.with_legend(s)Attach a legend label

Layout methods used with heatmaps:

MethodDescription
Layout::with_x_categories(labels)Column labels on the x-axis
Layout::with_y_categories(labels)Row labels on the y-axis

Strip Plot

A strip plot (dot plot / univariate scatter) shows every individual data point along a categorical axis. Unlike a box or violin, nothing is summarised — the raw values are shown directly, making sample size and exact distribution shape immediately visible.

Import path: kuva::plot::StripPlot


Basic usage

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

#![allow(unused)]
fn main() {
use kuva::plot::StripPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let strip = StripPlot::new()
    .with_group("Control",   control_data)
    .with_group("Low dose",  low_data)
    .with_group("High dose", high_data)
    .with_group("Washout",   washout_data)
    .with_color("steelblue")
    .with_point_size(2.5)
    .with_jitter(0.35);

let plots = vec![Plot::Strip(strip)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Jittered Strip Plot")
    .with_y_label("Measurement");

let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("strip.svg", svg).unwrap();
}
Jittered strip plot

300 points per group. The jitter cloud fills out the slot width, making the spread and central tendency of each distribution easy to compare.


Layout modes

Three modes control how points are spread horizontally within each group slot.

Jittered strip

.with_jitter(j) assigns each point a random horizontal offset. j is the half-width as a fraction of the slot — 0.3 spreads points ±30 % of the slot width. This is the default (j = 0.3).

Use a smaller j to tighten the column or a larger j to spread it out. The jitter positions are randomised with a fixed seed (changeable via .with_seed()), so output is reproducible.

#![allow(unused)]
fn main() {
use kuva::plot::StripPlot;
let strip = StripPlot::new()
    .with_group("A", data)
    .with_jitter(0.35)      // ±35 % of slot width
    .with_point_size(2.5);
}

Beeswarm

.with_swarm() uses a deterministic algorithm to place each point as close to the group center as possible without overlapping any already-placed point. The outline of the resulting shape traces the density of the distribution.

Swarm works best for N < ~200 per group. With very large N, points are pushed far from center and the spread becomes impractical.

#![allow(unused)]
fn main() {
use kuva::plot::StripPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let strip = StripPlot::new()
    .with_group("Control",      normal_data)
    .with_group("Bimodal",      bimodal_data)
    .with_group("Right-skewed", skewed_data)
    .with_color("steelblue")
    .with_point_size(3.0)
    .with_swarm();

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

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Beeswarm strip plot

150 points per group. The bimodal group shows two distinct lobes; the right-skewed group shows the long tail — structure that jitter reveals less cleanly at this sample size.

Center stack

.with_center() places all points at x = group center with no horizontal spread, creating a vertical column. The density of the distribution is readable directly from where points are most tightly packed.

#![allow(unused)]
fn main() {
use kuva::plot::StripPlot;
let strip = StripPlot::new()
    .with_group("Normal",  normal_data)
    .with_group("Bimodal", bimodal_data)
    .with_group("Skewed",  skewed_data)
    .with_color("steelblue")
    .with_point_size(2.0)
    .with_center();
}
Center stack strip plot

400 points per group. The bimodal group shows a clear gap in the column; the skewed group has a dense cluster at the low end thinning toward the tail.


Composing with a box plot

A StripPlot can be layered on top of a BoxPlot by passing both to render_multiple. Use a semi-transparent rgba color for the strip so the box summary remains legible underneath.

#![allow(unused)]
fn main() {
use kuva::plot::{StripPlot, BoxPlot};
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let boxplot = BoxPlot::new()
    .with_group("Control",     control_data.clone())
    .with_group("Bimodal",     bimodal_data.clone())
    .with_group("High-spread", spread_data.clone())
    .with_color("steelblue");

let strip = StripPlot::new()
    .with_group("Control",     control_data)
    .with_group("Bimodal",     bimodal_data)
    .with_group("High-spread", spread_data)
    .with_color("rgba(0,0,0,0.3)")   // semi-transparent so box shows through
    .with_point_size(2.5)
    .with_jitter(0.2);

let plots = vec![Plot::Box(boxplot), Plot::Strip(strip)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Box + Strip")
    .with_y_label("Value");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Box plot with strip overlay

The box summarises Q1/median/Q3; the individual points reveal that the bimodal group has two sub-populations the box conceals entirely.


Multiple strip plots with a palette

Passing multiple StripPlots to render_multiple with a Layout::with_palette() auto-assigns distinct colors. Attach .with_legend() to each plot to identify them.

#![allow(unused)]
fn main() {
use kuva::plot::StripPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;
use kuva::Palette;

let line_a = StripPlot::new()
    .with_group("WT",  wt_a).with_group("HET", het_a).with_group("KO", ko_a)
    .with_jitter(0.3).with_point_size(2.5)
    .with_legend("Line A");

let line_b = StripPlot::new()
    .with_group("WT",  wt_b).with_group("HET", het_b).with_group("KO", ko_b)
    .with_jitter(0.3).with_point_size(2.5)
    .with_legend("Line B");

let plots = vec![Plot::Strip(line_a), Plot::Strip(line_b)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Two Lines – Palette")
    .with_y_label("Expression")
    .with_palette(Palette::wong());

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Two strip plots with palette colors

API reference

MethodDescription
StripPlot::new()Create a strip plot with defaults
.with_group(label, values)Add a group; accepts any Into<f64> iterable
.with_color(s)Point fill color (CSS color string, default "steelblue")
.with_point_size(r)Point radius in pixels (default 4.0)
.with_jitter(j)Jittered strip layout; j is half-width as fraction of slot (default 0.3)
.with_swarm()Beeswarm layout — non-overlapping, best for N < 200
.with_center()All points at group center — vertical density column
.with_seed(n)RNG seed for jitter positions (default 42)
.with_legend(s)Attach a legend label

Waterfall Chart

A waterfall chart shows a running total as a sequence of floating bars. Each bar starts where the previous one ended — rising for positive increments (green) and falling for negative ones (red). Summary bars can be placed at any point to show accumulated subtotals.

Import path: kuva::plot::WaterfallPlot


Basic usage

Add bars with .with_delta(label, value). The chart tracks a running total from left to right — each bar floats between the previous total and the new one.

#![allow(unused)]
fn main() {
use kuva::plot::WaterfallPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let wf = WaterfallPlot::new()
    .with_delta("Revenue",        850.0)
    .with_delta("Cost of goods", -340.0)
    .with_delta("Personnel",     -180.0)
    .with_delta("Operations",     -90.0)
    .with_delta("Marketing",      -70.0)
    .with_delta("Other income",    55.0)
    .with_delta("Tax",            -85.0);

let plots = vec![Plot::Waterfall(wf)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Revenue Breakdown")
    .with_y_label("USD (thousands)");

let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("waterfall.svg", svg).unwrap();
}
Basic waterfall chart

Green bars add to the running total; red bars subtract from it.


Total bars

.with_total(label) places a bar that spans from zero to the current running total, rendered in a distinct color (default "steelblue"). The value field is irrelevant — the bar height is the accumulated total at that position. Place totals after sections of delta bars to show intermediate subtotals.

#![allow(unused)]
fn main() {
use kuva::plot::WaterfallPlot;
use kuva::render::plots::Plot;
let wf = WaterfallPlot::new()
    .with_delta("Revenue",        850.0)
    .with_delta("Cost of goods", -340.0)
    .with_total("Gross profit")         // subtotal after first section
    .with_delta("Personnel",     -180.0)
    .with_delta("Operations",     -90.0)
    .with_delta("Marketing",      -70.0)
    .with_total("EBITDA")               // second subtotal
    .with_delta("Depreciation",   -40.0)
    .with_delta("Interest",       -20.0)
    .with_delta("Tax",            -65.0)
    .with_total("Net income");          // final total
}
Waterfall chart with totals

The three blue bars show gross profit, EBITDA, and net income alongside the individual line items that make them up.


Connectors and value labels

.with_connectors() draws a dashed horizontal line from the top (or bottom) of each bar to the start of the next, making the running total easier to trace across wide charts. .with_values() prints the numeric value of each bar.

#![allow(unused)]
fn main() {
use kuva::plot::WaterfallPlot;
use kuva::render::plots::Plot;
let wf = WaterfallPlot::new()
    .with_delta("Q1 sales",    420.0)
    .with_delta("Q2 sales",    380.0)
    .with_delta("Returns",     -95.0)
    .with_delta("Discounts",   -60.0)
    .with_total("H1 net")
    .with_delta("Q3 sales",   410.0)
    .with_delta("Q4 sales",   455.0)
    .with_delta("Returns",   -105.0)
    .with_delta("Discounts",  -70.0)
    .with_total("H2 net")
    .with_connectors()
    .with_values();
}
Waterfall with connectors and values

Difference bars

.with_difference(label, from, to) adds a standalone comparison bar anchored at explicit y-values rather than the running total. The bar spans [from, to] — green when to > from, red when to < from — and does not change the running total.

The clearest use is when from and to match the heights of existing Total bars, so the reader can trace the connection directly. In the example below both period totals are in the chart; the difference bar sits between those two reference levels and shows the gain between them.

#![allow(unused)]
fn main() {
use kuva::plot::WaterfallPlot;
use kuva::render::plots::Plot;

let wf = WaterfallPlot::new()
    .with_delta("Revenue",   500.0)   // running total → 500
    .with_delta("Costs",    -180.0)   // running total → 320
    .with_total("Period A")           // total bar: 0 → 320
    .with_delta("Revenue",   600.0)   // running total → 920
    .with_delta("Costs",    -190.0)   // running total → 730
    .with_total("Period B")           // total bar: 0 → 730
    // from = Period A total (320), to = Period B total (730)
    .with_difference("Period A→B", 320.0, 730.0)
    .with_values();
}
Waterfall with a difference bar

The "Period A→B" bar is anchored at 320–730 regardless of where the running total is. It does not alter the "Period B" total — it is purely a visual annotation showing the improvement between the two periods.


Custom colors

Override any of the three bar colors with CSS color strings.

#![allow(unused)]
fn main() {
use kuva::plot::WaterfallPlot;
let wf = WaterfallPlot::new()
    .with_delta("Gain",  100.0)
    .with_delta("Loss",  -40.0)
    .with_total("Net")
    .with_color_positive("darkgreen")   // default: "rgb(68,170,68)"
    .with_color_negative("crimson")     // default: "rgb(204,68,68)"
    .with_color_total("navy");          // default: "steelblue"
}

API reference

MethodDescription
WaterfallPlot::new()Create a waterfall chart with defaults
.with_delta(label, value)Floating increment/decrement bar; updates running total
.with_total(label)Summary bar from zero to current running total
.with_difference(label, from, to)Anchored comparison bar; does not affect running total
.with_bar_width(f)Bar width as a fraction of the slot (default 0.6)
.with_color_positive(s)Color for positive delta bars (default "rgb(68,170,68)")
.with_color_negative(s)Color for negative delta bars (default "rgb(204,68,68)")
.with_color_total(s)Color for total/subtotal bars (default "steelblue")
.with_connectors()Draw dashed connector lines between consecutive bars
.with_values()Print numeric values on each bar
.with_legend(s)Attach a legend label

Dot Plot

A dot plot (bubble matrix) places circles at the intersections of two categorical axes. Each circle encodes two independent continuous variables: size (radius) and color. This makes it well suited for compact display of multi-variable summaries across a grid — the canonical use case in bioinformatics is a gene expression dot plot where size shows the fraction of cells expressing a gene and color shows the mean expression level.

Import path: kuva::plot::DotPlot


Basic usage

Pass an iterator of (x_cat, y_cat, size, color) tuples to .with_data(). Category order on each axis follows first-seen insertion order. Enable legends independently with .with_size_legend() and .with_colorbar().

#![allow(unused)]
fn main() {
use kuva::plot::DotPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data = vec![
    // (x_cat,   y_cat,   size,  color)
    ("CD4 T",  "CD3E",  88.0_f64, 3.8_f64),
    ("CD8 T",  "CD3E",  91.0,     4.0    ),
    ("NK",     "CD3E",  12.0,     0.5    ),
    ("CD4 T",  "CD4",   85.0,     3.5    ),
    ("CD8 T",  "CD4",    8.0,     0.3    ),
    ("NK",     "CD4",    4.0,     0.2    ),
];

let dot = DotPlot::new()
    .with_data(data)
    .with_size_legend("% Expressing")
    .with_colorbar("Mean expression");

let plots = vec![Plot::DotPlot(dot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Gene Expression")
    .with_x_label("Cell type")
    .with_y_label("Gene");

let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("dotplot.svg", svg).unwrap();
}
Gene expression dot plot

Eight immune marker genes across six cell types. Large circles show genes expressed in many cells; bright colors (Viridis: yellow) show high mean expression. The cell-type specificity of each marker is immediately legible — CD3E is large in T cells, MS4A1 only in B cells, LYZ highest in monocytes.


Matrix input

.with_matrix(x_cats, y_cats, sizes, colors) accepts dense 2-D data where every grid cell is filled. sizes[row_i][col_j] maps to y_cats[row_i] and x_cats[col_j]. Use this when your data comes from a matrix or 2-D array.

#![allow(unused)]
fn main() {
use kuva::plot::DotPlot;
use kuva::render::plots::Plot;

let x_cats = vec!["TypeA", "TypeB", "TypeC", "TypeD", "TypeE"];
let y_cats = vec!["Gene1", "Gene2", "Gene3", "Gene4", "Gene5", "Gene6"];

// sizes[row_i][col_j] → y_cats[row_i], x_cats[col_j]
let sizes = vec![
    vec![80.0, 25.0, 60.0, 45.0, 70.0],
    vec![15.0, 90.0, 35.0, 70.0, 20.0],
    // ...
];
let colors = vec![
    vec![3.5, 1.2, 2.8, 2.0, 3.1],
    vec![0.8, 4.1, 1.5, 3.2, 0.9],
    // ...
];

let dot = DotPlot::new()
    .with_matrix(x_cats, y_cats, sizes, colors)
    .with_size_legend("% Expressing")
    .with_colorbar("Mean expression");
}
Dot plot from matrix input

Sparse data

With .with_data(), grid positions with no corresponding tuple are simply left empty — no circle is drawn at that cell. There is no need to fill missing values with zero or NaN.

#![allow(unused)]
fn main() {
use kuva::plot::DotPlot;
let dot = DotPlot::new().with_data(vec![
    ("TypeA", "GeneX", 80.0_f64, 2.5_f64),
    // ("TypeA", "GeneY") absent — no circle drawn
    ("TypeA", "GeneZ", 40.0,     1.2    ),
    ("TypeB", "GeneY", 90.0,     2.9    ),
]);
}

Legends

The size legend and colorbar are independent. Enable either, both, or neither.

Size legend only

.with_size_legend(label) adds a key in the right margin showing representative radii with their corresponding values. Use this when color carries no additional information (or when all dots share the same constant color value).

#![allow(unused)]
fn main() {
use kuva::plot::DotPlot;
let dot = DotPlot::new()
    .with_data(data)
    .with_size_legend("% Expressing");   // only size legend; no colorbar
}
Dot plot with size legend only

Colorbar only

.with_colorbar(label) adds a colorbar for the color encoding. Useful when all dots should be the same size (pass a constant for the size field) so the color variable is the sole focus.

#![allow(unused)]
fn main() {
use kuva::plot::DotPlot;
let dot = DotPlot::new()
    .with_data(data)
    .with_colorbar("Mean expression");   // only colorbar; no size legend
}
Dot plot with colorbar only

Both legends

When both are set, the size legend and colorbar are stacked in a single right-margin column automatically.

#![allow(unused)]
fn main() {
use kuva::plot::DotPlot;
let dot = DotPlot::new()
    .with_data(data)
    .with_size_legend("% Expressing")
    .with_colorbar("Mean expression");
}

Clamping ranges

By default the size and color encodings are normalised to the data extent automatically. Use .with_size_range() and .with_color_range() to fix an explicit [min, max] — useful for excluding outliers or keeping a consistent scale across multiple plots.

#![allow(unused)]
fn main() {
use kuva::plot::DotPlot;
let dot = DotPlot::new()
    .with_data(data)
    // Clamp size to 0–100 % regardless of data range;
    // values above 100 map to max_radius.
    .with_size_range(0.0, 100.0)
    // Fix color scale to 0–5 log-normalised expression.
    .with_color_range(0.0, 5.0);
}

Radius range

.with_max_radius(px) and .with_min_radius(px) set the pixel size limits (defaults: 12.0 and 1.0). Increase max_radius for a larger grid or reduce it for dense plots with many categories.

#![allow(unused)]
fn main() {
use kuva::plot::DotPlot;
let dot = DotPlot::new()
    .with_data(data)
    .with_max_radius(18.0)   // default 12.0
    .with_min_radius(2.0);   // default 1.0
}

Color maps

.with_color_map(ColorMap) selects the color encoding (default Viridis). The same ColorMap variants available for heatmaps apply here: Viridis, Inferno, Grayscale, and Custom.

#![allow(unused)]
fn main() {
use kuva::plot::{DotPlot, ColorMap};
use kuva::render::plots::Plot;

let dot = DotPlot::new()
    .with_data(data)
    .with_color_map(ColorMap::Inferno);
}

API reference

MethodDescription
DotPlot::new()Create a dot plot with defaults
.with_data(iter)Sparse input: iterator of (x_cat, y_cat, size, color) tuples
.with_matrix(x, y, sizes, colors)Dense input: category lists + 2-D matrices
.with_color_map(map)Color encoding: Viridis, Inferno, Grayscale, Custom (default Viridis)
.with_max_radius(px)Largest circle radius in pixels (default 12.0)
.with_min_radius(px)Smallest circle radius in pixels (default 1.0)
.with_size_range(min, max)Clamp size values before normalising (default: data extent)
.with_color_range(min, max)Clamp color values before normalising (default: data extent)
.with_size_legend(label)Add a size key in the right margin
.with_colorbar(label)Add a colorbar in the right margin

Stacked Area Plot

A stacked area chart places multiple series on top of each other so the reader can see both the individual contribution of each series and the combined total at any x position. It is well suited for showing how a whole is composed of parts over a continuous axis — typically time.

Import path: kuva::plot::StackedAreaPlot


Basic usage

Set the x values with .with_x(), then add series one at a time with .with_series(). Call .with_color() and .with_legend() immediately after each series to configure it — these methods always operate on the most recently added series.

#![allow(unused)]
fn main() {
use kuva::plot::StackedAreaPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let months: Vec<f64> = (1..=12).map(|m| m as f64).collect();

let sa = StackedAreaPlot::new()
    .with_x(months)
    .with_series([420.0, 445.0, 398.0, 510.0, 488.0, 501.0,
                  467.0, 523.0, 495.0, 540.0, 518.0, 555.0])
    .with_color("steelblue").with_legend("SNVs")
    .with_series([ 95.0, 102.0,  88.0, 115.0, 108.0, 112.0,
                   98.0, 125.0, 118.0, 130.0, 122.0, 140.0])
    .with_color("orange").with_legend("Indels")
    .with_series([ 22.0,  25.0,  20.0,  28.0,  26.0,  27.0,
                   24.0,  31.0,  28.0,  33.0,  30.0,  35.0])
    .with_color("mediumseagreen").with_legend("SVs")
    .with_series([ 15.0,  17.0,  14.0,  19.0,  18.0,  18.0,
                   16.0,  21.0,  19.0,  23.0,  21.0,  24.0])
    .with_color("tomato").with_legend("CNVs");

let plots = vec![Plot::StackedArea(sa)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Monthly Variant Counts by Type")
    .with_x_label("Month")
    .with_y_label("Variant count");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("stacked_area.svg", svg).unwrap();
}
Stacked area plot of monthly variant counts

Four variant types are stacked so the total height of each column shows the aggregate monthly count while the coloured bands show how much each type contributes. The growing trend in SNVs lifts all upper bands over time.


Normalized (100 % stacking)

.with_normalized() rescales each column so all series sum to 100 %, shifting the y-axis to span 0–100 %. Use this when you want to emphasise proportional composition rather than absolute magnitude.

#![allow(unused)]
fn main() {
use kuva::plot::StackedAreaPlot;
let sa = StackedAreaPlot::new()
    .with_x(months)
    // ... add series ...
    .with_normalized();
}
Normalized stacked area plot

The relative dominance of SNVs (~77 %) is immediately clear and the slight month-to-month shifts in variant-type mix become visible — information that was hidden in the absolute chart by the large SNV counts.


Stroke lines

By default a stroke is drawn along the top edge of each band. .with_strokes(false) removes all outlines for a softer, flat appearance — useful when the colors provide enough contrast.

#![allow(unused)]
fn main() {
use kuva::plot::StackedAreaPlot;
let sa = StackedAreaPlot::new()
    .with_x(months)
    // ... add series ...
    .with_strokes(false);
}
Stacked area plot without stroke outlines

Legend position

.with_legend_position(pos) controls which corner of the plot area the legend occupies. Four positions are available from LegendPosition:

VariantDescription
TopRightUpper-right corner (default)
TopLeftUpper-left corner
BottomRightLower-right corner
BottomLeftLower-left corner
#![allow(unused)]
fn main() {
use kuva::plot::{StackedAreaPlot, LegendPosition};

let sa = StackedAreaPlot::new()
    .with_x(months)
    // ... add series ...
    .with_legend_position(LegendPosition::BottomLeft);
}
Stacked area plot with legend at bottom left

Styling

Fill opacity

.with_fill_opacity(f) sets the transparency of every band (default 0.7, range 0.01.0). Lower values let background grid lines show through; 1.0 gives fully opaque fills.

Stroke width

.with_stroke_width(px) sets the thickness of the top-edge strokes (default 1.5). Has no effect when .with_strokes(false) is set.

#![allow(unused)]
fn main() {
use kuva::plot::StackedAreaPlot;
let sa = StackedAreaPlot::new()
    .with_x(months)
    // ... add series ...
    .with_fill_opacity(0.9)   // nearly opaque bands
    .with_stroke_width(2.5);  // thicker border lines
}

Colors

If no color is set for a series the built-in fallback palette is used (cycling when there are more than eight series):

steelblue, orange, green, red, purple, brown, pink, gray

Set an explicit color by calling .with_color() immediately after .with_series():

#![allow(unused)]
fn main() {
use kuva::plot::StackedAreaPlot;
let sa = StackedAreaPlot::new()
    .with_x([1.0, 2.0, 3.0])
    .with_series([10.0, 20.0, 15.0]).with_color("#2c7bb6").with_legend("Group A")
    .with_series([ 5.0,  8.0,  6.0]).with_color("#d7191c").with_legend("Group B");
}

API reference

MethodDescription
StackedAreaPlot::new()Create a plot with defaults
.with_x(iter)Set shared x-axis values
.with_series(iter)Append a new y series
.with_color(s)Fill color of the most recently added series
.with_legend(s)Legend label of the most recently added series
.with_fill_opacity(f)Band transparency — 0.0 to 1.0 (default 0.7)
.with_stroke_width(px)Top-edge stroke thickness (default 1.5)
.with_strokes(bool)Show/hide top-edge strokes (default true)
.with_normalized()Enable 100 % percent-stacking
.with_legend_position(pos)Legend corner: TopRight, TopLeft, BottomRight, BottomLeft

Candlestick Plot

A candlestick chart visualises OHLC (open, high, low, close) data. Each candle encodes four values for a single period:

  • The body spans from open to close. Green = close above open (bullish); red = close below open (bearish); gray = close equals open (doji).
  • The wicks extend from the body to the period high (upper wick) and low (lower wick).

An optional volume panel shows trading volume as bars below the price chart.

Import path: kuva::plot::CandlestickPlot


Basic usage

Add candles one at a time with .with_candle(label, open, high, low, close). Labels are placed on the x-axis as category ticks and candles are spaced evenly.

#![allow(unused)]
fn main() {
use kuva::plot::CandlestickPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plot = CandlestickPlot::new()
    .with_candle("Nov 01", 142.50, 146.20, 141.80, 145.30)  // bullish
    .with_candle("Nov 02", 145.40, 147.80, 143.50, 144.10)  // bearish
    .with_candle("Nov 03", 144.10, 144.90, 142.20, 144.10)  // doji
    .with_candle("Nov 04", 143.80, 148.50, 143.20, 147.90)  // bullish
    .with_candle("Nov 05", 147.90, 150.20, 146.30, 149.80); // bullish

let plots = vec![Plot::Candlestick(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Daily OHLC — November")
    .with_x_label("Date")
    .with_y_label("Price (USD)");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("candlestick.svg", svg).unwrap();
}
Candlestick chart — 20 daily candles

Twenty trading days showing the full mix of bullish, bearish, and doji candles. The overall upward trend is visible from the rising body positions — two doji sessions (Nov 03 and Nov 21) mark brief pauses in the move.


Volume panel

Attach volume data with .with_volume(iter) then call .with_volume_panel() to render a bar sub-panel below the price chart. Volume bars are colored to match their candle (green for up days, red for down days).

#![allow(unused)]
fn main() {
use kuva::plot::CandlestickPlot;
let plot = CandlestickPlot::new()
    .with_candle("Nov 01", 142.50, 146.20, 141.80, 145.30)
    .with_candle("Nov 02", 145.40, 147.80, 143.50, 144.10)
    // ... more candles ...
    .with_volume([1_250_000.0, 980_000.0 /*, ... */])
    .with_volume_panel();
}
Candlestick with volume panel

The volume panel occupies the bottom 22 % of the chart area by default. Adjust with .with_volume_ratio(f) — for example .with_volume_ratio(0.30) gives the panel 30 % of the height.


Custom colors

Replace the default green/red/gray with any CSS color string using .with_color_up(), .with_color_down(), and .with_color_doji().

#![allow(unused)]
fn main() {
use kuva::plot::CandlestickPlot;
let plot = CandlestickPlot::new()
    .with_color_up("#00c896")     // teal green
    .with_color_down("#ff4560")   // bright red
    .with_color_doji("#aaaaaa")   // light gray
    .with_candle("Mon", 100.0, 105.8, 99.2, 104.5)
    .with_candle("Tue", 104.5, 106.2, 103.1, 103.4)
    // ...
    ;
}
Candlestick with custom colors

Numeric x-axis

.with_candle_at(x, label, open, high, low, close) places a candle at an explicit numeric x position, enabling uneven spacing and a true numeric axis. This is useful for quarterly, monthly, or any irregularly spaced data.

When using this mode, set .with_candle_width(w) to a value in data units smaller than the spacing between candles (e.g. 0.15 for quarterly data spaced 0.25 apart).

#![allow(unused)]
fn main() {
use kuva::plot::CandlestickPlot;
let plot = CandlestickPlot::new()
    // x = fractional year; candles spaced 0.25 apart
    .with_candle_at(2023.00, "Q1'23", 110.5, 118.0, 110.0, 116.8)
    .with_candle_at(2023.25, "Q2'23", 116.8, 122.0, 115.5, 121.0)
    .with_candle_at(2023.50, "Q3'23", 121.0, 125.5, 119.0, 120.2)
    .with_candle_at(2023.75, "Q4'23", 120.2, 128.0, 119.8, 127.0)
    .with_candle_width(0.15);
}
Candlestick with numeric x-axis — quarterly data

Twelve quarters (2022 Q1 – 2024 Q4) on a continuous fractional-year axis. The x-axis tick marks are placed at round numeric values rather than category slots.


Legend

.with_legend(label) adds a legend box inside the plot area identifying the series.

#![allow(unused)]
fn main() {
use kuva::plot::CandlestickPlot;
let plot = CandlestickPlot::new()
    .with_candle("Jan", 100.0, 108.0, 98.0, 106.0)
    .with_candle("Feb", 106.0, 112.0, 104.0, 110.5)
    .with_legend("ACME Corp");
}

Sizing

MethodDefaultDescription
.with_candle_width(f)0.7Body width as fraction of slot (categorical) or in data units (numeric)
.with_wick_width(px)1.5Wick stroke width in pixels
.with_volume_ratio(f)0.22Fraction of chart height used by the volume panel

API reference

MethodDescription
CandlestickPlot::new()Create a plot with defaults
.with_candle(label, o, h, l, c)Append a candle at the next categorical position
.with_candle_at(x, label, o, h, l, c)Append a candle at an explicit numeric x position
.with_volume(iter)Attach volume values to existing candles (in order)
.with_volume_panel()Enable the volume bar sub-panel
.with_volume_ratio(f)Panel height as fraction of total chart (default 0.22)
.with_candle_width(f)Body width — fraction of slot or data units (default 0.7)
.with_wick_width(px)Wick stroke width in pixels (default 1.5)
.with_color_up(s)Bullish candle color (default green)
.with_color_down(s)Bearish candle color (default red)
.with_color_doji(s)Doji candle color (default #888888)
.with_legend(s)Add a legend box with the given label

Contour Plot

A contour plot draws iso-lines (or filled iso-bands) on a 2D scalar field, connecting all points that share the same z value. It is well suited for visualising any continuous surface: density functions, spatial expression gradients, topographic elevation, or any field that varies over an x–y plane.

Import path: kuva::plot::ContourPlot


Basic usage — iso-lines from a grid

Supply a pre-computed grid with .with_grid(z, x_coords, y_coords). z[row][col] is the scalar value at position (x_coords[col], y_coords[row]). By default, n_levels evenly spaced iso-lines are drawn (default 8).

#![allow(unused)]
fn main() {
use kuva::plot::ContourPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

// Build a 60×60 Gaussian grid over [-3, 3]²
let n = 60_usize;
let coords: Vec<f64> = (0..n)
    .map(|i| -3.0 + i as f64 / (n - 1) as f64 * 6.0)
    .collect();
let z: Vec<Vec<f64>> = coords.iter()
    .map(|&y| coords.iter()
        .map(|&x| (-(x * x + y * y) / 2.0).exp())
        .collect())
    .collect();

let cp = ContourPlot::new()
    .with_grid(z, coords.clone(), coords)
    .with_n_levels(10)
    .with_line_color("steelblue")
    .with_line_width(1.2);

let plots = vec![Plot::Contour(cp)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Iso-line Contours — Gaussian Peak")
    .with_x_label("x")
    .with_y_label("y");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("contour.svg", svg).unwrap();
}
Iso-line contour plot of a Gaussian peak

Ten evenly spaced iso-lines trace the nested ellipses of the Gaussian peak. A fixed line color is used here; without .with_line_color() each iso-line would be colored by the active colormap.


Filled contours

.with_filled() fills each band between adjacent iso-levels using the colormap. .with_legend(label) enables a colorbar in the right margin.

#![allow(unused)]
fn main() {
use kuva::plot::ContourPlot;
use kuva::render::plots::Plot;
let cp = ContourPlot::new()
    .with_grid(z, xs, ys)
    .with_n_levels(9)
    .with_filled()
    .with_legend("Density");
}
Filled contour plot — bimodal surface

A bimodal surface with two overlapping peaks. The Viridis colormap maps low z values to purple/blue and high values to yellow. The colorbar on the right shows the full z range.


Scattered input — IDW interpolation

.with_points(iter) accepts an iterator of (x, y, z) triples at arbitrary positions — no regular grid required. The values are interpolated onto an internal 50×50 grid using inverse-distance weighting (IDW) before iso-lines are computed. This is the natural input mode for spatial data such as tissue sample coordinates or irregular sensor readings.

#![allow(unused)]
fn main() {
use kuva::plot::{ContourPlot, ColorMap};
use kuva::render::plots::Plot;
// 121 sample points from a bimodal function
let pts: Vec<(f64, f64, f64)> = (-5..=5)
    .flat_map(|i| (-5..=5).map(move |j| {
        let (x, y) = (i as f64, j as f64);
        let z = (- ((x - 1.5) * (x - 1.5) + (y - 1.5) * (y - 1.5)) / 4.0).exp()
              + 0.7 * (- ((x + 2.0) * (x + 2.0) + (y + 1.5) * (y + 1.5)) / 3.0).exp();
        (x, y, z)
    }))
    .collect();

let cp = ContourPlot::new()
    .with_points(pts)
    .with_n_levels(8)
    .with_filled()
    .with_colormap(ColorMap::Inferno)
    .with_legend("Value");
}
Contour plot from scattered IDW input

The Inferno colormap with filled bands on IDW-interpolated data. Denser point clouds produce sharper interpolations; the 50×50 internal grid is fixed regardless of input count.


Explicit iso-levels

.with_levels(&[…]) pins the iso-lines to specific z values. This overrides n_levels. Use it when lines should correspond to meaningful thresholds — specific expression cutoffs, probability contours, or fixed elevation intervals.

#![allow(unused)]
fn main() {
use kuva::plot::ContourPlot;
use kuva::render::plots::Plot;
let (z, xs, ys) = (vec![vec![0.0f64]], vec![0.0f64], vec![0.0f64]);
let cp = ContourPlot::new()
    .with_grid(z, xs, ys)
    .with_levels(&[0.1, 0.25, 0.5, 0.75, 0.9])
    .with_line_color("darkgreen")
    .with_line_width(1.5);
}
Contour plot with explicit iso-levels

Five explicit iso-lines at z = 0.1, 0.25, 0.5, 0.75, 0.9 on the same Gaussian. The innermost ring at 0.9 is very tight around the peak; the outermost at 0.1 reaches almost to the grid boundary.


Line color

By default each iso-line is colored using the active colormap. .with_line_color(s) overrides this with a single fixed color for all lines — useful for clean black-and-white figures or when the colormap is reserved for a filled background.

#![allow(unused)]
fn main() {
use kuva::plot::ContourPlot;
let (z, xs, ys) = (vec![vec![0.0f64]], vec![0.0f64], vec![0.0f64]);
let cp = ContourPlot::new()
    .with_grid(z, xs, ys)
    .with_line_color("navy")    // all iso-lines in navy
    .with_line_width(1.2);
}

Color maps

.with_colormap(map) selects the colormap (default Viridis). The same ColorMap variants available for heatmaps apply: Viridis, Inferno, Grayscale, and Custom.

#![allow(unused)]
fn main() {
use kuva::plot::{ContourPlot, ColorMap};
let (z, xs, ys) = (vec![vec![0.0f64]], vec![0.0f64], vec![0.0f64]);
let cp = ContourPlot::new()
    .with_grid(z, xs, ys)
    .with_filled()
    .with_colormap(ColorMap::Inferno)
    .with_legend("Density");
}

API reference

MethodDescription
ContourPlot::new()Create a plot with defaults
.with_grid(z, xs, ys)Regular grid input: z[row][col] with coordinate vectors
.with_points(iter)Scattered (x, y, z) input — IDW interpolated to 50×50 grid
.with_n_levels(n)Number of auto-spaced iso-levels (default 8)
.with_levels(&[…])Explicit iso-level values — overrides n_levels
.with_filled()Enable filled color bands between iso-levels
.with_colormap(map)Color map: Viridis, Inferno, Grayscale, Custom (default Viridis)
.with_line_color(s)Fixed color for all iso-lines (default: derive from colormap)
.with_line_width(px)Iso-line stroke width in pixels (default 1.0)
.with_legend(s)Colorbar label (filled mode) or line legend entry (line mode)

Chord Diagram

A chord diagram arranges N nodes around a circle and connects them with ribbons whose widths are proportional to flow magnitudes from an N×N matrix. Each node occupies an arc on the outer ring; arc length is proportional to the node's total flow. It is well suited for showing pairwise relationships in network data — co-occurrence, migration, regulatory influence, or any square flow matrix.

Import path: kuva::plot::ChordPlot


Basic usage

Supply an N×N matrix with .with_matrix() and node labels with .with_labels(). The diagram renders in pixel space — no x/y axis system is used. A title set on the Layout is still shown.

#![allow(unused)]
fn main() {
use kuva::plot::ChordPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

// Co-clustering proximity scores between PBMC cell types
let matrix = vec![
    //         CD4T   CD8T    NK   Bcell   Mono
    vec![   0.0, 120.0,  70.0,  40.0,  25.0],  // CD4 T
    vec![ 120.0,   0.0,  88.0,  32.0,  18.0],  // CD8 T
    vec![  70.0,  88.0,   0.0,  15.0,  35.0],  // NK
    vec![  40.0,  32.0,  15.0,   0.0,  10.0],  // B cell
    vec![  25.0,  18.0,  35.0,  10.0,   0.0],  // Monocyte
];

let chord = ChordPlot::new()
    .with_matrix(matrix)
    .with_labels(["CD4 T", "CD8 T", "NK", "B cell", "Monocyte"]);

let plots = vec![Plot::Chord(chord)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("PBMC Cell Type Co-clustering");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("chord.svg", svg).unwrap();
}
Chord diagram — PBMC cell type co-clustering

The symmetric matrix means each ribbon has equal width at both ends. The CD4 T–CD8 T ribbon is the widest because those two cell types have the highest co-clustering score (120); B cell–Monocyte is the thinnest (10). Colors come from the default category10 palette.


Asymmetric (directed) flows

When matrix[i][j] ≠ matrix[j][i], flows are directed — for example regulatory influence, migration counts, or transition probabilities. The ribbon is thicker at the source end (high outgoing flow) and thinner at the target end.

Use .with_colors() to assign explicit per-node colors, and .with_legend() to show a color-coded node legend.

#![allow(unused)]
fn main() {
use kuva::plot::ChordPlot;
use kuva::render::plots::Plot;
// Directed regulatory influence between five transcription factors
let matrix = vec![
    vec![ 0.0, 85.0, 20.0, 45.0, 10.0],  // TF1 → others
    vec![15.0,  0.0, 65.0, 30.0,  8.0],  // TF2 → others
    vec![30.0, 12.0,  0.0, 75.0, 25.0],  // TF3 → others
    vec![ 5.0, 40.0, 18.0,  0.0, 90.0],  // TF4 → others
    vec![50.0,  8.0, 35.0, 12.0,  0.0],  // TF5 → others
];

let chord = ChordPlot::new()
    .with_matrix(matrix)
    .with_labels(["TF1", "TF2", "TF3", "TF4", "TF5"])
    .with_colors(["#e6194b", "#3cb44b", "#4363d8", "#f58231", "#911eb4"])
    .with_gap(3.0)
    .with_legend("Transcription factors");
}
Asymmetric chord diagram — gene regulatory network

TF4→TF5 (90) and TF1→TF2 (85) are the strongest regulatory edges. Asymmetry is visible — the TF4→TF5 ribbon is much thicker at the TF4 end than the TF5→TF4 end (5). The legend in the top-right corner maps each color to its node label.


Gap and opacity

.with_gap(degrees) controls the white space between adjacent arc segments (default 2.0°). Larger gaps make individual nodes easier to distinguish at the cost of compressing arc lengths.

.with_opacity(f) sets ribbon transparency (default 0.7). Reducing opacity helps readability when many ribbons overlap in the centre.

#![allow(unused)]
fn main() {
use kuva::plot::ChordPlot;
let matrix = vec![vec![0.0_f64; 5]; 5];
let chord = ChordPlot::new()
    .with_matrix(matrix)
    .with_labels(["CD4 T", "CD8 T", "NK", "B cell", "Monocyte"])
    .with_gap(6.0)       // default 2.0 — wider arc separation
    .with_opacity(0.45); // default 0.7 — more transparent ribbons
}
Chord diagram with wider gaps and reduced opacity

The same co-clustering data as the basic example. Wider gaps make each cell type's arc clearly separate; the lower opacity lets the arc ring show through the ribbon bundle in the centre.


Matrix layout

The N×N matrix convention:

EntryMeaning
matrix[i][j]Flow from node i to node j
matrix[i][i]Self-loop (typically 0.0 — not rendered)
Symmetric matrixUndirected relationships (co-occurrence, correlation)
Asymmetric matrixDirected flows (migration, regulation, transitions)

Arc length for node i is proportional to the row sum of matrix[i]. In a symmetric matrix this equals the column sum, so arcs represent total pairwise interaction strength.


Colors

Without .with_colors(), node colors are assigned automatically from the category10 palette (cycling for more than ten nodes). Supply explicit per-node colors to match publication figures or color-blind-safe palettes:

#![allow(unused)]
fn main() {
use kuva::plot::ChordPlot;
let chord = ChordPlot::new()
    .with_matrix(vec![vec![0.0, 1.0], vec![1.0, 0.0]])
    .with_labels(["Group A", "Group B"])
    .with_colors(["#377eb8", "#e41a1c"]);   // ColorBrewer blue / red
}

API reference

MethodDescription
ChordPlot::new()Create a chord plot with defaults
.with_matrix(m)N×N flow matrix: m[i][j] = flow from node i to node j
.with_labels(iter)Node labels — one per row/column
.with_colors(iter)Per-node fill colors (default: category10 palette)
.with_gap(deg)Gap between arc segments in degrees (default 2.0)
.with_opacity(f)Ribbon fill opacity 0.01.0 (default 0.7)
.with_legend(s)Enable a per-node color legend

Sankey Diagram

A Sankey diagram arranges nodes in columns and connects them with tapered ribbons whose widths are proportional to flow magnitude. It is well suited for showing multi-stage flows — energy transfer, budget allocation, data processing pipelines, or any directed network where quantities must be conserved through each stage.

Import path: kuva::plot::SankeyPlot


Basic usage

Add directed links with .with_link(source, target, value). Nodes are created automatically from the label strings; column positions are inferred by tracing the flow graph from left to right.

#![allow(unused)]
fn main() {
use kuva::plot::SankeyPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let sankey = SankeyPlot::new()
    .with_link("Input", "Process A", 50.0)
    .with_link("Input", "Process B", 30.0)
    .with_link("Process A", "Output X", 40.0)
    .with_link("Process A", "Output Y", 10.0)
    .with_link("Process B", "Output X", 10.0)
    .with_link("Process B", "Output Y", 20.0);

let plots = vec![Plot::Sankey(sankey)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Energy Flow");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("sankey.svg", svg).unwrap();
}
Basic Sankey diagram

Node heights are proportional to the larger of incoming and outgoing flow at each node. Colors come from the default category10 palette. Each label is printed to the left or right of its column.


Node colors & legend

Set per-node fill colors with .with_node_color(label, color). Call .with_legend("") to enable the legend; each node's name becomes an entry label automatically.

#![allow(unused)]
fn main() {
use kuva::plot::SankeyPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let sankey = SankeyPlot::new()
    .with_node_color("Input",     "#888888")
    .with_node_color("Process A", "#377eb8")
    .with_node_color("Process B", "#4daf4a")
    .with_node_color("Output",    "#984ea3")
    .with_link("Input",     "Process A", 40.0)
    .with_link("Input",     "Process B", 30.0)
    .with_link("Process A", "Output",    35.0)
    .with_link("Process B", "Output",    25.0)
    .with_node_width(24.0)
    .with_legend("Stage");

let plots = vec![Plot::Sankey(sankey)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Node Colors & Legend");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Sankey with node colors and legend

Ribbons inherit the source node color by default. To color nodes without linking them first (e.g. to control palette order), use .with_node(label) to declare a node explicitly before adding links.


Gradient ribbons

.with_gradient_links() renders each ribbon as a linear gradient from the source node color to the target node color.

#![allow(unused)]
fn main() {
use kuva::plot::SankeyPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let sankey = SankeyPlot::new()
    .with_node_color("Budget",    "#e41a1c")
    .with_node_color("R&D",       "#377eb8")
    .with_node_color("Marketing", "#4daf4a")
    .with_node_color("Ops",       "#ff7f00")
    .with_node_color("Product A", "#984ea3")
    .with_node_color("Product B", "#a65628")
    .with_link("Budget",    "R&D",       40.0)
    .with_link("Budget",    "Marketing", 25.0)
    .with_link("Budget",    "Ops",       35.0)
    .with_link("R&D",       "Product A", 25.0)
    .with_link("R&D",       "Product B", 15.0)
    .with_link("Marketing", "Product A", 15.0)
    .with_link("Marketing", "Product B", 10.0)
    .with_link("Ops",       "Product A", 20.0)
    .with_link("Ops",       "Product B", 15.0)
    .with_gradient_links()
    .with_link_opacity(0.6);

let plots = vec![Plot::Sankey(sankey)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Budget Allocation — Gradient Ribbons");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Sankey with gradient ribbons

For full control, supply a color on each link individually with .with_link_colored(), then call .with_per_link_colors() to activate that mode:

#![allow(unused)]
fn main() {
use kuva::plot::SankeyPlot;
let sankey = SankeyPlot::new()
    .with_link_colored("Budget", "R&D",       40.0, "#377eb8")
    .with_link_colored("Budget", "Marketing", 25.0, "#e41a1c")
    .with_link_colored("Budget", "Ops",       35.0, "#4daf4a")
    // …remaining links…
    .with_per_link_colors()
    .with_link_opacity(0.55);
}

.with_links() accepts any iterator of (source, target, value) triples, which is convenient when links come from a data file or computed table:

#![allow(unused)]
fn main() {
use kuva::plot::SankeyPlot;
let edges: Vec<(&str, &str, f64)> = vec![
    ("A", "B", 10.0),
    ("A", "C", 20.0),
    ("B", "D", 10.0),
    ("C", "D", 20.0),
];

let sankey = SankeyPlot::new().with_links(edges);
}

Column layout

By default, columns are assigned by propagating each node one step further right than its leftmost source (BFS order). Use .with_node_column(label, col) to pin specific nodes to explicit columns when the automatic layout is incorrect — for example, when a node should appear in a later column despite having an early source:

#![allow(unused)]
fn main() {
use kuva::plot::SankeyPlot;
let sankey = SankeyPlot::new()
    .with_node_column("Input",  0)
    .with_node_column("Middle", 1)
    .with_node_column("Output", 2)
    .with_link("Input",  "Middle", 80.0)
    .with_link("Input",  "Output", 20.0)   // skip link — goes directly to col 2
    .with_link("Middle", "Output", 80.0);
}

Bioinformatics example

A 4-stage variant filtering pipeline: raw variants pass QC, get classified by confidence level, and are split into variant types or discarded.

#![allow(unused)]
fn main() {
use kuva::plot::SankeyPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let sankey = SankeyPlot::new()
    .with_node_color("Raw Variants", "#888888")
    .with_node_color("QC Pass",      "#4daf4a")
    .with_node_color("QC Fail",      "#e41a1c")
    .with_node_color("High Conf",    "#377eb8")
    .with_node_color("Low Conf",     "#ff7f00")
    .with_node_color("SNP",          "#984ea3")
    .with_node_color("Indel",        "#a65628")
    .with_node_color("Filtered Out", "#cccccc")
    .with_link("Raw Variants", "QC Pass",       8000.0)
    .with_link("Raw Variants", "QC Fail",       2000.0)
    .with_link("QC Pass",      "High Conf",     6000.0)
    .with_link("QC Pass",      "Low Conf",      2000.0)
    .with_link("High Conf",    "SNP",           4500.0)
    .with_link("High Conf",    "Indel",         1200.0)
    .with_link("High Conf",    "Filtered Out",   300.0)
    .with_link("Low Conf",     "SNP",            800.0)
    .with_link("Low Conf",     "Filtered Out",  1200.0)
    .with_link_opacity(0.45)
    .with_legend("Stage");

let plots = vec![Plot::Sankey(sankey)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Variant Filtering Pipeline");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Sankey — variant filtering pipeline

API reference

MethodDescription
SankeyPlot::new()Create a Sankey plot with defaults
.with_link(src, tgt, val)Add a directed link; auto-creates nodes from labels
.with_link_colored(src, tgt, val, color)Add a link with an explicit ribbon color
.with_links(iter)Bulk-add links from (source, target, value) triples
.with_node(label)Declare a node without adding a link
.with_node_color(label, color)Set a node's fill color (CSS color string)
.with_node_column(label, col)Pin a node to a specific column (0-indexed)
.with_node_width(px)Node rectangle width in pixels (default 20.0)
.with_node_gap(px)Minimum vertical gap between nodes in a column (default 8.0)
.with_gradient_links()Ribbons fade from source to target color
.with_per_link_colors()Use per-link color set by .with_link_colored()
.with_link_opacity(f)Ribbon fill opacity 0.01.0 (default 0.5)
.with_legend("")Enable the legend; one entry per node, labeled with the node name

SankeyLinkColor variants

VariantBehavior
SourceRibbon inherits the source node color (default)
GradientSVG linearGradient from source to target color
PerLinkColor from .with_link_colored() per ribbon

Phylogenetic Tree

A phylogenetic tree (dendrogram) visualises evolutionary or hierarchical relationships between taxa. kuva supports three branch styles (rectangular, slanted, circular), four orientations, phylogram mode for branch-length-accurate layouts, clade coloring, and support value display. Trees can be constructed from Newick strings, edge lists, pairwise distance matrices, or scipy/R linkage output.

Import path: kuva::plot::PhyloTree


Basic usage

Parse a Newick string with PhyloTree::from_newick(). Branch lengths are optional; support values on internal nodes are read automatically. Call .with_support_threshold(t) to display any support value ≥ t on the plot.

#![allow(unused)]
fn main() {
use kuva::plot::PhyloTree;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let tree = PhyloTree::from_newick(
    "((TaxonA:1.0,TaxonB:2.0)95:1.0,(TaxonC:0.5,TaxonD:0.5)88:1.5,TaxonE:3.0);"
)
.with_support_threshold(80.0);   // show internal node values ≥ 80

let plots = vec![Plot::PhyloTree(tree)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Rectangular Tree");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("phylo.svg", svg).unwrap();
}
Basic rectangular phylogenetic tree

The default layout is rectangular branches, root on the left, leaves on the right (cladogram mode — all leaves aligned). Support values are drawn beside their internal node.


Phylogram mode

In cladogram mode (the default) all leaves are aligned regardless of branch length. Enable phylogram mode with .with_phylogram() to position each node according to the accumulated branch lengths from the root, so edge lengths directly represent evolutionary distance.

#![allow(unused)]
fn main() {
use kuva::plot::{PhyloTree, TreeOrientation};
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let tree = PhyloTree::from_newick(
    "((A:1.0,B:3.0)90:1.0,(C:2.0,(D:0.5,E:1.5)85:1.0):2.0);"
)
.with_orientation(TreeOrientation::Top)
.with_phylogram()
.with_support_threshold(80.0);

let plots = vec![Plot::PhyloTree(tree)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Phylogram (Top orientation)");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Phylogram — branch lengths on depth axis, Top orientation

Circular layout

.with_branch_style(TreeBranchStyle::Circular) projects the tree radially — the root is at the centre and leaves fan outward. This style is especially useful for large trees where a linear layout becomes tall.

#![allow(unused)]
fn main() {
use kuva::plot::{PhyloTree, TreeBranchStyle};
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let newick = concat!(
    "(((((Sp_A:0.05,Sp_B:0.08):0.12,(Sp_C:0.07,Sp_D:0.06):0.10):0.15,",
    "((Sp_E:0.09,Sp_F:0.11):0.08,(Sp_G:0.06,Sp_H:0.10):0.13):0.12):0.20,",
    "(((Sp_I:0.08,Sp_J:0.12):0.10,(Sp_K:0.05,Sp_L:0.09):0.11):0.15,",
    "((Sp_M:0.07,Sp_N:0.08):0.09,(Sp_O:0.10,Sp_P:0.06):0.12):0.14):0.18):0.10,",
    "((Sp_Q:0.15,Sp_R:0.12):0.20,(Sp_S:0.08,Sp_T:0.10):0.18):0.25);"
);

let tree = PhyloTree::from_newick(newick)
    .with_branch_style(TreeBranchStyle::Circular)
    .with_phylogram();

let plots = vec![Plot::PhyloTree(tree)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Circular Tree — 20 Taxa");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Circular radial tree — 20 taxa

Clade coloring

.with_clade_color(node_id, color) colors the entire subtree rooted at node_id. Call .with_legend("") to enable the legend; each colored clade gets an entry labeled with its node's name (or "Node N" for unnamed internal nodes).

When building a tree with from_edges(), node IDs are assigned in order of first appearance across all (parent, child, length) tuples — the first unique label encountered becomes node 0, the second node 1, and so on.

#![allow(unused)]
fn main() {
use kuva::plot::PhyloTree;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let edges: Vec<(&str, &str, f64)> = vec![
    ("root",     "Bacteria",    1.5),  // root=0, Bacteria=1
    ("root",     "Eukarya",     2.0),  //         Eukarya=2
    ("Bacteria", "E. coli",     0.5),
    ("Bacteria", "B. subtilis", 0.7),
    ("Eukarya",  "Yeast",       1.0),
    ("Eukarya",  "Human",       0.8),
];

let tree = PhyloTree::from_edges(&edges)
    .with_clade_color(1, "#e41a1c")   // Bacteria subtree
    .with_clade_color(2, "#377eb8")   // Eukarya subtree
    .with_legend("Domains");

let plots = vec![Plot::PhyloTree(tree)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Clade Coloring by Domain");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Phylogenetic tree with clade coloring

UPGMA from a distance matrix

PhyloTree::from_distance_matrix(labels, dist) clusters taxa by UPGMA and returns a rooted ultrametric tree. The distance matrix must be square and symmetric; diagonal values are ignored.

#![allow(unused)]
fn main() {
use kuva::plot::PhyloTree;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let labels = ["Wolf", "Cat", "Whale", "Human"];
let dist = vec![
    //         Wolf  Cat   Whale Human
    vec![0.0,  0.5,  0.9,  0.8],  // Wolf
    vec![0.5,  0.0,  0.9,  0.8],  // Cat
    vec![0.9,  0.9,  0.0,  0.7],  // Whale
    vec![0.8,  0.8,  0.7,  0.0],  // Human
];

let tree = PhyloTree::from_distance_matrix(&labels, &dist)
    .with_phylogram();

let plots = vec![Plot::PhyloTree(tree)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("UPGMA Tree from Distance Matrix");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
UPGMA tree from pairwise distance matrix

Wolf and Cat cluster first (distance 0.5), reflecting their close relationship; Whale and Human share the next merge (0.7) before joining the carnivore clade.


Scipy / R linkage input

PhyloTree::from_linkage(labels, linkage) accepts the output format of scipy.cluster.hierarchy.linkage or R's hclust. Each row is [left_idx, right_idx, distance, n_leaves]. Leaf indices 0..n-1 correspond to labels; merge nodes use indices n...

#![allow(unused)]
fn main() {
use kuva::plot::PhyloTree;
let labels = ["A", "B", "C", "D"];
// linkage matrix produced by scipy or R
let linkage: &[[f64; 4]] = &[
    [0.0, 1.0, 0.5, 2.0],   // merge leaf 0 (A) + leaf 1 (B) at d=0.5
    [2.0, 3.0, 0.7, 2.0],   // merge leaf 2 (C) + leaf 3 (D) at d=0.7
    [4.0, 5.0, 1.2, 4.0],   // merge node 4 + node 5 at d=1.2
];

let tree = PhyloTree::from_linkage(labels, linkage).with_phylogram();
}

Orientations

Four root positions are available via TreeOrientation:

VariantRoot position
LeftLeft edge — leaves fan rightward (default)
RightRight edge — leaves fan leftward
TopTop edge — leaves hang downward
BottomBottom edge — leaves grow upward
#![allow(unused)]
fn main() {
use kuva::plot::{PhyloTree, TreeOrientation};
let tree = PhyloTree::from_newick("((A:1,B:2):1,C:3);")
    .with_orientation(TreeOrientation::Top);
}

Branch styles

VariantShape
RectangularRight-angle elbow at the parent depth (default)
SlantedSingle diagonal segment from parent to child
CircularPolar / radial projection
#![allow(unused)]
fn main() {
use kuva::plot::{PhyloTree, TreeBranchStyle};
let slanted = PhyloTree::from_newick("((A:1,B:2):1,C:3);")
    .with_branch_style(TreeBranchStyle::Slanted);
}

Heatmap alignment

leaf_labels_top_to_bottom() returns leaf labels in the exact top-to-bottom render order. Pass the result to Heatmap::with_y_categories() to align heatmap rows with tree leaves when composing both plots side by side.

#![allow(unused)]
fn main() {
use kuva::plot::{PhyloTree, Heatmap};

let tree = PhyloTree::from_newick("((A:1,B:2):1,C:3);");
let leaf_order = tree.leaf_labels_top_to_bottom();  // ["A", "B", "C"]

// Use leaf_order as the y-axis category order for a paired heatmap
let heatmap = Heatmap::new()
    .with_y_categories(leaf_order);
}

API reference

Constructors

MethodDescription
PhyloTree::from_newick(s)Parse a Newick-format string
PhyloTree::from_edges(edges)Build from (parent, child, branch_length) triples
PhyloTree::from_distance_matrix(labels, dist)UPGMA clustering of a pairwise distance matrix
PhyloTree::from_linkage(labels, linkage)Import scipy / R hclust linkage output

Builder methods

MethodDescription
.with_orientation(TreeOrientation)Root position (default Left)
.with_branch_style(TreeBranchStyle)Branch drawing style (default Rectangular)
.with_phylogram()Use branch lengths for the depth axis
.with_branch_color(s)Line color for branches (default "black")
.with_leaf_color(s)Text color for leaf labels (default "black")
.with_support_threshold(t)Show support values ≥ t at internal nodes
.with_clade_color(node_id, color)Color the entire subtree rooted at node_id
.with_legend("")Enable the clade legend; one entry per colored clade, labeled with the node name

Helper

MethodDescription
.leaf_labels_top_to_bottom()Leaf labels in render order — use to align a Heatmap

TreeOrientation variants

Left (default) · Right · Top · Bottom

TreeBranchStyle variants

Rectangular (default) · Slanted · Circular

Synteny Plot

A synteny plot visualises conserved genomic regions between two or more sequences. Each sequence is drawn as a horizontal bar; collinear blocks are drawn as ribbons connecting the matching regions. Forward blocks use parallel-sided ribbons; inverted (reverse-complement) blocks draw crossed (bowtie) ribbons. The plot is well suited for comparing chromosomes, assembled genomes, or any ordered sequence data.

Import path: kuva::plot::synteny::SyntenyPlot


Basic usage

Add sequences with .with_sequences() as (label, length) pairs, then connect collinear regions with .with_block(seq1, start1, end1, seq2, start2, end2). Sequences are referred to by their 0-based index in the order they were added.

#![allow(unused)]
fn main() {
use kuva::plot::synteny::SyntenyPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plot = SyntenyPlot::new()
    .with_sequences([
        ("Human chr1", 248_956_422.0),
        ("Mouse chr1", 195_471_971.0),
    ])
    .with_block(0,   0.0,        50_000_000.0,  1,   0.0,        45_000_000.0)
    .with_block(0,  60_000_000.0, 120_000_000.0, 1,  55_000_000.0, 100_000_000.0)
    .with_block(0, 130_000_000.0, 200_000_000.0, 1, 110_000_000.0, 170_000_000.0);

let plots = vec![Plot::Synteny(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Human chr1 vs Mouse chr1");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("synteny.svg", svg).unwrap();
}
Pairwise synteny — Human vs Mouse chr1

By default each bar fills the full plot width regardless of sequence length (per-sequence scale). Ribbons are drawn before bars so the bars overlay them cleanly. Labels are right-padded to the left of each bar.


Inversions

.with_inv_block() marks a region as reverse-complement: the ribbon connects the right edge of the source interval to the left edge of the target (and vice versa), producing a characteristic crossed or bowtie shape.

#![allow(unused)]
fn main() {
use kuva::plot::synteny::SyntenyPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plot = SyntenyPlot::new()
    .with_sequences([("Seq A", 1_000_000.0), ("Seq B", 1_000_000.0)])
    .with_block(    0,       0.0, 200_000.0, 1,       0.0, 200_000.0)
    .with_inv_block(0, 250_000.0, 500_000.0, 1, 250_000.0, 500_000.0)  // inverted
    .with_block(    0, 600_000.0, 900_000.0, 1, 600_000.0, 900_000.0);

let plots = vec![Plot::Synteny(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Forward and Inverted Blocks");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Synteny plot with a crossed inversion ribbon

Multiple sequences

Add more than two sequences to show a stack of pairwise comparisons. A block can connect any two sequence indices — typically adjacent pairs for a multi-genome alignment view.

#![allow(unused)]
fn main() {
use kuva::plot::synteny::SyntenyPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plot = SyntenyPlot::new()
    .with_sequences([
        ("Genome A", 500_000.0),
        ("Genome B", 480_000.0),
        ("Genome C", 450_000.0),
    ])
    // blocks between sequences 0 and 1
    .with_block(    0,       0.0, 100_000.0, 1,       0.0,  95_000.0)
    .with_block(    0, 150_000.0, 300_000.0, 1, 140_000.0, 280_000.0)
    // blocks between sequences 1 and 2
    .with_block(    1,       0.0, 100_000.0, 2,   5_000.0, 105_000.0)
    .with_inv_block(1, 200_000.0, 350_000.0, 2, 190_000.0, 340_000.0);

let plots = vec![Plot::Synteny(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Three-way Synteny");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Synteny plot with three sequences

Blocks do not need to connect adjacent sequences — a block between sequences 0 and 2 will span the full height of the diagram.


Custom colors & legend

Set bar colors with .with_sequence_colors(). Override the ribbon color of individual blocks with .with_colored_block() or .with_colored_inv_block(). Call .with_legend("") to enable the legend; each sequence gets an entry labeled with its name.

#![allow(unused)]
fn main() {
use kuva::plot::synteny::SyntenyPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let plot = SyntenyPlot::new()
    .with_sequences([("Seq 1", 500_000.0), ("Seq 2", 500_000.0)])
    .with_sequence_colors(["#4393c3", "#d6604d"])          // bar fill colors
    .with_colored_block(    0,       0.0, 150_000.0,
                            1,       0.0, 150_000.0, "#2ca02c")
    .with_colored_block(    0, 200_000.0, 350_000.0,
                            1, 200_000.0, 340_000.0, "#9467bd")
    .with_colored_inv_block(0, 380_000.0, 480_000.0,
                             1, 370_000.0, 470_000.0, "#ff7f0e")
    .with_legend("Blocks");

let plots = vec![Plot::Synteny(plot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Custom Colors & Legend");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
}
Synteny with custom bar and ribbon colors

Without explicit block colors, ribbons inherit the source sequence bar color.


Shared scale

By default each bar fills the full plot width independently (per-sequence scale), which maximises detail for each sequence but hides length differences between them. Call .with_shared_scale() to use a common ruler: each bar's width is proportional to sequence_length / max_length, so relative sizes are accurately represented.

#![allow(unused)]
fn main() {
use kuva::plot::synteny::SyntenyPlot;
let plot = SyntenyPlot::new()
    .with_sequences([("Long", 1_000_000.0), ("Short", 400_000.0)])
    .with_shared_scale()                                   // shorter bar is 40% width
    .with_block(0,       0.0, 300_000.0, 1,      0.0, 300_000.0)
    .with_block(0, 350_000.0, 700_000.0, 1, 50_000.0, 380_000.0);
}

Bulk block loading

.with_blocks() accepts an iterator of pre-built SyntenyBlock structs, which is convenient when blocks come from a parsed alignment file:

#![allow(unused)]
fn main() {
use kuva::plot::synteny::{SyntenyPlot, SyntenyBlock, Strand};

let blocks: Vec<SyntenyBlock> = vec![
    SyntenyBlock { seq1: 0, start1:       0.0, end1: 100_000.0,
                   seq2: 1, start2:       0.0, end2:  95_000.0,
                   strand: Strand::Forward, color: None },
    SyntenyBlock { seq1: 0, start1: 150_000.0, end1: 300_000.0,
                   seq2: 1, start2: 140_000.0, end2: 280_000.0,
                   strand: Strand::Reverse, color: Some("#e41a1c".into()) },
];

let plot = SyntenyPlot::new()
    .with_sequences([("Ref", 500_000.0), ("Query", 480_000.0)])
    .with_blocks(blocks);
}

API reference

MethodDescription
SyntenyPlot::new()Create a synteny plot with defaults
.with_sequences(iter)Add sequences from (label, length) pairs
.with_sequence_colors(iter)Override bar fill colors (parallel to sequences)
.with_block(s1, start1, end1, s2, start2, end2)Add a forward collinear block
.with_inv_block(s1, start1, end1, s2, start2, end2)Add an inverted (crossed ribbon) block
.with_colored_block(s1, start1, end1, s2, start2, end2, color)Forward block with explicit ribbon color
.with_colored_inv_block(s1, start1, end1, s2, start2, end2, color)Inverted block with explicit ribbon color
.with_blocks(iter)Batch-add pre-built SyntenyBlock structs
.with_bar_height(px)Sequence bar height in pixels (default 18.0)
.with_opacity(f)Ribbon fill opacity 0.01.0 (default 0.65)
.with_shared_scale()Use a common ruler — bar width proportional to sequence length
.with_legend("")Enable the legend; one entry per sequence, labeled with the sequence name

Strand variants

VariantRibbon shape
ForwardParallel-sided trapezoid
ReverseCrossed / bowtie

UpSet Plot

An UpSet plot is the scalable successor to Venn diagrams for showing set intersections when there are more than three or four sets. It uses three visual components:

  • Intersection size bars (top): vertical bars showing how many elements belong to each exact intersection.
  • Dot matrix (middle): a grid of circles. Filled dots indicate which sets participate in each intersection; a vertical line connects them.
  • Set size bars (left, optional): horizontal bars showing the total size of each set.

Import path: kuva::plot::UpSetPlot


Basic usage

Pass (name, elements) pairs to .with_sets(). Intersection counts are computed automatically — each element is assigned to the unique combination of sets it belongs to.

#![allow(unused)]
fn main() {
use kuva::plot::UpSetPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

// Overlapping differentially expressed genes across four treatment conditions
let up = UpSetPlot::new()
    .with_sets(vec![
        ("Condition A", (1u32..=40).collect::<Vec<_>>()),
        ("Condition B", (21u32..=55).collect::<Vec<_>>()),
        ("Condition C", (31u32..=58).collect::<Vec<_>>()),
        ("Condition D", (1u32..=10).chain(45..=60).collect::<Vec<_>>()),
    ]);

let plots = vec![Plot::UpSet(up)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("DEG Overlap Across Treatment Conditions");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("upset.svg", svg).unwrap();
}
UpSet plot — DEG overlap across four conditions

Intersections are sorted by frequency (default) so the largest bars are leftmost. The tallest bar (A∩B∩C: 10) is at the far left; the dot matrix below it shows three filled circles connected by a vertical line. The set-size bars on the left confirm Condition A has the most DEGs (40).


Precomputed data

.with_data(names, sizes, intersections) accepts precomputed counts directly when you do not have the individual elements — for example summary output from an external tool.

The third argument is an iterator of (mask, count) pairs. The mask is a bitmask: bit i is set when set_names[i] participates in that intersection.

#![allow(unused)]
fn main() {
use kuva::plot::UpSetPlot;
use kuva::render::plots::Plot;
// Three variant-calling pipelines
// bit 0 = GATK, bit 1 = FreeBayes, bit 2 = Strelka
let up = UpSetPlot::new()
    .with_data(
        ["GATK", "FreeBayes", "Strelka"],
        [280usize, 263, 249],   // total variants per pipeline
        vec![
            (0b001, 45),   // GATK only
            (0b010, 35),   // FreeBayes only
            (0b100, 28),   // Strelka only
            (0b011, 62),   // GATK ∩ FreeBayes
            (0b101, 55),   // GATK ∩ Strelka
            (0b110, 48),   // FreeBayes ∩ Strelka
            (0b111, 118),  // all three (high-confidence)
        ],
    )
    .with_max_visible(5)      // show only the top 5 intersections
    .without_set_sizes();     // hide the left set-size panel
}
UpSet plot — variant callers, precomputed data

118 variants are called by all three pipelines (the high-confidence set). with_max_visible(5) caps the display to the five largest intersections after sorting; without_set_sizes() removes the left panel for a more compact layout.


Sort order

.with_sort(order) controls the left-to-right ordering of intersection columns.

VariantBehaviour
ByFrequencyLargest bar leftmost (default)
ByDegreeMost sets involved first, ties broken by count
NaturalInput order preserved (useful with with_data)

ByDegree is useful when you specifically want to highlight complex multi-set intersections rather than the most common ones:

#![allow(unused)]
fn main() {
use kuva::plot::{UpSetPlot, UpSetSort};
use kuva::render::plots::Plot;
let up = UpSetPlot::new()
    .with_sets(vec![
        ("Condition A", (1u32..=40).collect::<Vec<_>>()),
        ("Condition B", (21u32..=55).collect::<Vec<_>>()),
        ("Condition C", (31u32..=58).collect::<Vec<_>>()),
        ("Condition D", (1u32..=10).chain(45u32..=60).collect::<Vec<_>>()),
    ])
    .with_sort(UpSetSort::ByDegree)
    .with_bar_color("#1d4ed8")
    .with_dot_color("#1e3a8a");
}
UpSet plot — degree sort, custom colors

The leftmost columns now show intersections involving the most sets. Custom blue bar and dot colors make the plot match a specific color scheme.


Limiting visible intersections

.with_max_visible(n) keeps only the first n intersections after sorting. This is essential for plots with many sets — five sets have up to 31 non-empty intersections, which can be too many to read. The limit is applied after sorting, so the hidden intersections are always the smallest (or lowest-degree) ones.

#![allow(unused)]
fn main() {
use kuva::plot::UpSetPlot;
use kuva::render::plots::Plot;
let up = UpSetPlot::new()
    .with_sets(/* five sets */)
    .with_max_visible(15);
}

Hiding set-size bars

.without_set_sizes() removes the left panel showing each set's total size. Use this for a more compact layout when set sizes are already known from context or when the focus is entirely on the intersections.


Colors

MethodDefaultDescription
.with_bar_color(s)"#333333"Intersection and set-size bar fill
.with_dot_color(s)"#333333"Filled dot color (participating set)

Non-participating dots are always shown in light gray (#dddddd) for contrast.


API reference

MethodDescription
UpSetPlot::new()Create a plot with defaults
.with_sets(iter)Raw (name, elements) pairs — intersections auto-computed
.with_data(names, sizes, masks)Precomputed (mask, count) intersection data
.with_sort(order)ByFrequency (default), ByDegree, or Natural
.with_max_visible(n)Show only the top n intersections after sorting
.without_set_sizes()Hide the left set-size bar panel
.with_bar_color(s)Intersection and set-size bar color (default "#333333")
.with_dot_color(s)Filled dot color (default "#333333")

Terminal output

UpSet plots are not yet supported in terminal mode. Running kuva upset --terminal prints a message to stderr and exits cleanly; use -o file.svg to generate an SVG instead.

Volcano Plot

A volcano plot visualises differential expression results by plotting log₂ fold change (x-axis) against −log₁₀(p-value) (y-axis). Points that pass both the fold-change and p-value thresholds are colored as up-regulated (right) or down-regulated (left); all others are shown as not-significant (gray). Dashed threshold lines are drawn automatically.

Import path: kuva::plot::VolcanoPlot


Basic usage

Pass (gene_name, log2fc, pvalue) tuples to .with_points(). Default thresholds are |log2FC| > 1.0 and p < 0.05.

#![allow(unused)]
fn main() {
use kuva::plot::VolcanoPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let results: Vec<(&str, f64, f64)> = vec![
    ("AKT1",   3.5, 5e-5),
    ("EGFR",   3.2, 1e-4),
    ("VHL",   -3.0, 5e-4),
    ("P21",   -3.2, 2e-4),
    ("GAPDH",  0.3, 0.50),
    // ...
];

let vp = VolcanoPlot::new()
    .with_points(results)
    .with_legend("DEG status");

let plots = vec![Plot::Volcano(vp)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Tumour vs. Normal — Volcano Plot")
    .with_x_label("log₂ fold change")
    .with_y_label("−log₁₀(p-value)");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("volcano.svg", svg).unwrap();
}
Volcano plot — tumour vs. normal

Up-regulated genes (red, right) include AKT1, EGFR, and FGFR1 at the top right. Down-regulated genes (blue, left) include P21, MLH1, and VHL. House-keeping genes (GAPDH, ACTB) and genes with large fold change but non-significant p-values (GeneC, GeneD) appear in gray. The legend shows the Up / Down / NS classification.


Gene labels

.with_label_top(n) labels the n most significant points (lowest p-values). Three placement styles are available via .with_label_style().

Nudge (default)

Labels are sorted by x position and nudged vertically to reduce stacking — the best default for most datasets.

#![allow(unused)]
fn main() {
use kuva::plot::VolcanoPlot;
use kuva::render::plots::Plot;
let vp = VolcanoPlot::new()
    .with_points(results)
    .with_label_top(12);  // LabelStyle::Nudge is the default
}
Volcano plot with nudge-style gene labels

The twelve most significant genes are labeled. Labels are spread vertically to avoid overlap while staying close to their points.

Arrow

LabelStyle::Arrow { offset_x, offset_y } moves labels by the given pixel offset and draws a short gray leader line back to the point. Useful when labels would crowd the high-significance region.

#![allow(unused)]
fn main() {
use kuva::plot::{VolcanoPlot, LabelStyle};
use kuva::render::plots::Plot;
let vp = VolcanoPlot::new()
    .with_points(results)
    .with_label_top(10)
    .with_label_style(LabelStyle::Arrow { offset_x: 14.0, offset_y: 16.0 });
}
Volcano plot with arrow-style gene labels

Each label is offset from its point by 14 px horizontally and 16 px vertically with a connecting line. This style works well for the top points in the upper corners where nudging alone may not provide enough separation.

Exact

LabelStyle::Exact places labels at the precise point position without adjustment. Labels may overlap on dense data; use this for sparse plots or when you will post-process the SVG.


Thresholds

.with_fc_cutoff(f) and .with_p_cutoff(f) set the classification thresholds. Dashed vertical lines appear at ±fc_cutoff; a dashed horizontal line appears at −log10(p_cutoff).

#![allow(unused)]
fn main() {
use kuva::plot::VolcanoPlot;
use kuva::render::plots::Plot;
let vp = VolcanoPlot::new()
    .with_points(results)
    .with_fc_cutoff(2.0)   // default 1.0 — require ≥ 4× fold change
    .with_p_cutoff(0.01)   // default 0.05 — stricter significance
    .with_label_top(8);
}
Volcano plot with stricter thresholds

With |log2FC| > 2 and p < 0.01, fewer genes pass both filters. The threshold lines move inward on the x-axis and upward on the y-axis compared to the defaults. Orange/purple colors distinguish this comparison from the default red/blue palette.


Colors

MethodDefaultDescription
.with_color_up(s)"firebrick"Points with log2FC ≥ fc_cutoff and p ≤ p_cutoff
.with_color_down(s)"steelblue"Points with log2FC ≤ −fc_cutoff and p ≤ p_cutoff
.with_color_ns(s)"#aaaaaa"All other points

All color methods accept any CSS color string.


Zero and extreme p-values

p-values of exactly 0.0 cannot be log-transformed. They are automatically clamped to the smallest non-zero p-value in the data. To set an explicit ceiling (useful when comparing multiple plots on the same y-axis scale):

#![allow(unused)]
fn main() {
use kuva::plot::VolcanoPlot;
let vp = VolcanoPlot::new()
    .with_points(results)
    .with_pvalue_floor(1e-10);  // y-axis ceiling fixed at 10
}

API reference

MethodDescription
VolcanoPlot::new()Create a plot with defaults
.with_point(name, fc, p)Add a single point
.with_points(iter)Add points from a (name, log2fc, pvalue) iterator
.with_fc_cutoff(f)|log₂FC| threshold (default 1.0)
.with_p_cutoff(f)p-value significance threshold (default 0.05)
.with_color_up(s)Up-regulated point color (default "firebrick")
.with_color_down(s)Down-regulated point color (default "steelblue")
.with_color_ns(s)Not-significant point color (default "#aaaaaa")
.with_point_size(f)Circle radius in pixels (default 3.0)
.with_label_top(n)Label the n most significant points (default 0)
.with_label_style(s)Nudge (default), Exact, or Arrow { offset_x, offset_y }
.with_pvalue_floor(f)Explicit p-value floor for −log₁₀ transform
.with_legend(s)Show an Up / Down / NS legend box

Manhattan Plot

A Manhattan plot displays GWAS p-values across the genome. Each point represents a SNP; the x-axis spans chromosomes and the y-axis shows −log₁₀(p-value). Chromosomes are colored with an alternating scheme. Dashed threshold lines are drawn automatically at the genome-wide (p = 5×10⁻⁸) and suggestive (p = 1×10⁻⁵) significance levels.

Import path: kuva::plot::ManhattanPlot


Basic usage — sequential mode

When base-pair positions are unavailable, pass (chrom, pvalue) pairs to .with_data(). Chromosomes are sorted in standard genomic order (1–22, X, Y, MT); points within each chromosome receive consecutive integer x positions.

#![allow(unused)]
fn main() {
use kuva::plot::ManhattanPlot;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;

let data: Vec<(String, f64)> = vec![
    ("1".into(), 0.42), ("1".into(), 3e-8),
    ("3".into(), 2e-9), ("6".into(), 5e-6),
    // ...
];

let mp = ManhattanPlot::new()
    .with_data(data)
    .with_legend("GWAS thresholds");

let plots = vec![Plot::Manhattan(mp)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("GWAS — Sequential x-coordinates")
    .with_y_label("−log₁₀(p-value)");

let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("manhattan.svg", svg).unwrap();
}
Manhattan plot — sequential mode, all chromosomes

All 23 chromosomes appear. Points in the significant and suggestive regions are visible above the dashed threshold lines. The legend shows both threshold entries.


Base-pair mode — GRCh38

.with_data_bp(data, GenomeBuild::Hg38) maps each (chrom, bp, pvalue) triplet onto a true genomic x-axis. All chromosomes in the build appear as labeled bands even if they contain no data points.

#![allow(unused)]
fn main() {
use kuva::plot::{ManhattanPlot, GenomeBuild};
use kuva::render::plots::Plot;

// (chrom, base-pair position, pvalue) from PLINK/GCTA output
let data: Vec<(String, f64, f64)> = vec![];  // ...

let mp = ManhattanPlot::new()
    .with_data_bp(data, GenomeBuild::Hg38)
    .with_label_top(10)
    .with_legend("GWAS thresholds");
}
Manhattan plot — GRCh38 base-pair coordinates with top-hit labels

The x-axis now reflects true chromosomal proportions. The 10 most significant hits above the genome-wide threshold are labeled. Chromosome names are accepted with or without the "chr" prefix.

Available builds:

VariantAssemblyChromosomes
GenomeBuild::Hg19GRCh37 / hg191–22, X, Y, MT
GenomeBuild::Hg38GRCh38 / hg381–22, X, Y, MT
GenomeBuild::T2TT2T-CHM13 v2.01–22, X, Y, MT
GenomeBuild::Custom(…)User-definedAny

Gene-name labels with with_point_labels

.with_point_labels(iter) attaches specific gene or SNP names to individual points by (chrom, x, label) triplets. Combine this with .with_data_x() (pre-computed x positions) for exact matching.

#![allow(unused)]
fn main() {
use kuva::plot::{ManhattanPlot, LabelStyle};
use kuva::render::plots::Plot;

let data: Vec<(&str, f64, f64)> = vec![
    // chr1 — BRCA2 locus
    ("1",  10.0, 0.42), ("1",  40.0, 2e-10), ("1",  60.0, 0.09),
    // chr2 — TP53 locus
    ("2", 120.0, 0.71), ("2", 140.0, 5e-9),  ("2", 160.0, 0.13),
    // chr3 — EGFR locus
    ("3", 220.0, 0.62), ("3", 250.0, 1e-9),  ("3", 270.0, 0.51),
];

let mp = ManhattanPlot::new()
    .with_data_x(data)
    .with_label_top(5)
    .with_label_style(LabelStyle::Arrow { offset_x: 10.0, offset_y: 14.0 })
    .with_point_labels(vec![
        ("1",  40.0, "BRCA2"),
        ("2", 140.0, "TP53"),
        ("3", 250.0, "EGFR"),
    ]);
}
Manhattan plot with gene-name labels (Arrow style)

Each significant peak is labeled with its gene name. The Arrow style draws a short leader line from the label to the point, keeping the label legible even when the peak is narrow.

The x value passed to with_point_labels must match the x coordinate assigned at data-load time. A tolerance of ±0.5 is used, so integer positions are always matched exactly.


Custom genome build

GenomeBuild::Custom accepts a Vec<(chrom_name, size_in_bp)> list in the order you want chromosomes displayed. Use this for non-human organisms or subsets of the human genome.

#![allow(unused)]
fn main() {
use kuva::plot::{ManhattanPlot, GenomeBuild};
use kuva::Palette;
use kuva::render::plots::Plot;

let build = GenomeBuild::Custom(vec![
    ("chr1".to_string(), 120_000_000),
    ("chr2".to_string(),  95_000_000),
    ("chr3".to_string(),  80_000_000),
    ("chrX".to_string(),  55_000_000),
]);

let mp = ManhattanPlot::new()
    .with_data_bp(data, build)
    .with_palette(Palette::tol_bright())
    .with_label_top(6)
    .with_legend("GWAS thresholds");
}
Manhattan plot — custom four-chromosome genome with tol_bright palette

Palette::tol_bright() cycles six colorblind-safe colors across the four chromosomes. Chromosome names are accepted with or without the "chr" prefix in both the build definition and the data.


Thresholds

MethodDefaultDescription
.with_genome_wide(f)7.301−log₁₀ threshold for the red dashed line (p = 5×10⁻⁸)
.with_suggestive(f)5.0−log₁₀ threshold for the gray dashed line (p = 1×10⁻⁵)

Pass −log₁₀(p) directly: for p = 1×10⁻⁶, use 6.0.


Colors

By default chromosomes alternate between two shades of blue:

MethodDefaultDescription
.with_color_a(s)"steelblue"Even-indexed chromosomes (0, 2, 4, …)
.with_color_b(s)"#5aadcb"Odd-indexed chromosomes (1, 3, 5, …)
.with_palette(p)Full palette; overrides the two-color scheme

All color methods accept any CSS color string. .with_palette() assigns colors in chromosome order, cycling with modulo wrapping.


API reference

MethodDescription
ManhattanPlot::new()Create a plot with defaults
.with_data(iter)Load (chrom, pvalue) pairs; sequential integer x
.with_data_bp(iter, build)Load (chrom, bp, pvalue) triplets; true genomic x
.with_data_x(iter)Load (chrom, x, pvalue) triplets; pre-computed x
.with_genome_wide(f)Genome-wide threshold in −log₁₀ (default 7.301)
.with_suggestive(f)Suggestive threshold in −log₁₀ (default 5.0)
.with_color_a(s)Even-chromosome color (default "steelblue")
.with_color_b(s)Odd-chromosome color (default "#5aadcb")
.with_palette(p)Full palette, overrides alternating colors
.with_point_size(f)Circle radius in pixels (default 2.5)
.with_label_top(n)Label the n top hits above genome-wide threshold
.with_label_style(s)Nudge (default), Exact, or Arrow { offset_x, offset_y }
.with_point_labels(iter)Attach gene/SNP names to specific (chrom, x) positions
.with_pvalue_floor(f)Explicit p-value floor for −log₁₀ transform
.with_legend(s)Show genome-wide and suggestive legend entries

kuva CLI

kuva is the command-line front-end for the kuva plotting library. It reads tabular data from a TSV or CSV file (or stdin) and writes an SVG — or PNG/PDF with the right feature flag — to a file or stdout.

kuva <SUBCOMMAND> [FILE] [OPTIONS]

Building

cargo build --bin kuva                  # SVG output only
cargo build --bin kuva --features png   # adds PNG output via resvg
cargo build --bin kuva --features pdf   # adds PDF output via svg2pdf
cargo build --bin kuva --features full  # both PNG and PDF

After building, the binary is at target/debug/kuva (or target/release/kuva with --release).


Input

Every subcommand takes an optional positional FILE argument. If omitted or -, data is read from stdin.

# from file
kuva scatter data.tsv

# from stdin
cat data.tsv | kuva scatter

# explicit stdin
kuva scatter - < data.tsv

Delimiter detection

PriorityRule
1--delimiter flag
2File extension: .csv,, .tsv/.txt → tab
3Sniff first line: whichever of tab or comma appears more often

Header detection

If the first field of the first row fails to parse as a number, the row is treated as a header. Override with --no-header.

Column selection

Columns are selected by 0-based integer index or header name:

kuva scatter data.tsv --x 0 --y 1          # by index
kuva scatter data.tsv --x time --y value   # by name (requires header)

Output

FlagEffect
(omitted)SVG to stdout
-o out.svgSVG to file
-o out.pngPNG (requires --features png)
-o out.pdfPDF (requires --features pdf)

Format is inferred from the file extension. Any unrecognised extension is treated as SVG.


Shared flags

These flags are available on every subcommand.

Output & appearance

FlagDefaultDescription
-o, --output <FILE>stdout (SVG)Output file path (mutually exclusive with --terminal)
--title <TEXT>Title displayed above the chart
--width <PX>800Canvas width in pixels
--height <PX>500Canvas height in pixels
--theme <NAME>lightTheme: light, dark, solarized, minimal
--palette <NAME>category10Color palette for multi-series plots
--cvd-palette <NAME>Colour-vision-deficiency palette: deuteranopia, protanopia, tritanopia. Overrides --palette.
--background <COLOR>(theme default)SVG background color (any CSS color string)

Terminal output

FlagDefaultDescription
--terminaloffRender directly in the terminal using Unicode braille and block characters; mutually exclusive with -o
--term-width <N>(auto)Terminal width in columns (overrides auto-detect)
--term-height <N>(auto)Terminal height in rows (overrides auto-detect)

Terminal output uses Unicode braille dots (U+2800–U+28FF) for scatter points and continuous curves, full-block characters () for bar and histogram fills, and ANSI 24-bit colour. Terminal dimensions are auto-detected from the current tty; pass --term-width and --term-height to override (useful in scripts or when piping).

# Scatter plot directly in terminal
kuva scatter data.tsv --x x --y y --terminal

# Explicit dimensions
kuva bar counts.tsv --label-col gene --value-col count --terminal --term-width 120 --term-height 40

# Manhattan plot on a remote server
cat gwas.tsv | kuva manhattan --chr-col chr --pvalue-col pvalue --terminal

Note: Terminal output is not yet supported for upset. Running kuva upset --terminal prints a message and exits cleanly; use -o file.svg instead.

Axes (most subcommands)

FlagDefaultDescription
--x-label <TEXT>X-axis label
--y-label <TEXT>Y-axis label
--ticks <N>5Hint for number of tick marks
--no-gridoffDisable background grid

Log scale (scatter, line, histogram, hist2d)

FlagDescription
--log-xLogarithmic X axis
--log-yLogarithmic Y axis

Input

FlagDescription
--no-headerTreat first row as data, not a header
-d, --delimiter <CHAR>Override field delimiter

Subcommands


scatter

Scatter plot of (x, y) point pairs. Supports multi-series coloring, trend lines, and log scale.

Input: any tabular file with two numeric columns.

FlagDefaultDescription
--x <COL>0X-axis column
--y <COL>1Y-axis column
--color-by <COL>Group by this column; each group gets a distinct color
--color <CSS>steelbluePoint color (single-series only)
--size <PX>3.0Point radius in pixels
--trendoffOverlay a linear trend line
--equationoffAnnotate with regression equation (requires --trend)
--correlationoffAnnotate with Pearson R² (requires --trend)
--legendoffShow legend
kuva scatter measurements.tsv --x time --y value --color steelblue

kuva scatter measurements.tsv --x time --y value \
    --color-by group --legend --title "Expression over time"

kuva scatter measurements.tsv --x time --y value \
    --trend --equation --correlation --log-y

line

Line plot. Identical column flags to scatter; adds line-style options.

Input: any tabular file with two numeric columns, sorted by x.

FlagDefaultDescription
--x <COL>0X-axis column
--y <COL>1Y-axis column
--color-by <COL>Multi-series grouping
--color <CSS>steelblueLine color (single-series)
--stroke-width <PX>2.0Line stroke width
--dashedoffDashed line style
--dottedoffDotted line style
--filloffFill area under the line
--legendoffShow legend
kuva line measurements.tsv --x time --y value --color-by group --legend

kuva line measurements.tsv --x time --y value --fill --color "rgba(70,130,180,0.4)"

bar

Bar chart from label/value pairs.

Input: first column labels, second column numeric values.

FlagDefaultDescription
--label-col <COL>0Label column
--value-col <COL>1Value column
--color <CSS>steelblueBar fill color
--bar-width <F>0.8Bar width as a fraction of the slot
kuva bar bar.tsv --label-col category --value-col count --color "#4682b4"

kuva bar bar.tsv --x-label "Pathway" --y-label "Gene count" \
    -o pathways.svg

histogram

Frequency histogram from a single numeric column.

Input: one numeric column per row.

FlagDefaultDescription
--value-col <COL>0Value column
--color <CSS>steelblueBar fill color
--bins <N>10Number of bins
--normalizeoffNormalize to probability density (area = 1)
kuva histogram histogram.tsv --value-col value --bins 30

kuva histogram histogram.tsv --bins 20 --normalize \
    --title "Expression distribution" --y-label "Density"

box

Box-and-whisker plot. Groups are taken from one column; values from another.

Input: two columns — group label and numeric value, one observation per row.

FlagDefaultDescription
--group-col <COL>0Group label column
--value-col <COL>1Numeric value column
--color <CSS>steelblueBox fill color
--overlay-pointsoffOverlay individual points as a jittered strip
--overlay-swarmoffOverlay individual points as a non-overlapping beeswarm
kuva box samples.tsv --group-col group --value-col expression

kuva box samples.tsv --group-col group --value-col expression \
    --overlay-swarm --color "rgba(70,130,180,0.6)"

violin

Kernel-density violin plot. Same input format as box.

Input: two columns — group label and numeric value, one observation per row.

FlagDefaultDescription
--group-col <COL>0Group label column
--value-col <COL>1Numeric value column
--color <CSS>steelblueViolin fill color
--bandwidth <F>(Silverman)KDE bandwidth
--overlay-pointsoffOverlay individual points as a jittered strip
--overlay-swarmoffOverlay individual points as a non-overlapping beeswarm
kuva violin samples.tsv --group-col group --value-col expression

kuva violin samples.tsv --group-col group --value-col expression \
    --overlay-swarm --bandwidth 0.3

pie

Pie or donut chart.

Input: label column + numeric value column.

FlagDefaultDescription
--label-col <COL>0Label column
--value-col <COL>1Value column
--color-col <COL>Optional CSS color column
--donutoffRender as a donut (hollow center)
--inner-radius <PX>80Donut hole radius in pixels
--percentoffAppend percentage to slice labels
--label-position <MODE>(auto)inside, outside, or none
--legendoffShow legend
kuva pie pie.tsv --label-col feature --value-col percentage --percent

kuva pie pie.tsv --label-col feature --value-col percentage \
    --donut --legend --label-position outside

strip

Strip / jitter plot — individual points along a categorical axis.

Input: group label column + numeric value column, one observation per row.

FlagDefaultDescription
--group-col <COL>0Group label column
--value-col <COL>1Numeric value column
--color <CSS>steelbluePoint color
--point-size <PX>4.0Point radius in pixels
--swarmoffBeeswarm (non-overlapping) layout
--centeroffAll points at group center (no spread)

Default layout when neither --swarm nor --center is given: random jitter (±30 % of slot width).

kuva strip samples.tsv --group-col group --value-col expression

kuva strip samples.tsv --group-col group --value-col expression --swarm

waterfall

Waterfall / bridge chart showing a running total built from incremental bars.

Input: label column + numeric value column. Mark subtotal/total bars with --total.

FlagDefaultDescription
--label-col <COL>0Label column
--value-col <COL>1Value column
--total <LABEL>Mark this label as a summary bar (repeatable)
--connectorsoffDraw dashed connector lines between bars
--valuesoffPrint numeric values on each bar
--color-pos <CSS>greenPositive delta bar color
--color-neg <CSS>redNegative delta bar color
--color-total <CSS>steelblueTotal/subtotal bar color
kuva waterfall waterfall.tsv --label-col process --value-col log2fc \
    --connectors --values

# mark two rows as summary bars
kuva waterfall income.tsv \
    --total "Gross profit" --total "Net income" \
    --connectors --values

stacked-area

Stacked area chart in long format.

Input: three columns — x value, group label, y value — one observation per row. Rows are grouped by the group column; within each group the x/y pairs are collected in order.

FlagDefaultDescription
--x-col <COL>0X-axis column
--group-col <COL>1Series group column
--y-col <COL>2Y-axis column
--normalizeoffNormalize each x-position to 100 %
--fill-opacity <F>0.7Fill opacity for each band
kuva stacked-area stacked_area.tsv \
    --x-col week --group-col species --y-col abundance

kuva stacked-area stacked_area.tsv \
    --x-col week --group-col species --y-col abundance \
    --normalize --y-label "Relative abundance (%)"

volcano

Volcano plot for differential expression results.

Input: three columns — gene name, log₂ fold change, raw p-value.

FlagDefaultDescription
--name-col <COL>0Gene/feature name column
--x-col <COL>1log₂FC column
--y-col <COL>2p-value column (raw, not −log₁₀)
--fc-cutoff <F>1.0|log₂FC| threshold
--p-cutoff <F>0.05p-value significance threshold
--top-n <N>0Label the N most-significant points
--color-up <CSS>firebrickUp-regulated point color
--color-down <CSS>steelblueDown-regulated point color
--color-ns <CSS>#aaaaaaNot-significant point color
--point-size <PX>3.0Point radius
--legendoffShow Up / Down / NS legend
kuva volcano gene_stats.tsv \
    --name-col gene --x-col log2fc --y-col pvalue \
    --top-n 20 --legend

kuva volcano gene_stats.tsv \
    --name-col gene --x-col log2fc --y-col pvalue \
    --fc-cutoff 2.0 --p-cutoff 0.01 --top-n 10

manhattan

Manhattan plot for GWAS results.

Input: chromosome, (optional) base-pair position, and p-value columns.

Two layout modes:

  • Sequential (default): chromosomes are sorted and SNPs receive consecutive integer x-positions. Position column is not used.
  • Base-pair (--genome-build): SNP x-coordinates are resolved from chromosome sizes in a reference build.
FlagDefaultDescription
--chr-col <COL>0Chromosome column
--pos-col <COL>1Base-pair position column (bp mode only)
--pvalue-col <COL>2p-value column
--genome-build <BUILD>Enable bp mode: hg19, hg38, or t2t
--genome-wide <F>7.301Genome-wide threshold (−log₁₀ scale)
--suggestive <F>5.0Suggestive threshold (−log₁₀ scale)
--top-n <N>0Label N most-significant points above genome-wide threshold
--point-size <PX>2.5Point radius
--color-a <CSS>steelblueEven-chromosome color
--color-b <CSS>#5aadcbOdd-chromosome color
--legendoffShow threshold legend
# sequential mode (no position column needed)
kuva manhattan gene_stats.tsv --chr-col chr --pvalue-col pvalue --top-n 5

# base-pair mode
kuva manhattan gwas.tsv \
    --chr-col chr --pos-col pos --pvalue-col pvalue \
    --genome-build hg38 --top-n 10 --legend

candlestick

OHLC candlestick chart.

Input: label, open, high, low, close columns (and optionally volume).

FlagDefaultDescription
--label-col <COL>0Period label column
--open-col <COL>1Open price column
--high-col <COL>2High price column
--low-col <COL>3Low price column
--close-col <COL>4Close price column
--volume-col <COL>Optional volume column
--volume-paneloffShow volume bar panel below price chart
--candle-width <F>0.7Body width as a fraction of slot
--color-up <CSS>greenBullish candle color
--color-down <CSS>redBearish candle color
--color-doji <CSS>#888888Doji candle color
kuva candlestick candlestick.tsv \
    --label-col date --open-col open --high-col high \
    --low-col low --close-col close

kuva candlestick candlestick.tsv \
    --label-col date --open-col open --high-col high \
    --low-col low --close-col close \
    --volume-col volume --volume-panel

heatmap

Color-encoded matrix heatmap.

Input: wide-format matrix — first column is the row label, remaining columns are numeric values. The header row (if present) supplies column labels.

gene    Sample_01  Sample_02  Sample_03 …
TP53    0.25       -1.78       1.58     …
BRCA1   0.23        0.48       1.06     …
FlagDefaultDescription
--colormap <NAME>viridisColor map: viridis, inferno, grayscale
--valuesoffPrint numeric values in each cell
--legend <LABEL>Show color bar with this label
kuva heatmap heatmap.tsv

kuva heatmap heatmap.tsv --colormap inferno --values --legend "z-score"

hist2d

Two-dimensional histogram (density grid) from two numeric columns.

Input: two numeric columns.

FlagDefaultDescription
--x <COL>0X-axis column
--y <COL>1Y-axis column
--bins-x <N>10Number of bins on the X axis
--bins-y <N>10Number of bins on the Y axis
--colormap <NAME>viridisColor map: viridis, inferno, grayscale
--correlationoffOverlay Pearson correlation coefficient
kuva hist2d measurements.tsv --x time --y value

kuva hist2d measurements.tsv --x time --y value \
    --bins-x 20 --bins-y 20 --correlation

contour

Contour plot from scattered (x, y, z) triplets.

Input: three columns — x coordinate, y coordinate, scalar value.

FlagDefaultDescription
--x <COL>0X column
--y <COL>1Y column
--z <COL>2Scalar value column
--levels <N>8Number of contour levels
--filledoffFill between contour levels
--colormap <NAME>viridisColor map (filled mode)
--line-color <CSS>Line color (unfilled mode)
--legend <LABEL>Show legend entry
kuva contour contour.tsv --x x --y y --z density

kuva contour contour.tsv --x x --y y --z density \
    --filled --levels 12 --colormap inferno

dot

Dot plot encoding two variables (size and color) at categorical (x, y) positions.

Input: four columns — x category, y category, size value, color value.

FlagDefaultDescription
--x-col <COL>0X-category column
--y-col <COL>1Y-category column
--size-col <COL>2Size-encoding column
--color-col <COL>3Color-encoding column
--colormap <NAME>viridisColor map
--max-radius <PX>12.0Maximum dot radius
--size-legend <LABEL>Show size legend with this label
--colorbar <LABEL>Show color bar with this label
kuva dot dot.tsv \
    --x-col pathway --y-col cell_type \
    --size-col pct_expressed --color-col mean_expr

kuva dot dot.tsv \
    --x-col pathway --y-col cell_type \
    --size-col pct_expressed --color-col mean_expr \
    --size-legend "% expressed" --colorbar "mean expr"

upset

UpSet plot for set-intersection analysis.

Input: binary (0/1) matrix — one column per set, one row per element. Column headers become set names.

GWAS_hit  eQTL  Splicing_QTL  Methylation_QTL  Conservation  ClinVar
1         0     0             1                1             1
0         0     1             1                1             0
FlagDefaultDescription
--sort <MODE>frequencySort intersections: frequency, degree, natural
--max-visible <N>Show only the top N intersections
kuva upset upset.tsv

kuva upset upset.tsv --sort degree --max-visible 15

Terminal output: not yet supported. kuva upset --terminal prints a message and exits cleanly; use -o file.svg instead.


chord

Chord diagram for pairwise flow data.

Input: square N×N matrix — first column is the row label (ignored for layout), header row supplies node names.

region        Cortex  Hippocampus  Amygdala …
Cortex        0       320          13       …
Hippocampus   320     0            210      …
FlagDefaultDescription
--gap <DEG>2.0Gap between arcs in degrees
--opacity <F>0.7Ribbon opacity
--legend <LABEL>Show legend
kuva chord chord.tsv

kuva chord chord.tsv --gap 3.0 --opacity 0.5 --legend "connectivity"

sankey

Sankey / alluvial flow diagram.

Input: three columns — source node, target node, flow value.

FlagDefaultDescription
--source-col <COL>0Source node column
--target-col <COL>1Target node column
--value-col <COL>2Flow value column
--link-gradientoffFill each link with a gradient from source node colour to target node colour
--opacity <F>0.5Link opacity
--legend <LABEL>Show legend
kuva sankey sankey.tsv \
    --source-col source --target-col target --value-col value

kuva sankey sankey.tsv \
    --source-col source --target-col target --value-col value \
    --link-gradient --legend "read flow"

phylo

Phylogenetic tree from a Newick string or edge-list TSV.

Input (default): edge-list TSV with parent, child, and branch-length columns.

Input (alternative): pass --newick with a Newick string; the file argument is not used.

FlagDefaultDescription
--newick <STR>Newick string (overrides file input)
--parent-col <COL>0Parent node column
--child-col <COL>1Child node column
--length-col <COL>2Branch length column
--orientation <DIR>leftleft, right, top, bottom
--branch-style <STYLE>rectangularrectangular, slanted, circular
--phylogramoffScale branches by length
--legend <LABEL>Show legend
# from edge-list TSV
kuva phylo phylo.tsv \
    --parent-col parent --child-col child --length-col length

# from Newick string
kuva phylo --newick "((A:0.1,B:0.2):0.3,C:0.4);" --branch-style circular

# phylogram, top orientation
kuva phylo phylo.tsv \
    --parent-col parent --child-col child --length-col length \
    --phylogram --orientation top

synteny

Synteny / genomic alignment ribbon plot.

Input: two files:

  • Sequences file (positional): TSV with sequence name and length columns.
  • Blocks file (--blocks-file): TSV with columns seq1, start1, end1, seq2, start2, end2, strand.
# sequences.tsv
name    length
Chr1A   2800000
Chr1B   2650000

# blocks.tsv
seq1   start1  end1    seq2   start2  end2    strand
Chr1A  56000   137237  Chr1B  63958   143705  +
Chr1A  150674  271188  Chr1B  165366  303075  -
FlagDefaultDescription
--blocks-file <FILE>(required)Blocks TSV file
--bar-height <PX>18.0Sequence bar height in pixels
--opacity <F>0.65Block ribbon opacity
--proportionaloffScale bar widths proportionally to sequence length
--legend <LABEL>Show legend
kuva synteny synteny_seqs.tsv --blocks-file synteny_blocks.tsv

kuva synteny synteny_seqs.tsv --blocks-file synteny_blocks.tsv \
    --proportional --legend "synteny blocks"

Tips

Pipe to a viewer:

kuva scatter data.tsv | display            # ImageMagick
kuva scatter data.tsv | inkscape --pipe    # Inkscape

Quick PNG without a file:

kuva scatter data.tsv -o /tmp/out.png      # requires --features png

Themed dark output:

kuva manhattan gwas.tsv --chr-col chr --pvalue-col pvalue \
    --theme dark --background "#1a1a2e" -o manhattan_dark.svg

Colour-vision-deficiency palette:

kuva scatter data.tsv --x time --y value --color-by group \
    --cvd-palette deuteranopia

Terminal Output

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.

#![allow(unused)]
fn main() {
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.

#![allow(unused)]
fn main() {
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

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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():

#![allow(unused)]
fn main() {
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.

VariantExample outputUse case
Auto (default)5, 3.14, 1.2e5General purpose — integers without .0, minimal decimals, sci notation for extremes
Fixed(n)3.14 (n=2)Fixed decimal places
Integer5Round to nearest integer
Sci1.23e4Always scientific notation
Percent45.0%Multiply by 100 and append % — for data in the range 0–1
Custom(fn)anythingProvide your own fn(f64) -> String

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

#![allow(unused)]
fn main() {
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))
    ));
}
Auto tick format Fixed(2) tick format
Auto Fixed(2)
Percent tick format Sci tick format
Percent Sci

Tick rotation

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

#![allow(unused)]
fn main() {
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.

#![allow(unused)]
fn main() {
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();
}
Log-scale axes

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:

#![allow(unused)]
fn main() {
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.

#![allow(unused)]
fn main() {
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
    );
}

Reference line

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

#![allow(unused)]
fn main() {
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"
    );
}

Shaded region

Fills a horizontal or vertical band across the plot area.

#![allow(unused)]
fn main() {
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),
    );
}
Text annotation, reference lines, and shaded region

Typography

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

#![allow(unused)]
fn main() {
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: system sans-serif
    .with_title_size(20)                    // default: 16
    .with_label_size(14)                    // default: 14  (axis labels)
    .with_tick_size(11)                     // default: 10  (tick labels)
    .with_body_size(12);                    // default: 12  (legend, annotations)
}

These can also be set via a Theme — see the Themes reference.


Quick reference

Layout constructors

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

Axes and labels

MethodDescription
.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

MethodDescription
.with_width(px)Total SVG width in pixels
.with_height(px)Total SVG height in pixels

Annotations

MethodDescription
.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

Typography

MethodDefaultDescription
.with_font_family(s)system sansCSS font-family string
.with_title_size(n)16Title font size (px)
.with_label_size(n)14Axis label font size (px)
.with_tick_size(n)10Tick label font size (px)
.with_body_size(n)12Body text font size (px)

TickFormat variants

VariantOutput example
Auto5, 3.14, 1.2e5
Fixed(n)3.14
Integer5
Sci1.23e4
Percent45.0%
Custom(Arc<dyn Fn(f64) -> String>)user-defined

Themes

Themes control the colours of all plot chrome — background, axes, grid lines, tick marks, text, and legend. Plot data colours are not affected by themes; those come from the color passed to each plot or from a Palette.

Four built-in themes are available. The default is light.


Applying a theme

Rust API

#![allow(unused)]
fn main() {
use kuva::render::layout::Layout;
use kuva::render::theme::Theme;

let layout = Layout::auto_from_plots(&plots)
    .with_theme(Theme::dark());
}

CLI

kuva scatter data.tsv --x x --y y --theme dark
kuva bar data.tsv --label-col gene --value-col count --theme minimal

Available CLI values: light, dark, minimal, solarized.


Built-in themes

light (default)

White background, black axes and text, light gray grid lines.

PropertyValue
Backgroundwhite
Axes / ticks / textblack
Grid#ccc
Legend backgroundwhite
Legend borderblack
Fontsystem sans-serif
Grid shownyes

dark

Dark charcoal background, light gray text and axes.

PropertyValue
Background#1e1e1e
Axes / ticks#cccccc
Text#e0e0e0
Grid#444444
Legend background#2d2d2d
Legend border#666666
Fontsystem sans-serif
Grid shownyes

minimal

White background, no grid, serif font, no legend border. Suited for publication figures where grid lines add visual noise.

PropertyValue
Backgroundwhite
Axes / ticks / textblack
Grid#e0e0e0
Legend bordernone
Fontserif
Grid shownno

solarized

Warm cream background based on Ethan Schoonover's Solarized palette.

PropertyValue
Background#fdf6e3
Axes / ticks#586e75
Text#657b83
Grid#eee8d5
Legend background#fdf6e3
Legend border#93a1a1
Fontsystem sans-serif
Grid shownyes

Custom themes

Build a Theme struct directly to set any combination of properties:

#![allow(unused)]
fn main() {
use kuva::render::theme::Theme;

let theme = Theme {
    background: "#0d1117".into(),   // GitHub dark background
    axis_color: "#8b949e".into(),
    grid_color: "#21262d".into(),
    tick_color: "#8b949e".into(),
    text_color: "#c9d1d9".into(),
    legend_bg: "#161b22".into(),
    legend_border: "#30363d".into(),
    pie_leader: "#8b949e".into(),
    box_median: "#0d1117".into(),
    violin_border: "#8b949e".into(),
    colorbar_border: "#8b949e".into(),
    font_family: None,
    show_grid: true,
};

let layout = Layout::auto_from_plots(&plots).with_theme(theme);
}

Color Palettes

A Palette is a named, ordered list of colors that auto-cycles across plots. Palettes are used to assign consistent, visually distinct colors to multiple series without specifying each one manually.


Using a palette

Auto-cycle across plots

Pass a palette to Layout::with_palette() and kuva assigns colors in order to each plot that does not already have an explicit color set:

#![allow(unused)]
fn main() {
use kuva::render::layout::Layout;
use kuva::render::palette::Palette;

let layout = Layout::auto_from_plots(&plots)
    .with_palette(Palette::wong());
}

Manual indexing

Index directly into a palette with []. Indexing wraps with modulo, so pal[n] is always valid regardless of palette size:

#![allow(unused)]
fn main() {
let pal = Palette::tol_bright();
let color_a = &pal[0];  // "#4477AA"
let color_b = &pal[1];  // "#EE6677"
let color_c = &pal[7];  // wraps: same as pal[0]
}

CLI

kuva scatter data.tsv --x x --y y --palette wong
kuva line data.tsv --x-col time --y-col value --color-by group --palette tol_muted

Available CLI values: wong, okabe_ito, tol_bright, tol_muted, tol_light, ibm, category10, pastel, bold.

For convenience, --cvd-palette TYPE selects a colorblind-safe palette by condition name: deuteranopia, protanopia, tritanopia.


Built-in palettes

Colorblind-safe

ConstructorNColors
Palette::wong()8#E69F00 #56B4E9 #009E73 #F0E442 #0072B2 #D55E00 #CC79A7 #000000
Palette::okabe_ito()8Same as Wong — widely known by both names
Palette::tol_bright()7#4477AA #EE6677 #228833 #CCBB44 #66CCEE #AA3377 #BBBBBB
Palette::tol_muted()10#CC6677 #332288 #DDCC77 #117733 #88CCEE #882255 #44AA99 #999933 #AA4499 #DDDDDD
Palette::tol_light()9#77AADD #EE8866 #EEDD88 #FFAABB #99DDFF #44BB99 #BBCC33 #AAAA00 #DDDDDD
Palette::ibm()5#648FFF #785EF0 #DC267F #FE6100 #FFB000

Recommendations:

  • wong / okabe_ito — best general choice; safe for deuteranopia and protanopia (~7% of males)
  • tol_bright — safe for tritanopia; good for presentations
  • tol_muted — 10 colors for larger datasets; safe for all common CVD types
  • ibm — compact 5-color set from the IBM Design Language

Colorblind condition aliases

These are convenience constructors that return an appropriate palette for a specific condition:

ConstructorReturnsSafe for
Palette::deuteranopia()WongRed-green (~6% of males)
Palette::protanopia()WongRed-green (~1% of males)
Palette::tritanopia()Tol BrightBlue-yellow (rare)

General-purpose

ConstructorNColors
Palette::category10()10#1f77b4 #ff7f0e #2ca02c #d62728 #9467bd #8c564b #e377c2 #7f7f7f #bcbd22 #17becf
Palette::pastel()10#aec7e8 #ffbb78 #98df8a #ff9896 #c5b0d5 #c49c94 #f7b6d2 #c7c7c7 #dbdb8d #9edae5
Palette::bold()10#e41a1c #377eb8 #4daf4a #984ea3 #ff7f00 #a65628 #f781bf #999999 #66c2a5 #fc8d62

category10 is the default when no palette is set.


Custom palettes

#![allow(unused)]
fn main() {
use kuva::render::palette::Palette;

let pal = Palette::custom(
    "my_palette",
    vec!["#264653".into(), "#2a9d8f".into(), "#e9c46a".into(),
         "#f4a261".into(), "#e76f51".into()],
);

let layout = Layout::auto_from_plots(&plots).with_palette(pal);
}

API reference

MethodDescription
Palette::wong()Bang Wong 8-color colorblind-safe palette
Palette::okabe_ito()Alias for Wong
Palette::tol_bright()Paul Tol qualitative bright, 7 colors
Palette::tol_muted()Paul Tol qualitative muted, 10 colors
Palette::tol_light()Paul Tol qualitative light, 9 colors
Palette::ibm()IBM Design Language, 5 colors
Palette::deuteranopia()Alias for Wong
Palette::protanopia()Alias for Wong
Palette::tritanopia()Alias for Tol Bright
Palette::category10()Tableau/D3 Category10, 10 colors (default)
Palette::pastel()Pastel variant of Category10, 10 colors
Palette::bold()High-saturation vivid, 10 colors
Palette::custom(name, colors)User-defined palette
pal[i]Color at index i; wraps with modulo
pal.len()Number of colors
pal.colors()Slice of all color strings
pal.iter()Cycling iterator (never returns None)

Benchmarks

kuva uses Criterion for statistical micro-benchmarks. All numbers on this page were collected on a release build (opt-level = 3) on AMD64 Linux. Timing is median wall-clock; HTML reports with per-sample distributions live in target/criterion/ after running.

Running the benchmarks

# All benchmark groups (requires the png + pdf backends to compile cleanly)
cargo bench --features full

# A single group
cargo bench --features full -- render

# HTML report (opens in browser)
open target/criterion/report/index.html

Criterion runs a 3-second warm-up then collects 100 samples per benchmark. The measurement is median time; outlier detection flags any samples more than 2 IQRs from the median.

Benchmark files

filewhat it measures
benches/render.rsFull pipeline (scene build + SVG emit) and SVG-only for scatter, scatter+errorbars, line, violin, manhattan, heatmap
benches/kde.rssimple_kde in isolation at 100 → 100k samples
benches/svg.rsSvgBackend::render_scene alone, N Circle primitives, no rendering pipeline

Results

kde — Gaussian KDE (simple_kde, 256 evaluation points)

The truncated kernel sorts the input once then binary-searches for the window [x ± 4·bw] around each evaluation point. Only values inside that window contribute (Gaussian weight beyond 4σ is < 0.003%).

n samplestime
10085.7 µs
1,000621 µs
10,0004.07 ms
100,00028.0 ms

Scaling: ~6.8× per decade of n. For uniformly distributed data the window captures ~10% of points, giving roughly O(n^0.8 × samples) total work instead of the naive O(n × samples). On bounded data (e.g. sin-shaped violin groups) the window covers ~30% of points, yielding a ~3× improvement over naive.

render — scatter and line

n=100n=1kn=10kn=100kn=1M
scatter scene+svg41 µs314 µs3.10 ms34.5 ms414 ms
scatter svg-only34 µs294 µs3.07 ms31.2 ms317 ms
scatter scene build~7 µs~20 µs~30 µs~3 ms~97 ms
line scene+svg39 µs262 µs2.49 ms28.6 ms308 ms

At 1M points the SVG emit accounts for 317 ms (77% of total). The scene build — coordinate mapping, Vec allocation — costs ~97 ms. SVG generation rate: ~200 ns/element.

render — scatter with error bars

Each error bar adds 3 Line primitives (cap–shaft–cap), so n=100k scatter+errorbars emits 400k primitives vs 100k for plain scatter.

nplain scatterwith y_erroverhead
10041 µs175 µs4.3×
1,000314 µs1.70 ms5.4×
10,0003.10 ms18.8 ms6.1×
100,00034.5 ms222 ms6.5×

render — violin (3 groups)

Violin SVG is cheap — three KDE curves emit ~10 path primitives regardless of n. All time is in KDE.

n per groupscene+svgsvg-onlyKDE cost
100557 µs20 µs~537 µs
1,0002.00 ms19 µs~1.98 ms
10,00012.7 ms25 µs~12.7 ms
100,00089.0 ms34 µs~89 ms

The violin SVG time is flat at ~25 µs across all scales. KDE matches 3× the kde bench values exactly. Practical advice: violin plots are fast at the scales bioinformatics data actually produces (100–5000 points per group); 100k/group is an extreme stress test.

render — manhattan (22 chromosomes)

Pre-bucketing builds a HashMap<chr → Vec<idx>> once before the span loop, reducing chromosome lookups from O(22 × n) to O(n).

n SNPstime
1,000372 µs
10,0003.88 ms
100,00042.0 ms
1,000,000501 ms

Scales linearly: each 10× increase in n costs ~10–12× more time. At 1M SNPs (a full GWAS): 501 ms including scene build and SVG emit. Note: this is render-only; file I/O for a 1M-row TSV adds read time on top.

render — heatmap (n×n matrix)

ncellsno valueswith valuestext overhead
1010061.7 µs114 µs1.85×
502,5001.18 ms2.54 ms2.15×
10010,0004.86 ms10.4 ms2.14×
20040,00024.6 ms
500250,000154 ms

Scales with n² (doubling n quadruples cells). The single-loop merge means show_values costs exactly ~2× no-values — one extra format! and Text primitive per cell.

svg — raw string generation

Measures SvgBackend::render_scene on a scene containing only Circle primitives. Isolates the string formatting cost with no rendering pipeline.

n circlestimens/element
1,000198 µs198
10,0001.99 ms199
100,00019.9 ms199
1,000,000213 ms213

Perfectly linear. String::with_capacity pre-allocation eliminates reallocation variance. ~200 ns/element is the baseline cost of format! through the std::fmt machinery.

Interpretation

SVG string generation dominates at scale. For scatter/line at 1M points, 77% of total time is in SvgBackend::render_scene. Improving the SVG backend (e.g. write-to-file streaming, avoiding format! for simple floats) would have the most impact at extreme scales.

Violin is all KDE. The SVG stage for a violin scene is ~25 µs at any n — it's a handful of path curves. For n > 10k/group, consider whether that resolution is actually needed; downsampling to 10k rarely changes the visible shape.

Manhattan is now O(n). Pre-bucketing reduced chromosome filtering from O(22n) to O(n). A 1M-SNP GWAS renders in ~500 ms end-to-end (render only, excluding file I/O).

Error bars are expensive at scale. 3 extra primitives per point means 4–6× the cost of plain scatter. For large n with error bars, consider downsampling or rendering only bars for significant points.

Heatmap with values is 2×. The single-pass loop keeps the overhead exactly proportional — no wasted traversal.

Optimisations applied

These were identified by profiling before benchmarks existed and confirmed by the benchmark numbers above:

changefileexpected gain
Truncated KDE kernel (sort + binary search)render_utils.rs~8× at 100k uniform data
Manhattan pre-bucketing (HashMap)render.rs~22× at 1M SNPs with 22 chr
Heatmap single-loop + no flat Vecrender.rs~2× for show_values; -1 alloc
String::with_capacity in SVG backendsvg.rseliminates O(log n) reallocs