Skip to main content
A dot plot places circles at the intersections of two categorical axes. Each circle encodes two independent continuous variables simultaneously: size (radius) and color. This makes it ideal for compact display of multi-variable summaries across a grid — the canonical bioinformatics use case is gene expression across cell types.

When to Use

  • Single-cell gene expression: Show % expressing (size) and mean expression (color) per cell type
  • Multi-condition experiments: Display two measurements (e.g. effect size and significance) across conditions
  • Pathway enrichment: Show gene count (size) and FDR (color) for pathways across datasets
  • Pharmacology: Display drug response (size) and toxicity (color) across cell lines
  • Any categorical grid: When two continuous variables need simultaneous encoding

Basic Example

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![
    ("CD4 T", "CD3E", 88.0_f64, 3.8_f64),
    ("CD8 T", "CD3E", 91.0,     4.0    ),
    ("CD4 T", "CD4",  85.0,     3.5    ),
    ("CD8 T", "CD4",   8.0,     0.3    ),
];

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 svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("dotplot.svg", svg).unwrap();

Input Modes

Sparse Tuples: with_data

Pass an iterator of (x_cat, y_cat, size, color) tuples. Missing grid positions have no circle drawn.
let dot = DotPlot::new().with_data(vec![
    ("CD4 T", "CD3E", 88.0_f64, 3.8_f64),
    ("CD8 T", "CD3E", 91.0,     4.0    ),
    // ("NK", "CD3E") absent — no circle at that position
]);
Category order follows first-seen insertion order. Natural for data as list of records.

Dense Matrix: with_matrix

Pass explicit category lists and sizes[row][col] / colors[row][col] matrices. Every grid cell is filled.
let dot = DotPlot::new().with_matrix(
    vec!["TypeA", "TypeB"],          // x categories
    vec!["Gene1", "Gene2"],          // y categories
    vec![vec![80.0, 25.0],           // sizes[row_i][col_j]
         vec![15.0, 90.0]],
    vec![vec![3.5,  1.2],            // colors[row_i][col_j]
         vec![0.8,  4.1]],
);
Use when data comes from a matrix (e.g. differential expression tool output).

Key Methods

Data Input

with_data(iter)

Add data as sparse (x_cat, y_cat, size, color) tuples.
let dot = DotPlot::new().with_data(vec![
    ("CD4 T", "CD3E", 88.0_f64, 3.8_f64),
    ("CD8 T", "CD3E", 91.0,     4.0    ),
]);

with_matrix(x_cats, y_cats, sizes, colors)

Add data as explicit category lists and dense matrices.
let dot = DotPlot::new().with_matrix(
    vec!["TypeA", "TypeB"],
    vec!["Gene1", "Gene2"],
    vec![vec![80.0, 25.0], vec![15.0, 90.0]],
    vec![vec![3.5,  1.2],  vec![0.8,  4.1]],
);

Styling

with_color_map(map: ColorMap)

Set color map for color encoding (default ColorMap::Viridis).
use kuva::plot::ColorMap;

let dot = DotPlot::new()
    .with_data(data)
    .with_color_map(ColorMap::Inferno);
Options: Viridis, Inferno, Grayscale, Custom.

with_max_radius(r: f64)

Set maximum circle radius in pixels (default 12.0).
let dot = DotPlot::new()
    .with_max_radius(15.0);
Largest size value maps to this radius.

with_min_radius(r: f64)

Set minimum circle radius in pixels (default 1.0).
let dot = DotPlot::new()
    .with_min_radius(2.0);
Smallest size value maps to this radius.

Range Clamping

with_size_range(min: f64, max: f64)

Clamp size encoding to explicit [min, max] range before normalizing.
let dot = DotPlot::new()
    .with_data(data)
    .with_size_range(0.0, 100.0);  // always map 0–100% to radius range
Values below min map to min_radius; values above max map to max_radius. Useful for consistent scale across multiple plots.

with_color_range(min: f64, max: f64)

Clamp color encoding to explicit [min, max] range before normalizing.
let dot = DotPlot::new()
    .with_data(data)
    .with_color_range(0.0, 5.0);  // clamp expression to [0, 5]
Values outside range are clamped before color map is applied.

Legends

with_size_legend(label: impl Into<String>)

Enable size legend in right margin showing representative circle sizes.
let dot = DotPlot::new()
    .with_data(data)
    .with_size_legend("% Expressing");

with_colorbar(label: impl Into<String>)

Enable colorbar in right margin showing color scale.
let dot = DotPlot::new()
    .with_data(data)
    .with_colorbar("Mean expression");
When both legends present, they are stacked in a single right-margin column.

Examples

Full Single-Cell Gene Expression

let data: Vec<(&str, &str, f64, f64)> = vec![
    // PTPRC (CD45) — pan-leukocyte
    ("CD4 T",  "PTPRC",  92.0, 4.1), ("CD8 T",  "PTPRC",  95.0, 4.3),
    ("NK",     "PTPRC",  88.0, 3.9), ("B cell", "PTPRC",  91.0, 4.0),
    // CD3E — pan-T marker
    ("CD4 T",  "CD3E",   88.0, 3.8), ("CD8 T",  "CD3E",   91.0, 4.0),
    ("NK",     "CD3E",   12.0, 0.5), ("B cell", "CD3E",    5.0, 0.2),
    // CD4 — helper T marker
    ("CD4 T",  "CD4",    85.0, 3.5), ("CD8 T",  "CD4",     8.0, 0.3),
    ("NK",     "CD4",     4.0, 0.2), ("B cell", "CD4",     3.0, 0.1),
    // MS4A1 (CD20) — B cell marker
    ("CD4 T",  "MS4A1",   2.0, 0.1), ("CD8 T",  "MS4A1",   3.0, 0.1),
    ("NK",     "MS4A1",   2.0, 0.1), ("B cell", "MS4A1",  90.0, 4.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 Dot Plot")
    .with_x_label("Cell type")
    .with_y_label("Gene");
Eight marker genes across four immune cell types. Size = % expressing; color = mean expression.

Dense Matrix Input

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

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],
    vec![55.0, 40.0, 85.0, 20.0, 65.0],
    vec![30.0, 65.0, 10.0, 95.0, 40.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],
    vec![2.4, 1.8, 3.8, 0.9, 2.7],
    vec![1.3, 2.9, 0.5, 4.3, 1.6],
];

let dot = DotPlot::new()
    .with_matrix(x_cats, y_cats, sizes, colors)
    .with_size_legend("% Expressing")
    .with_colorbar("Mean expression");

let plots = vec![Plot::DotPlot(dot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Matrix Input")
    .with_x_label("Cell type")
    .with_y_label("Gene");
All grid cells filled — no missing combinations.

Size Legend Only

let data: Vec<(&str, &str, f64, f64)> = vec![
    ("CD4 T", "CD3E", 88.0, 0.0), ("CD8 T", "CD3E", 91.0, 0.0),
    ("NK",    "CD3E", 12.0, 0.0), ("Mono",  "CD3E",  3.0, 0.0),
    ("CD4 T", "CD4",  85.0, 0.0), ("CD8 T", "CD4",   8.0, 0.0),
];

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

let plots = vec![Plot::DotPlot(dot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Size Legend Only")
    .with_x_label("Cell type")
    .with_y_label("Gene");
Dot radius encodes a single variable; no colorbar.

Colorbar Only

let data: Vec<(&str, &str, f64, f64)> = vec![
    ("CD4 T", "CD3E", 50.0, 3.8), ("CD8 T", "CD3E", 50.0, 4.0),
    ("NK",    "CD3E", 50.0, 0.5), ("Mono",  "CD3E", 50.0, 0.1),
    ("CD4 T", "CD4",  50.0, 3.5), ("CD8 T", "CD4",  50.0, 0.3),
];

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

let plots = vec![Plot::DotPlot(dot)];
let layout = Layout::auto_from_plots(&plots)
    .with_title("Colorbar Only")
    .with_x_label("Cell type")
    .with_y_label("Gene");
All dots same size; color encodes expression level.

Legends

Both legends are optional and independent:
  • Size legend: Shows representative circle sizes with corresponding values
  • Colorbar: Shows color scale mapping data range to colormap
When both present, they stack in a single right-margin column.

See Also

  • Heatmap — For single-variable categorical grids
  • Bubble — For continuous x/y axes with size encoding

Build docs developers (and LLMs) love