Figure (Multi-Plot Layout)

Figure arranges multiple independent plots in a grid. Each cell can contain any plot type. Cells can span multiple rows or columns, axes can be shared across cells, and a single legend can be collected from all panels.


Basic grid

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

let scatter = ScatterPlot::new()
    .with_data(vec![(1.0_f64, 2.3), (2.1, 4.1), (3.4, 3.2), (4.2, 5.8)])
    .with_color("steelblue");

let line = LinePlot::new()
    .with_data(vec![(0.0_f64, 0.4), (2.0, 2.0), (4.0, 3.7), (6.0, 4.9), (8.0, 6.3)])
    .with_color("crimson");

// Build plot vecs first so layouts can auto-range from the data
let plots_a: Vec<Plot> = vec![scatter.into()];
let plots_b: Vec<Plot> = vec![line.into()];

let layout_a = Layout::auto_from_plots(&plots_a).with_title("Scatter").with_x_label("X").with_y_label("Y");
let layout_b = Layout::auto_from_plots(&plots_b).with_title("Line").with_x_label("Time").with_y_label("Value");

let scene = Figure::new(1, 2)                          // 1 row, 2 columns
    .with_plots(vec![plots_a, plots_b])
    .with_layouts(vec![layout_a, layout_b])
    .with_labels()                                     // bold A, B panel labels
    .render();

let svg = SvgBackend.render_scene(&scene);
std::fs::write("figure.svg", svg).unwrap();
}

with_plots takes a Vec<Vec<Plot>> — one inner Vec per panel, in row-major order (left to right, top to bottom). with_layouts takes a Vec<Layout> in the same order.

Build each layout from its plot vec before passing both to the figure — Layout::auto_from_plots needs to see the data to compute axis ranges. with_layouts is optional; omit it and each panel auto-computes its own range.

Basic 1×2 figure with panel labels

Merged cells

Use with_structure to span cells. The structure is a Vec<Vec<usize>> where each inner vec lists the cell indices (row-major) that form one panel.

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

// 2×3 grid: three small plots on top, one wide plot spanning the full bottom row
let figure = Figure::new(2, 3)
    .with_structure(vec![
        vec![0],        // top-left
        vec![1],        // top-centre
        vec![2],        // top-right
        vec![3, 4, 5],  // bottom row — spans all 3 columns
    ])
    .with_plots(vec![
        vec![/* plot A */],
        vec![/* plot B */],
        vec![/* plot C */],
        vec![/* wide plot D */],
    ]);
}

For a tall left panel spanning both rows of a 2×2 grid:

#![allow(unused)]
fn main() {
// cell indices for a 2×2 grid:
//   0  1
//   2  3
Figure::new(2, 2)
    .with_structure(vec![
        vec![0, 2],  // left column, both rows — tall panel
        vec![1],     // top-right
        vec![3],     // bottom-right
    ]);
}

Groups must be filled rectangles — L-shapes and other non-rectangular spans are not supported.

2×3 figure with merged bottom panel

For a tall left panel:

2×2 figure with tall left panel

Shared axes

Sharing an axis unifies the range across the linked panels and suppresses duplicate tick labels on inner edges.

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

Figure::new(2, 2)
    // ...
    .with_shared_y_all()       // same Y range across all panels
    .with_shared_x_all()       // same X range across all panels
    ;
}
2×2 figure with shared Y axes per row

Fine-grained control:

MethodEffect
.with_shared_y_all()Shared Y across every panel
.with_shared_x_all()Shared X across every panel
.with_shared_y(row)Shared Y within a single row
.with_shared_x(col)Shared X within a single column
.with_shared_y_slice(row, col_start, col_end)Shared Y for a subset of a row
.with_shared_x_slice(col, row_start, row_end)Shared X for a subset of a column

Panel labels

#![allow(unused)]
fn main() {
figure.with_labels()            // A, B, C, ... (bold, size 16 — default)
figure.with_labels_lowercase()  // a, b, c, ...
figure.with_labels_numeric()    // 1, 2, 3, ...

// Custom strings — size and bold are the only meaningful config fields here
figure.with_labels_custom(
    vec!["i", "ii", "iii"],
    LabelConfig { style: PanelLabelStyle::Uppercase, size: 14, bold: false },
)
}

Shared legend

Collect legend entries from all panels into a single figure-level legend. Per-panel legends are suppressed automatically.

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

Figure::new(1, 2)
    // ...
    .with_shared_legend()         // legend to the right (default)
    .with_shared_legend_bottom()  // legend below the grid
    ;
}
1×2 figure with shared legend

To keep per-panel legends visible alongside the shared one:

#![allow(unused)]
fn main() {
figure.with_keep_panel_legends()
}

To supply manual legend entries instead of auto-collecting:

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

figure.with_shared_legend_entries(vec![
    LegendEntry { label: "Control".into(), color: "steelblue".into(), shape: LegendShape::Rect },
    LegendEntry { label: "Treatment".into(), color: "crimson".into(), shape: LegendShape::Rect },
])
}

Sizing and spacing

#![allow(unused)]
fn main() {
Figure::new(2, 3)
    .with_cell_size(500.0, 380.0)  // width × height per cell in pixels (default)
    .with_spacing(15.0)            // gap between cells in pixels (default 15)
    .with_padding(10.0)            // outer margin in pixels (default 10)
    .with_title("My Figure")       // centered title above the grid
    .with_title_size(20)           // title font size (default 20)
}

The total SVG dimensions are computed automatically from the cell size, spacing, padding, title height, and any shared legend.

Alternatively, set the total figure size and let cells auto-compute:

#![allow(unused)]
fn main() {
Figure::new(2, 3)
    .with_figure_size(900.0, 560.0)  // total width × height; cells sized to fit
}

with_figure_size takes precedence over with_cell_size when both are set. The cell size budget is computed after reserving space for padding, spacing, title height, and any shared legend — so the output SVG dimensions exactly match what you specify.

2×3 figure sized to a fixed 900×560 total

API reference

MethodDescription
Figure::new(rows, cols)Create a rows × cols grid
.with_structure(vec)Define merged cells; default is one panel per cell
.with_plots(vec)Set plot data, one Vec<Plot> per panel
.with_layouts(vec)Set layouts; panels without a layout auto-range from data
.with_title(s)Centered title above the grid
.with_title_size(n)Title font size in pixels (default 20)
.with_labels()Bold uppercase panel labels (A, B, C, …)
.with_labels_lowercase()Lowercase panel labels (a, b, c, …)
.with_labels_numeric()Numeric panel labels (1, 2, 3, …)
.with_labels_custom(labels, config)Custom label strings with font config
.with_shared_y_all()Shared Y range across all panels
.with_shared_x_all()Shared X range across all panels
.with_shared_y(row)Shared Y within one row
.with_shared_x(col)Shared X within one column
.with_shared_y_slice(row, c0, c1)Shared Y for columns c0..=c1 in a row
.with_shared_x_slice(col, r0, r1)Shared X for rows r0..=r1 in a column
.with_shared_legend()Figure-level legend to the right
.with_shared_legend_bottom()Figure-level legend below the grid
.with_shared_legend_entries(vec)Override auto-collected legend entries
.with_keep_panel_legends()Keep per-panel legends alongside the shared one
.with_cell_size(w, h)Cell dimensions in pixels (default 500 × 380)
.with_figure_size(w, h)Total figure dimensions; cells auto-compute to fit
.with_spacing(px)Gap between cells (default 15)
.with_padding(px)Outer margin (default 10)
.render()Consume the Figure and return a Scene