Skip to content

Charts

rich_ext::chart draws data with text:

  • Sparkline: one line, one cell per value.
  • BarChart: a label, a bar and a value per row.
  • Histogram: raw values counted into bins, drawn as bars.
  • LineChart: one or more series as lines or scattered points, with axes and a legend.

And the pieces of a dashboard:

  • Gauge and BulletChart: a value against a range, with a target and threshold bands, on one line or the full width.
  • Heatmap: a labelled grid of values drawn as shades, with a legend.
  • StatusMatrix: rows and columns of states (pass, fail, skip, flaky or your own), each a symbol and a colour.
  • KpiCard: a label, a value, its change, a sparkline and a status.
  • Timeline: labelled ranges on a numeric or seconds scale, overlaps stacked, with milestones; a Gantt strip when every range has its own row.

Each chart is a renderable. It measures itself, so it works in a Table cell, a Panel or a Live display. It never writes a line wider than the width it is given: when space is short it drops labels, values and axes before it drops data.

The examples come from guide_charts.rs. The module needs no feature:

cargo run -p rs-rich-ext --example guide_charts
cargo run -p rs-rich-ext --example charts            # every chart on one screen
cargo run -p rs-rich-ext --example charts -- --ascii
cargo run -p rs-rich-ext --example kpi_dashboard     # cards, a matrix and a timeline, live

The styles are theme keys (chart.*). Pass rich_ext::extended_theme() to the console builder to get them, or define them in your own theme. Without them the same defaults are used.

The same charts are in Python as rs_rich.chart, and in the shell as rich chart.

Sparklines

Sparkline::new(values) draws each value as a cell whose height shows where it falls between the smallest and largest value:

fn show_sparklines(console: &Console) {
    console.print(&Sparkline::new(REQUESTS));
    // Name the extremes and count the values over a threshold.
    console.print(&Sparkline::new(REQUESTS).min_max(true).threshold(40.0));
    // Two values per cell.
    console.print(&Sparkline::new(REQUESTS).charset(Charset::Braille));
    // Fewer cells than values: each cell is the mean of its bucket.
    let narrow = Console::builder().width(8).color_system(None).build();
    console.print(&Text::new(
        narrow.render_to_string(&Sparkline::new(REQUESTS)),
    ));
}

Sparklines in blocks, with a summary, in Braille and resampled

  • It is one cell per value wide. Given fewer cells, it resamples: the values are split into as many equal buckets as there are cells, and each cell shows the mean of its bucket's values.
  • .min_max(true) styles the lowest and highest cell (chart.min, chart.max) and writes min 12 max 52 after the line.
  • .threshold(40.0) styles the cells above 40 with chart.over and writes 4 > 40 (four values are above it).
  • The words are what make the extremes and the threshold readable without colour. They are the first thing left out when the line does not fit.
  • .range(min, max), .min(v) and .max(v) fix the scale; values outside it are clamped. NaN and infinite values are gaps.

Bars

BarChart draws a row per bar: the label, the bar and the value.

fn show_bars(console: &Console) {
    let chart = BarChart::new()
        .bar("api", 412.0)
        .bar("web", 268.5)
        .bar("worker", 97.0)
        .push(Bar::new("cron", 12.0).style("chart.over"))
        .bar_width(30);
    console.print(&chart);
    // Negative values extend left of zero.
    console.print(
        &BarChart::from_pairs([
            ("north", 18.0),
            ("south", -7.5),
            ("east", 4.0),
            ("west", -12.0),
        ])
        .bar_width(30),
    );
}

Bar charts with positive and negative values

  • Bars are measured from zero. The scale runs from the smallest to the largest value and always includes zero; .max(100.0) or .range(..) fix it.
  • Blocks give eighth-cell precision (█▉▊▋▌▍▎▏). A value above zero always shows at least a sliver.
  • Negative values extend left of zero, at whole-cell precision, in chart.negative. The written value keeps its minus sign, so the chart reads the same without colour.
  • Bar::new(label, value).style(..) styles one bar; .style(..) on the chart styles them all. Both take a theme key or a style definition.
  • The bar is 40 cells by default (.bar_width(n)). When the width is short the bar shrinks to 4 cells, then the values go, then the labels are cut with ….
  • .show_values(false) leaves the values out; .format(ValueFormat::Fixed(1)) writes them with one decimal instead of the compact form (1.2k, 3.4M).

Vertical bars

.orientation(Orientation::Vertical) stands the bars up side by side, with the labels underneath:

fn show_vertical(console: &Console) {
    let week = BarChart::from_pairs([
        ("mon", 31.0),
        ("tue", 42.5),
        ("wed", 38.0),
        ("thu", 51.0),
        ("fri", 47.0),
        ("sat", 12.0),
        ("sun", 9.5),
    ])
    .orientation(Orientation::Vertical)
    .bar_width(8);
    console.print(&week);
    // Negative values hang below zero, their values under them.
    console.print(
        &BarChart::from_pairs([("q1", 18.0), ("q2", -7.5), ("q3", 4.0), ("q4", -12.0)])
            .orientation(Orientation::Vertical)
            .bar_width(6),
    );
}

Vertical bar charts with positive and negative values

  • .bar_width(rows) is the tallest bar's height here, 8 rows by default.
  • Blocks give eighth-row precision (▁▂▃▄▅▆▇█), Braille half rows (⣤⣿), ASCII # and a half .. A value above zero always shows at least a sliver.
  • Each bar gets a slot as wide as the widest label or value, with a cell between slots, and is up to 3 cells thick. Each value sits just above its bar.
  • Negative values hang below the zero line at whole-row precision, their values under them.
  • When the width is short the values go first, then the labels are cut (and left out below two cells), then the gaps between bars. Bars that still do not fit are left off the right.
  • Histogram takes .orientation(..) too.

Histograms

Histogram::new(values).bins(n) counts raw values into n bins of equal width and draws them as a BarChart:

fn show_histogram(console: &Console) {
    console.print(&Histogram::new(latencies()).bins(8).bar_width(30));
}

A histogram of request latencies

  • Each label is [low, high), and the last [low, high], so every value falls in exactly one bin, the one its label names: a value on an edge counts in the bin that edge starts.
  • The edges are written exactly, never rounded: when the compact form would round one (1250 as 1.2k), they are all written in full ([1000, 1250)). .edges() returns them without floating-point noise (0.3, not 0.30000000000000004).
  • Without .range(min, max), the bin width is rounded up to 1, 2, 2.5 or 5 times a power of ten and the first edge down to a multiple of it, so the labels are round numbers. The last bins may then be empty.
  • With .range(min, max) the bins split that range exactly, and values outside it are not counted. NaN and infinite values are never counted.
  • .counts() and .edges() return the numbers; .to_bar_chart() the chart.

Line and scatter charts

A LineChart plots one or more Series. Series::line(name, points) joins its points, Series::scatter(name, points) does not, and Series::from_values(name, values) puts the values at x = 0, 1, 2, …

let cpu = (0..60).map(|i| (i as f64, 50.0 + 30.0 * (i as f64 / 8.0).sin()));
let memory = (0..60).map(|i| (i as f64, 30.0 + i as f64 * 0.8));
let alerts = [(7.0, 92.0), (23.0, 88.0), (41.0, 95.0), (55.0, 90.0)];
LineChart::new()
    .series(Series::line("cpu %", cpu))
    .series(Series::line("memory %", memory))
    .series(Series::scatter("alerts", alerts))
    .y_range(0.0, 100.0)
    .height(11)
    .width(64)

A Braille line chart with three series

  • The plot is Braille by default: each cell holds 2×4 dots, so a chart 60 cells wide has 120 points across.
  • Every row and column of the plot stands for one exact value, at its centre. Axis labels sit on evenly spaced rows and columns, and each label is the value of the row or column it is on; nothing is rounded to fit.
  • Without .y_range(..) and .x_range(..), each scale rounds out to a step of 1, 2, 2.5 or 5 times a power of ten that falls on whole rows or columns. The x scale may then run a little past the last point.
  • When the compact format cannot write those round values exactly, as with years (2015 would read 2.0k) or millisecond timestamps, the labels are all written in full (2015, 2016, …) rather than spreading the data thinly over a coarser, writable step.
  • With a fixed range, a row or column gets a label only when the format writes its value exactly, so a range must divide into the rows for every label to show: .y_range(0.0, 100.0) on 11 rows labels every 20, on 10 rows only 0 and 100. .y_format(..) and .x_format(..) write the labels.
  • It is .height(rows) rows of plot, an axis line, a row of x labels and the legend. Without .height(..) it takes 6 to 10 rows, near 8, picking the height its y labels divide best. It fills the width it is given, or .width(n). When the width is short it drops the y labels, then the axes.
  • A NaN or infinite value breaks a line.

Without colour

Series are told apart by colour (chart.series.1 to chart.series.5) and by marker: the legend shows each series' marker in its colour. Braille dots have no shape, so when the console shows no colour and there is more than one series, the chart plots at cell resolution with each series' marker instead:

fn show_without_colour(console: &Console) {
    // No colour: two or more series plot with markers, not Braille.
    let plain = Console::builder().width(64).color_system(None).build();
    console.print(&Text::new(plain.render_to_string(&traffic().height(6))));
}

The same chart without colour, plotted with markers

"No colour" is the console's fidelity below Rich: no colour system, NO_COLOR, or no_color(true). Series::marker('#') picks a series' marker; .charset(Charset::Blocks) asks for markers even with colour.

ASCII

Charset picks the glyphs: Blocks, Braille or Ascii. The default, Auto, is blocks for sparklines and bars and Braille for line charts. An ASCII-only console (its encoding is not UTF-8, or its fidelity is Ascii) always gets Ascii, whatever the chart asks for, and its output holds no character above U+007F:

fn show_ascii(console: &Console) {
    // An ASCII-only console, or `.charset(Charset::Ascii)`.
    let ascii = Console::builder()
        .width(64)
        .color_system(None)
        .ascii_only(true)
        .build();
    let out = [
        ascii.render_to_string(&Sparkline::new(REQUESTS).min_max(true)),
        ascii.render_to_string(
            &BarChart::new()
                .bar("api", 412.0)
                .bar("web", 268.5)
                .bar_width(20),
        ),
        ascii.render_to_string(&traffic().height(6)),
    ];
    console.print(&Text::new(out.concat().trim_end()));
}

Charts on an ASCII-only console

Chart ASCII
Sparkline the ramp _.-:=+*#, lowest to highest
BarChart, Histogram # for a cell, = for half a cell
LineChart *, +, o, x, . per series; axes drawn with -, + and a vertical bar
Gauge, BulletChart # and = for the bar, . and : for the bands, a vertical bar for the target
Heatmap ten shades, .:-=+*#%@, and ? for a missing value
StatusMatrix each state's ASCII symbol: + pass, X fail, - skip, ~ flaky
KpiCard a border of +, - and vertical bars, ^ and v for the delta, the status's ASCII symbol
Timeline # and = for ranges, * for milestones, ~ where a gap is cut

Labels with characters above U+007F are written with ? in their place, as fidelity::ascii_text does.

In tables and panels

A sparkline measures to one cell per value and a bar chart to its label, bar and value, so they fit table columns. A line chart takes the width it is given, or .width(n):

fn show_table(console: &Console) {
    let mut table = Table::new().title("Services");
    table.add_column("Service");
    table.add_column("Requests, last 24 h");
    table.add_column("p95 ms");
    for (name, offset, p95) in [("api", 0, 412.0), ("web", 6, 268.0), ("worker", 12, 97.0)] {
        let values = REQUESTS.iter().cycle().skip(offset).take(24).copied();
        table.add_row_cells(vec![
            Cell::Markup(name.into()),
            Cell::Renderable(Arc::new(Sparkline::new(values))),
            Cell::Renderable(Arc::new(
                BarChart::new()
                    .bar("", p95)
                    .max(500.0)
                    .bar_width(12)
                    .format(ValueFormat::Fixed(0)),
            )),
        ]);
    }
    console.print(&table);
    let panel = Panel::new(Box::new(traffic().width(56).height(5)))
        .title("cpu and memory")
        .expand(false);
    console.print(&panel);
}

Sparklines and bars in a table, a line chart in a panel

Gauges and bullet charts

A Gauge shows one value against a range. .target(t) draws a │ across the bar where the target is, and each Band is a threshold: the band runs up to its value, the bar takes the band's style while the value is in it, and the band's name is written after the value.

fn show_gauges(console: &Console) {
    let cpu = Gauge::new("cpu", 72.0)
        .range(0.0, 100.0)
        .target(80.0)
        .band(Band::new(60.0, "ok").style("chart.ok"))
        .band(Band::new(85.0, "high").style("chart.warning"))
        .band(Band::new(100.0, "critical").style("chart.critical"))
        .unit("%");
    // One line: label, bar, value and the band the value is in.
    console.print(&cpu.clone().bar_width(24));
    // Full width: a scale under the bar and a legend.
    console.print(&cpu.full_width(true));
    console.print(&Text::new(""));
    // Several gauges with their columns aligned.
    let quarter = BulletChart::new()
        .gauge(Gauge::new("revenue", 270.0).range(0.0, 300.0).target(250.0))
        .gauge(Gauge::new("profit", 22.5).range(0.0, 30.0).target(26.0))
        .gauge(
            Gauge::new("new customers", 1650.0)
                .range(0.0, 2000.0)
                .target(1800.0)
                .band(Band::new(1400.0, "poor").style("chart.critical"))
                .band(Band::new(1700.0, "fair").style("chart.warning"))
                .band(Band::new(2000.0, "good").style("chart.ok")),
        )
        .bar_width(30);
    console.print(&quarter);
}

A compact gauge, a full-width gauge with its scale and legend, and a bullet chart

  • The bar fills with eighth-cell blocks (█▉▊▋▌▍▎▏), half-cell Braille, or # and a half = in ASCII. The rest of the track is shaded by band, alternating ░ and ▒ (. and :), so each band's extent shows without colour.
  • The scale runs from 0 (or the smallest of the value, the target and the bands) to the largest of them; .range(min, max) fixes it.
  • The compact form is one line, with a bar of .bar_width(n) cells (20 by default). .full_width(true) fills the width and adds a scale line (the bounds, the target and the band edges, each at its cell) and a legend (░ ok up to 60% │ target 80%).
  • Given less width the bar shrinks to 4 cells, then the band's name goes, then the value, then the label is cut.
  • BulletChart stacks gauges with their labels, bars, values and band names in aligned columns. Each keeps its own scale, target and bands.

Heatmaps

A Heatmap is a grid of values, a row per label and a column per header, each cell a shade from the lowest value to the highest:

fn show_heatmap(console: &Console) {
    let hours: Vec<String> = (0..24).map(|h| format!("{h:02}")).collect();
    let mut map = Heatmap::new().columns(hours);
    for (day, name) in ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
        .into_iter()
        .enumerate()
    {
        let weekend = if day >= 5 { 0.4 } else { 1.0 };
        let load = (0..24).map(|h| {
            let peak = (-((h as f64 - 14.0).powi(2)) / 30.0).exp();
            (peak * 900.0 * weekend + 40.0 * day as f64).round()
        });
        map = map.row(name, load);
    }
    console.print(&map);
    // In ASCII: ten shades, so it reads in black and white.
    let ascii = Console::builder()
        .width(60)
        .color_system(None)
        .ascii_only(true)
        .build();
    console.print(&Text::new(ascii.render_to_string(&map).trim_end()));
}

A heatmap of load by hour and day, in blocks and in ASCII

  • Values are scaled into equal steps: five in blocks (░▒▓█), ten in ASCII (.:-=+*#%@). The shade is the value, so the grid reads in black and white; with colour each step also takes a chart.heat.N style, cold to hot. .range(min, max) fixes the scale.
  • A NaN or infinite value is drawn · (? in ASCII), never as a low value, and the legend says so.
  • Each value is .cell_width(n) cells wide (2 by default). A header is written at its column's first cell when it fits with a space after the one before, so with narrow cells every second or third header shows.
  • Given less width the cells narrow to one, then the row labels are cut, then neighbouring columns are merged, each showing the mean of its values, so every value still counts.

Status matrices

A StatusMatrix is rows and columns of states, such as a test suite across platforms. Each state is a symbol and a colour, never colour alone, and the legend counts the cells in each:

fn show_matrix(console: &Console) {
    let matrix = StatusMatrix::new()
        // Your own state: a name, a symbol, an ASCII symbol and a style.
        .state(State::new("running", '◌', 'o', "chart.state.unknown"))
        .columns(["linux", "macos", "windows", "wasm"])
        .row("unit", ["pass", "pass", "pass", "pass"])
        .row("integration", ["pass", "flaky", "fail", "skip"])
        .row("docs", ["pass", "pass", "running", "skip"]);
    console.print(&matrix);
}

A status matrix of test suites across platforms

State Symbol ASCII Theme key
pass ✓ + chart.state.pass
fail ✗ X chart.state.fail
skip ○ - chart.state.skip
flaky ≈ ~ chart.state.flaky
  • State::new(name, symbol, ascii, style) adds a state, or replaces a built-in one with the same name. A symbol must take one cell; a wide one is replaced by its ASCII form.
  • A name with no state is drawn ? in chart.state.unknown and named in the legend as it is. A row shorter than the headers leaves its last cells blank.
  • Columns are as wide as their widest header. Given less width the headers are cut (and left out below three cells), then the row labels, then the gaps; columns that still do not fit are left off the right.
  • .counts() returns how many cells are in each state.

KPI cards

A KpiCard is one key number: a label, the value, its change, an optional sparkline and an optional Status:

fn show_cards(console: &Console) {
    let cards = vec![
        KpiCard::new("Requests", 12_400.0)
            .unit("/s")
            .previous(11_430.0)
            .caption("last week")
            .trend(trend(0.0))
            .status(Status::Ok),
        KpiCard::new("Errors", 42.0)
            .delta(-14.0)
            .higher_is_better(false)
            .caption("per hour")
            .trend(trend(4.0))
            .status(Status::Warning),
        KpiCard::new("Latency", 412.0)
            .unit(" ms")
            .previous(260.0)
            .higher_is_better(false)
            .trend(trend(8.0))
            .status(Status::Critical),
    ];
    let cells = cards
        .into_iter()
        .map(|card| Cell::Renderable(Arc::new(card.width(24))))
        .collect();
    console.print(&Columns::from_cells(cells));
}

Three KPI cards side by side in Columns

  • The change is an arrow and a signed number: ▲ +8.49%, ▼ -14, = 0 (^, v and = in ASCII). .delta(d) gives an amount, .delta_percent(p) a percentage and .previous(v) the percentage change from v. A change that is NaN or too large for an f64 reads = -: no sign, and the flat arrow. It is styled chart.delta.good or chart.delta.bad by whether the change is for the better: .higher_is_better(false) for errors, latency and costs. The arrow and the sign carry the direction without colour.
  • Status::Ok, Warning, Critical and Unknown are a symbol and a word (✓ ok, ! warning, ✗ critical, ? unknown) in chart.ok, chart.warning, chart.critical or chart.unknown.
  • A card measures to its content, so cards sit side by side in Columns or a table. .width(n) fixes its width, border included, and .expand(true) fills what it is given, as in a Layout. Cards with the same parts have the same height, so a row of them lines up.
  • .border(false) leaves the border out; below 5 cells it goes anyway.

Timelines and Gantt strips

A Timeline puts labelled ranges on a numeric scale, such as the steps of a build in seconds. Ranges with the same row label share a row:

fn show_timeline(console: &Console) {
    let build = Timeline::new()
        .span("fetch", 0.0, 4.0)
        .span("compile", 4.0, 26.0)
        // Overlapping ranges on one row are stacked.
        .span("test", 12.0, 30.0)
        .span("test", 20.0, 34.0)
        .span("package", 34.0, 39.0)
        .milestone("ship", 40.0)
        .unit("s")
        .width(60);
    console.print(&build);
    console.print(&Text::new(""));
    // Ten minutes idle between two short bursts: the gap is cut out.
    let jobs = Timeline::new()
        .span("worker 1", 0.0, 3.0)
        .span("worker 1", 3.0, 5.0)
        .span("worker 2", 2.0, 6.0)
        .span("worker 1", 600.0, 604.0)
        .span("worker 2", 602.0, 610.0)
        .milestone("deploy", 611.0)
        .unit("s")
        .width(60);
    console.print(&jobs);
}

A Gantt strip of a build, and two bursts of jobs with the idle gap cut out

  • Ranges on one row that overlap are stacked onto extra lines, so none hides another. A range that starts where the one before it on its line ends is drawn ▓ instead of █ (= instead of #), so both show.
  • A range covers every column whose centre value is inside it, and at least one. Its length is written after it when there is room (.durations(false) leaves them out), so it reads without the axis.
  • .milestone(label, at) marks a point with ◆ (*) on a line under the ranges, its label beside it.
  • The axis labels sit on evenly spaced columns and name each column's exact value, as on a line chart; .unit("s") writes a unit after every value. The scale is plain numbers: pass seconds (or any unit) as f64.
  • Compression: when the range is wider than the plot, so that the shortest range would get less than a column, idle gaps are cut out when they are longer than both four times the shortest range and three columns' worth of the uncut scale. Each cut takes three columns with ≈ (~) on the axis, and each stretch is labelled at its start and end. .compress(false) keeps the scale linear; .range(min, max) fixes it.
  • It fills the width given, or .width(n). Given less, the row labels are cut so the plot keeps 8 columns.

A dashboard

Cards, a matrix and a timeline in a Layout, rebuilt and passed to Live::update on every tick, make a live dashboard. The kpi_dashboard example runs one; this is a frame of a smaller one:

fn dashboard() -> Layout {
    let card = |c: KpiCard| Layout::with_renderable(Box::new(c.expand(true)));
    let mut cards = Layout::new().size(6);
    cards.split_row(vec![
        card(
            KpiCard::new("Requests", 1240.0)
                .unit("/s")
                .previous(1180.0)
                .trend(trend(0.0)),
        ),
        card(
            KpiCard::new("Errors", 7.0)
                .delta(2.0)
                .higher_is_better(false)
                .trend(trend(5.0))
                .status(Status::Warning),
        ),
    ]);
    let checks = StatusMatrix::new()
        .columns(["eu", "us", "ap"])
        .row("api", ["pass", "pass", "flaky"])
        .row("worker", ["pass", "fail", "pass"]);
    let deploy = Timeline::new()
        .span("build", 0.0, 18.0)
        .span("test", 12.0, 34.0)
        .span("rollout", 36.0, 52.0)
        .milestone("live", 60.0)
        .unit("s");
    let mut root = Layout::new();
    root.split_column(vec![
        cards,
        Layout::with_renderable(Box::new(Panel::new(Box::new(checks)).title("checks"))).size(6),
        Layout::with_renderable(Box::new(Panel::new(Box::new(deploy)).title("deploy"))),
    ]);
    root
}

Two KPI cards, a status matrix and a timeline in a Layout

Cards take .expand(true) to fill their region. The layout fills the console's height, so give Live a console with a fixed .height(n) or let it take the terminal's.

From the shell: rich chart

rich chart draws data from a file, a URL or stdin with these charts, so a shell pipeline can end in a picture. It reads CSV and TSV (the dialect sniffed as rich --csv sniffs it), JSON (an array of records, of numbers or of rows, or an object of columns), JSON Lines, or whitespace-separated numbers.

rich chart sales.csv                                  # every numeric column as a line
rich chart sales.csv --kind bar --x month --y api     # a bar per row, labelled
rich chart sales.csv --kind heatmap                   # rows by series, as shades
seq 1 20 | rich chart --kind spark                    # numbers piped in
rich chart metrics.json --kind scatter --x t --y p50 --y p99
Option Meaning
--kind spark\|bar\|line\|scatter\|heatmap What to draw; line by default
--x COLUMN The positions (line, scatter: numbers) or labels (bar, heatmap). Default: the row number; for labels, the first column that is not all numbers
--y COLUMN A series; repeat for several. Default: every numeric column but --x
--width N The width to draw in

Columns go by header or by 1-based number, so a CSV without a header works too (its columns are 1, 2, ...). An empty cell or a JSON null is a gap. A column that is not there, or a value that is not a number, is refused with exit code 4 and a message naming the row (counted from 1, after the header) and the column:

$ rich chart sales.csv --y mobile
rich: no column "mobile"; the columns are "month", "api", "web"
$ rich chart sales.csv --x month
rich: row 1, column "month": "Jan" is not a number (a line or scatter chart's --x is a position; --kind bar takes labels)
$ rich chart sales.csv --kind bar --y api --width 50
Jan ███████████████████████▌                 30
Feb █████████████████████████████████        42
Mar ███████████████████████████▌             35
Apr ████████████████████████████████████████ 51
May █████████████████████████████████████▋   48

The recording shows it in a terminal.

The shared pieces

Type What it does
Scale A linear scale. Scale::from_values skips NaN and infinities and never has an empty range: when every value is v the range is twice as wide as v with v in the middle (0..1 for 0), and no values gives 0..1. .nice(n) widens to round bounds; .ticks(n) gives about n round values inside it; .normalize(v) maps to 0..1
ValueFormat Compact (950, 0.25, 1.2k, 3.4M, 1.0e15 from a thousand trillion) or Fixed(decimals) (at most ValueFormat::MAX_DECIMALS, 17; more are capped)
Status A health level for a KpiCard: a symbol, a word and a theme key
State A state of a StatusMatrix cell: a name, a symbol, an ASCII symbol and a style
Band, Span, Milestone A gauge's threshold band; a timeline's range and point
Charset Auto, Blocks, Braille, Ascii
DotCanvas The Braille canvas the line chart draws on: set dots, draw lines, read cells

Theme keys:

Key Default Used for
chart.axis bright_black Axes and ticks
chart.label, chart.value none Labels, tick labels and values
chart.bar cyan Bars
chart.negative magenta Bars below zero
chart.spark cyan Sparklines
chart.over bold red Sparkline values above the threshold
chart.min, chart.max blue, bold green Sparkline extremes
chart.series.1 … .5 cyan, magenta, yellow, green, blue Series, by position (they cycle); timeline rows
chart.track bright_black A gauge's track and band shades
chart.target bold A gauge's target marker
chart.ok, chart.warning, chart.critical, chart.unknown green, yellow, bold red, bright_black Status, and bands that name them
chart.delta.good, chart.delta.bad, chart.delta.flat green, red, bright_black A KPI card's change
chart.kpi.label, chart.kpi.value, chart.kpi.border none, bold, bright_black A KPI card
chart.heat.1 … .5 blue, cyan, green, yellow, red Heatmap steps, cold to hot
chart.state.pass, .fail, .skip, .flaky, .unknown green, bold red, bright_black, yellow, magenta Status matrix states
chart.milestone bold yellow Timeline milestones