malevich

Look up

Furniture

Titles, labels, legends, colorbars, and what sheds first when the frame shrinks.

Furniture is everything on a chart that is not data: the title, the axis labels, the legend, the colorbar, the ticks and their labels, and the axes themselves. Malevich lays it out with one rule — furniture sheds before data — and one switch, axes(false), for the charts that want none.

The pieces#

the furniture ── ridge 12 ┤ 1.0 │ 9 ┤ 0.5 │ ▓ r │ ▓ o 6 ┤ ▓ 0.0 w │ ▒ │ ▒ 3 ┤ ░ -0.5 │ ░ 0 ┤ ░ -1.0 └┬──────┬──────┬───────┬──────┬──────┬───────┬──────┬──────┬ 0 5 10 15 20 25 30 35 40 column
Plate 1. The plot has a title, axis labels, a legend, and a colorbar.
The code that drew it
use malevich::scale::Colormap;
use malevich::{Cells, Line, Plot};
// Every piece of furniture at once: title, axis labels, a legend, a colorbar.
let (w, h) = (40, 12);
let field: Vec<f64> = (0..w * h).map(|i| ((i % w) as f64 * 0.3).sin() * ((i / w) as f64 * 0.5).cos()).collect();
let x: Vec<f64> = (0..40).map(f64::from).collect();
let ridge: Vec<f64> = x.iter().map(|x| 6.0 + 3.0 * (x * 0.2).sin()).collect();
Plot::new()
    .layer(Cells::matrix(w, field).colormap(Colormap::CIVIDIS))
    .layer(Line::xy(x, ridge).label("ridge").glow())
    .colorbar()
    .title("the furniture")
    .x_label("column")
    .y_label("row")
piececomes fromshed order
titlePlot::titlesecond
axis labelsx_label, y_labelwith the title row
legendany layer with a label, or a color_by channelfirst
colorbarPlot::colorbar on a plot with a colormapwith the legend
context noteautomatic, when labels leave out a base or a datey note with the legend, x note with the title
tick labelscomputedlast, by thinning

The legend is built from layer labels in layer order. color_by adds one entry per category in first-appearance order, and Cells::classes adds swatches. A colorbar labels its ramp with the same exact-decimal formatter as the axes — decade ticks on a log colormap, band boundaries on a stepped one.

Shedding#

When the frame shrinks, the layout sheds furniture and does not fail: legend, then titles, then tick density. The data region is the last thing standing, because a small chart of the real numbers beats a complete frame around nothing. TERM=dumb at any width still gets a correct chart.

80×18

loss ── train ── val ── target 5 ┤ │ 4 ┤ │ l │ o 3 ┤ s │ s 2 ┤ < converging │ │ 1 ┤ │ 0 ┤ └┬─────┬─────┬─────┬──────┬─────┬─────┬─────┬─────┬──────┬─────┬─────┬─────┬ 0 10 20 30 40 50 60 70 80 90 100 110 120 step

56×14

loss ── train ── val ── target 5 ┤ 4 ┤ l │ o 3 ┤ s │ s 2 ┤ < converging │ 1 ┤ 0 ┤ └┬─────────┬─────────┬─────────┬─────────┬─────────┬ 0 25 50 75 100 125 step

40×10

loss ── train ── val ── target l 5 ┤ o │ s │ s │ < converging 0 ┤ └┬──────────┬───────────┬──────────┬ 0 40 80 120 step

26×7

loss l 5 ┤ o │ < convergi s 0 ┤ └┬────────────────┬─── 0 100 step

16×4

l 5 ┤ o 0 ┤ < conv └┬────────┬─ 0 100
Plate 2. One plot value, five frames. Watch the legend go first, then the title and axis labels, then the tick density, and note that the y labels keep their exact decimals to the end.

Rendering therefore never fails on frame grounds. Panics belong to construction, at the caller's line, on documented programmer invariants (unequal paired channels, a zero-column grid). A spec that arrives from data — deserialization, a config file — gets the checked twins, Plot::validate and Plot::try_render. They report the first problem as a typed error.

No axes at all#

axes(false) removes the axes, tick labels, and gutters, so the data region fills the frame. The sparkline preset is bars from zero with the axes off in a one-row frame. A waffle is class cells with the axes off. A thumbnail in a dashboard is anything with the axes off.

Plate 3. sparkline is the plot with axes(false), in a one-row frame.
The code that drew it
use malevich::sparkline;
// No axes at all: bars from zero, one row tall in a one-row frame.
sparkline(vec![3.0, 5.0, 2.0, 8.0, 6.0, 7.0, 4.0, 9.0, 5.0, 3.0, 6.0, 8.0, 2.0, 4.0, 7.0, 9.0, 8.0, 5.0])
Plate 4. axes(false) on a full plot lets the data region fill the frame.
The code that drew it
use malevich::{Line, Plot};
// The same furniture switch on a full plot: the data region fills the frame.
let x: Vec<f64> = (0..200).map(|i| f64::from(i) * 0.05).collect();
let y: Vec<f64> = x.iter().map(|x| (x * 1.3).sin() * (x * 0.2).cos()).collect();
Plot::new().layer(Line::xy(x, y)).axes(false)

The card#

The HTML and SVG cards add one more piece of furniture that a tty does not have: the card itself — a rounded rectangle in the theme's background and foreground. Theme::LIGHT selects the light card. Every other theme takes the dark one. The grid inside is the exact grid the terminal renderer would print.

loss ── train ── val ── target 5 ┤ │ 4 ┤ │ l │ o 3 ┤ s │ s 2 ┤ < converging │ │ 1 ┤ │ 0 ┤ └┬─────┬─────┬─────┬──────┬─────┬─────┬─────┬─────┬──────┬─────┬─────┬─────┬ 0 10 20 30 40 50 60 70 80 90 100 110 120 step
Plate 5. One plot value, drawn as a terminal card: a training run with two lines, a dark fill under the training loss, a dashed target rule, and a note at data coordinates.

What furniture will not do#

There is no legend placement option, no title alignment option, no gutter width option, no font. Each would be a knob on presentation that the frame already decides, and each would be one more thing the layout could not shed. Text the chart needs in a particular place is a Text mark at data coordinates. The rest is computed.