Learn
Recipes
Benchmarks piped through jq, the pie, tornado bars, two scales, and out-of-range rules.
These answer the requests that show up most, each with what already exists. Every chart type below is a composition of the grammar. None needed a new concept (what earns one).
Benchmarks through jq#
hyperfine exports one JSON file per
run. Its results carry a mean and every timing. Means as sideways bars in
seconds, then one command's distribution:
hyperfine --export-json bench.json 'sort big.txt' 'sort --parallel=4 big.txt'
jq -r '.results[] | [.command, .mean] | @tsv' bench.json | kaz bar --horizontal --unit s
jq -r '.results[0].times[]' bench.json | kaz hist --unit sCriterion writes
target/criterion/<bench>/new/estimates.json (nanoseconds) and the samples
beside it:
for run in target/criterion/*/new; do
printf '%s %s\n' "$(basename "$(dirname "$run")")" "$(jq .mean.point_estimate "$run/estimates.json")"
done | kaz bar --horizontal --unit ns
jq -r '.times[]' target/criterion/parse/new/sample.json | kaz density --unit nskaz describe prints the same numbers as a table. --emit-code on any of
them writes the Rust program.
The pie: a waffle or a breakdown#
A pie encodes parts by angle, which the eye ranks poorly and a cell grid cannot draw. Two compositions read the parts against a straight axis:
- The waffle: one hundred cells, each a category, as
Cells::classesover a ten-by-ten grid with the axes off — thewafflegallery example. Shares round to whole cells and the legend names them. - The breakdown:
Barsfrom astack— thebreakdownexample — withStackOffset::Normalizewhen the parts should sum to 100 %.
Tornado and breakdown bars#
A tornado is horizontal bars around zero, the longest first:
use malevich::{Bars, Frame, Plot, Rule};
let factors = ["price", "volume", "fx", "cost"];
let swing = [-4.0, 3.5, -1.2, 0.8];
let plot = Plot::new()
.layer(Bars::new(factors, &swing[..]).horizontal())
.layer(Rule::v(0.0));
println!("{}", plot.render(&Frame::plain(60, 10)));Sort the categories by |swing| first and the shape appears. A breakdown
stacks segments with Bars::base, one layer per part.
Two series, two scales: a Grid and a shared window#
Twin y axes are refused (why). The honest form is two panels that share the x window, so the eye compares shapes without a fabricated common magnitude:
use malevich::{Frame, Grid, Line, Plot};
let x: Vec<f64> = (0..200).map(f64::from).collect();
let rate: Vec<f64> = x.iter().map(|v| 3.0 + (v * 0.05).sin()).collect();
let count: Vec<f64> = x.iter().map(|v| 1000.0 + v * 12.0).collect();
let window = (0.0, 199.0);
let grid = Grid::new(1)
.with(Plot::new().layer(Line::xy(&x[..], &rate[..])).x_domain(window.0, window.1).title("rate"))
.with(Plot::new().layer(Line::xy(&x[..], &count[..])).x_domain(window.0, window.1).title("count"));
println!("{}", grid.render(&Frame::plain(60, 20)));Interactively, one Viewport applied to both plots keeps them linked
(interaction.md).
Positions clip, colors squish#
under and over colors, and the colorbar shows the range.Two out-of-range rules, deliberately different:
- A value outside a fixed axis domain (
x_domain,y_min, aViewport) is clipped: drawn nowhere, never moved onto the border. A point at the edge is a point at the edge, not a refugee from beyond it. - A value outside a fixed colormap domain (
Colormap::domain) is squished into the ramp's nearest end, because a cell must be some color.underandoverdisclose the squish with colors of their own, and the colorbar shows the fixed range.
Gaps follow the first rule everywhere. NaN is a break, never interpolated.
A non-positive value on a log axis is a gap.
Plain text is agent-legible#
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
Frame::plain renders the chart as the text it also is: an axis with exact
labels, marks in rows, no escape byte. That is the form a log, a diff, a
test assertion, or a language model reads — and every colored or pixel
rendering degrades to it, so nothing a chart says lives only in color.