malevich

Reference

Terminology

The vocabulary contract. Every public concept, what it means, what it maps to, illustrated.

This is the public vocabulary, and a codebase contract. Every public concept is named here before it is named in code. When a concept is added, renamed, or changes meaning, this file is updated in the same change. Each entry gives the word's meaning in the wider literature, and what it maps to in the crate. Design arguments live in vision and principles. Entries here only name their conclusions.

Plot#

Plot on docs.rs.

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 1. 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.

A plot is the retained description of a chart: layers, plus scales, plus furniture. Furniture is the title, the labels, the legend, and whether the axes are drawn at all. axes(false) is a sparkline's furniture. A plot is a plain value — cloneable, inspectable, serializable — with no connection to a terminal. Rendering is a pure function of a Plot and a Frame. Maps to plot::Plot (re-exported at the root). See The frame is run state.

Layer#

Plot::layer on docs.rs.

•• Adelie •• Chinstrap •• Gentoo │ 21 ┤ 20 ┤ │ 19 ┤ │ 18 ┤ 17 ┤ │ 16 ┤ │ 15 ┤ │ 14 ┤ 13 ┤ └┬──────────┬──────────┬───────────┬──────────┬──────────┬──────────┬ 30 35 40 45 50 55 60
Plate 2. Three more layers add a least-squares fit per species, one Line each, from the same Fit accumulator the trend preset uses.

A layer is one mark bound to data and options, stacked with other layers on shared scales. An axis domain is the union of all layers' data, and it is re-resolved at render time. The word is the layering concept of every grammar of graphics (Wilkinson 2005; Vega-Lite layer). Maps to Plot::layer.

Mark#

Mark on docs.rs.

rules: lines and spans ░░ warm-up ░░ tolerance ── signal ── baseline ── deploy │ 100 ┤ │ 90 ┤ │ 80 ┤ │ 70 ┤ │ 60 ┤ │ 50 ┤ └┬─────┬──────┬──────┬─────┬──────┬──────┬─────┬──────┬──────┬─────┬ 0 10 20 30 40 50 60 70 80 90 100
Plate 3. Rule draws horizontal and vertical lines, dashed or not, and spans that wash a band across the plot.

A mark is a family of geometric primitives that draw data. The word follows Observable Plot and Vega-Lite ("mark"), chosen over matplotlib's "artist" (too broad) and "geom" (ggplot jargon). Eight marks are joined under the closed mark::Mark enum: Line, Points, Bars, Area, Cells, Range, Rule, and Text.

Line is made from points, a paired series, or a sampled function. Bars is bands, contiguous numeric spans, free positions, or explicit intervals. Bars::intervals(starts, ends, values) is the histogram with irregular bins. Bars rise from the zero baseline, or from a per-bar base — the y2-style channel that makes stacked bars, grouped bars, and waterfalls plain compositions. horizontal turns any placement sideways: the bands go down the y axis in reading order, and the values go along x. That sideways placement is the barh of the catalog.

Area is baseline fills and bands. Cells is value grids, rgb images, or categorical class regions. Range is intervals with optional body and marker channels. Rule is reference lines at one value, and spans. h_span and v_span wash the band between two values across the plot: a recession, a warm-up phase, a tolerance window. Text is annotations at data coordinates.

Chart types are compositions of marks, never peers of them. The family is complete. See What earns a concept.

Channel#

flipper length against body mass •• Adelie •• Chinstrap •• Gentoo │ 6000 ┤ │ │ 5000 ┤ │ g │ │ 4000 ┤ │ │ 3000 ┤ │ └┬─────────┬──────────┬─────────┬─────────┬──────────┬─────────┬─ 170 180 190 200 210 220 230 mm
Plate 4. Points::color_by gives three species Okabe–Ito colors and a legend.

A channel is a per-mark visual variable, fed from data or set constant: x, y, y2, color, label, and the rest. The word follows Vega-Lite and Observable Plot ("encoding channel"). Position channels accept anything series-shaped, through constructor arguments. Constant channels are builder methods.

The data-bound color channel is color_by(categories) on Line, Points, Bars, and Range. Categories take palette colors in first-appearance order, name themselves in the legend, and cycle marker shapes in colorless output, so groups never vanish in a pipe.

The alignment channel is align(Align) on Text — Left (the default: start at the anchor and extend right), Center, and Right (end at the anchor). On a Bands x axis, the band nearest the anchor becomes the box, with exactly the geometry the band's own header label uses — its rounded center, its step-wide budget — so aligned text and band labels land in lockstep. Text wider than the box clips to it, ending with a truncation .. Digits from a neighboring column are never mixed into a number. Stat tables and annotated matrices are set in this channel.

Series#

Series on docs.rs.

a gap is a gap 3.4 ┤ │ 3.0 ┤ │ 2.6 ┤ │ 2.2 ┤ └┬─────────┬──────────┬──────────┬─────────┬ 0 2 4 6 8
Plate 5. Two missing readings. The line breaks; nothing is invented.

A series is one column of scalar data after ingestion: contiguous f64, where NaN is a gap (see Gap). The ingestion boundary is the IntoSeries trait. Slices, arrays, and vectors of any primitive numeric type convert exactly once, at the rim. Borrowed f64 slices are zero-copy. The core is monomorphic f64. Maps to data::Series and data::IntoSeries. See Conversion lives at the rim.

Stat#

stat on docs.rs.

Window: one reducer vocabulary ── samples ── rolling mean ── rolling p95 100 ┤ │ │ 80 ┤ │ 60 ┤ m │ s │ 40 ┤ │ │ 20 ┤ │ 0 ┤ └┬────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┬ 0 50 100 150 200 250 300 350 400
Plate 6. Window draws a rolling mean and a rolling p95 from one reducer vocabulary.

A stat is a data operation that runs before scales see the data. The word follows seaborn.objects (Stat) and ggplot (stat_*). It is the module-level umbrella, not one execution algebra. A stat may be an online accumulator, a reducer, keyed orchestration, or a batch transform. Maps to the stat module: M4, Bins/bins2, calendar_bins, Agg, BoxStats, kde/kde_with, Window, jitter, steps, cumsum/diff/rank/normalize, ecdf, roc/auc, ewma, stack/stack_with, dodge, lttb, Moments, Fit, and nearest.

Bins::heights rescales the counts under a Normalization — count, probability, percent, density per unit of x — and accumulates them. hist and kaz hist share it.

calendar_bins counts per hour, day, ISO week, month, or year — a TimeUnit — over unix timestamps. Buckets keep their true length, empties are kept, and the result feeds Bars::intervals.

BoxStats uses type-7 quartiles. The whiskers are Whiskers::Tukey(k), Percentiles(lo, hi), or MinMax.

kde and kde_with use Silverman's bandwidth by default, scaled or fixed via Bandwidth. The bounds reflect the kernels, so a latency density stays above zero. There is also the cumulative form.

Window is a sliding window, anchored at its end by default, or centered or leading via WindowAnchor. strict gaps the positions whose window is incomplete.

jitter uses van der Corput offsets that spread a strip of points evenly, with no seed.

steps is the piecewise-constant expansion stairs draws, changing after, before, or midway between samples per StepDirection.

cumsum, diff, rank, and normalize are the series maps: running sums, differences, ranks, and division by a reducer of the whole series — index charts, percent-of-peak.

stack and stack_with build cumulative bands, positives above the baseline and negatives below it. StackOffset::Normalize is the 100 % stack, Center the streamgraph silhouette, and StackOrder::Sum piles the largest series first.

dodge is side-by-side positions for grouped bars, one series per value series, fed to Bars::at — stack's sibling, for bars beside each other.

Fit is streaming least squares behind the trend preset. nearest is the crosshair-snapping lookup: the index of the closest finite value, so cursor readouts show a datum that exists rather than an interpolation.

Online accumulator#

An online accumulator is a bounded state updated one observation at a time. Some accumulators also merge partial states. stat::Moments and stat::Fit use order-independent summary state. stat::Bins requires identical geometry. stat::M4 requires chunks in series order, because gaps and first/last points are path topology. Each type states its own identity, merge preconditions, and ordering requirement. Merge results are understood over a fixed reduction tree, not as bitwise-independent reassociation.

Reducer#

Reducer on docs.rs.

stat::binned with Reducer::Mean accuracy per bin ── perfect calibration 1.00 ┤ a │ c 0.75 ┤ c │ u │ r 0.50 ┤ a │ c │ y 0.25 ┤ │ 0.00 ┤ └┬──────────┬──────────┬──────────┬──────────┬──────────┬ 0.0 0.2 0.4 0.6 0.8 1.0 confidence
Plate 7. binned with Reducer::Mean builds a reliability diagram from 0/1 outcomes.

A reducer is a named aggregation shared by every aggregating stat: Count, Sum, Mean, Median, Min, Max, Percentile(q) (type-7, the estimator the box plot's quartiles use), Deviation, Variance, StdErr (sample statistics, n − 1, gaps below two values — the error-band vocabulary of Vega-Lite and seaborn's Est), First, and Last. That is one vocabulary across bins, groups, and windows — the Observable Plot convention — so a rolling p95, a binned median, or a group's mean ± se is one call. A reducer promises a result for one collection, not a public merge operation. Maps to stat::Reducer.

Batch transform#

A batch transform is an operation that consumes a complete ordered collection and emits another collection or a structured result: Window, kde, ecdf, roc, auc, ewma, lttb, cumsum, diff, rank, normalize, contours, stacking, bins2, and BoxStats. A batch transform may use online accumulators internally. That does not make the transform itself mergeable.

Scale#

Scale on docs.rs.

a power law, rank against frequency │ │ 10⁴ ┤ │ 10³ ┤ │ 10² ┤ │ 10 ┤ │ │ 1 ┤ │ └──┬────────────────┬───────────────┬────────────────┬────── 1 10 10² 10³
Plate 8. log_x and log_y tick by decades, with superscript labels.

A scale is a mapping from a data domain to a raster range, with the d3-scale contract: nice, ticks(n), invert, and a tick formatter. Position scales are Linear, Log, Time, and Band. The axis specification is scale::Scale (Linear | Integer | Log | Time | Bands), set via Plot::x_scale/y_scale. Integer is a linear axis whose tick step never drops below one — counts, ranks, sizes — so a tall frame over 0..3 labels 0, 1, 2, 3, never 0.5. hist counts on it. Bands works on either axis: on x it is the bar-family categorical axis, and on y it labels matrix rows in matrix order.

A linear or integer axis may also carry a unit, scale::Unit, set via Plot::x_unit/y_unit. Unit::si("B") puts the axis's one SI prefix before the unit (2.5 MB, 0 kB). Unit::Bytes chooses ticks nice in the binary unit (512 KiB, 1.5 GiB). Unit::suffix("%") appends a bare suffix and never a prefix (45%). The unit is a scale option: the ticks stay the same ticks, only the labels change, and the Mapping readout speaks the same unit.

A domain is a scale option too. Plot::x_domain/y_domain fix both ends of an axis exactly, and x_min/x_max/y_min/y_max fix one end while the other fits the data and grows to its outer tick — a rate chart floored at zero whose top follows the traffic.

scale::Colormap covers sequential and diverging ramps. The curated named constants are VIRIDIS, MAGMA, CIVIDIS, GREYS, RED_BLUE, and PURPLE_ORANGE. centered_at(mid) anchors a diverging map to a data value. log() makes a sequential map logarithmic, with decade ticks. domain(lo, hi) fixes the value range so two grids read on one scale, with under/over disclosing what falls outside instead of clamping it. steps(n) and thresholds(values) quantize the ramp into bands the colorbar draws and labels at their boundaries. contourf is heatmap under a map split at contour's levels.

Stops mix in OKLab, so the color halfway between two stops looks halfway — no grey between blue and yellow. The 16-color tier picks by OKLab lightness and hue, the two things sixteen colors can carry.

scale::Palette is the categorical scale color_by draws from. Okabe–Ito (Wong 2011) is the default, with Paul Tol's BRIGHT and MUTED (nine colors) beside it.

Ticks#

Ticks on docs.rs.

a context note for the digits the labels share +1.000G s 40 ┤ a │ m 20 ┤ p │ l 0 ┤ e -20 ┤ s │ -40 ┤ └┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬ 0 10 20 30 40 50 60 70 80
Plate 9. The context note prints a base once, and the labels read relative to it.

Ticks are the axis values a scale chooses to label, placed by the extended Wilkinson algorithm (Talbot, Lin, Hanrahan 2010) — scored for simplicity, coverage, density, and legibility. Ticks are computed, never supplied as strings. They carry exact-decimal labels: a label parses back to its value, labels share one fraction width and one SI prefix per axis (one power of ten beyond the prefix table, 8.796e-100), and they never show float artifacts.

A range no nice step can cover — equal bounds, a span past the exact mantissa — falls back to its two bounds, formatted by the same formatter at its budget. What the labels leave out, an axis prints once as a context note (Ticks::context). Values agreeing in four or more leading digits read relative to a round base, printed above the y labels or at the end of the x title row (+1.000G, matplotlib's offset text). A time axis whose first label omits its date or year prints that part (Aug 1 2026 under hour labels, 2026 under day or month labels — Bokeh's context). This is an automatic layout rule, not an option: the y note is shed like the legend, the x note with the title row, and a colorbar never offsets.

Maps to scale::Ticks. scale::TickOptions — a Unit, an integer flag, and the context flag the axes set — is how an axis's scale options reach the tick search, through Ticks::linear_with. scale::NumberFormat makes the same label decisions once, for an arbitrary set of related values — one fraction width, one SI prefix or power of ten, whole labels for whole-number sets, gaps as — — the per-column formatter behind table, usable for any readout. There is no second, cheaper formatter anywhere. The one value written outside the set's resolution is the one that resolution would misstate — rounded to zero, or to a single inexact digit — which keeps its own, so a column of gigabytes never reads a mean of a thousand as 0. See The axes are the product.

Frame#

Frame on docs.rs.

60×12

training ── loss ── target 4 ┤ ───╮ │ ╰──╮ │ ╰────╮ 2 ┤ ╰─────╮ │ ╰───────╮ │ ╰──────────────────────────── 0 ┤ └┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬ 0 1 2 3 4 5 6 7 8 epoch

36×8

training ── loss ── target 4 ┤ ─────╮ │ ╰─────────╮ 0 ┤ ╰────────────── └┬─────────┬─────────┬─────────┬── 0.0 2.5 5.0 7.5 epoch
Plate 10. One plot, two frames.

A frame is where and how to render: width and height in cells, the charset, the color mode, and the theme. A frame is run state, not plot state — the same Plot renders into many frames. Frame::detect() is the convenience that inspects the environment. Plot::render with an explicit frame inspects nothing. Frame::plain() is the deterministic braille snapshot form. Frame::portable() is the conservative Unicode form. Maps to plot::Frame and plot::ColorMode. See The frame is run state.

Mapping#

Mapping on docs.rs.

A mapping is the resolved geometry of one render, as a queryable value: the plot rectangle in cells, the resolved axis windows, and the cell ↔ data mapping both ways. That both-ways mapping is the invert half of the scale contract, reachable at plot level. A mapping is obtained purely from Plot::mapping(&frame). The ratatui stateful widget caches one per render.

Queries answer in the coordinate conventions marks use (band indices, unix seconds). They name the plot panel as a Panel value rather than a bare tuple. They expose a categorical axis's labels (x_categories), disclose cell quantization (x_span_at/y_span_at), and format values the way the axis formats its own labels (format_x/format_y). They answer the x-only half of the invert (column_at) — what lets linked panes mirror one cursor whatever their gutters.

A mapping is derived state, deliberately not serializable. This is the physics interactive hosts build on. malevich never handles input — a host maps its events to questions a Mapping can answer. Maps to plot::Mapping.

Viewport#

Viewport on docs.rs.

100,000 points, the whole window 5.0 ┤ │ 2.5 ┤ │ │ 0.0 ┤ │ -2.5 ┤ │ └┬────────────┬────────────┬────────────┬────────────┬────────────┬ 0 20k 40k 60k 80k 100k the same plot, zoomed 5.0 ┤ │ 2.5 ┤ │ │ 0.0 ┤ │ -2.5 ┤ │ └┬──────────┬──────────┬──────────┬─────────┬──────────┬──────────┬ 41.00k 41.50k 42.00k 42.50k 43.00k 43.50k 44.00k
Plate 11. This is the whole window of a 100,000-point series. A Viewport over the same plot makes M4 re-aggregate to the new columns.

A viewport is an axis window pair for interactive viewing. Zoom and pan are pure domain arithmetic over x_domain/y_domain — a scale option, never a render mode, which is why zooming into millions of points re-aggregates (M4) to the new window with no special machinery. None on an axis means automatic. A window is seeded from Mapping::viewport ("the view I am looking at") and transformed by value: zoom around an anchor (decade space on log axes), pan by a fraction, clamp to an extent, tail for follow-the-stream, and reset to automatic. The viewport is applied with Plot::viewport. It is serializable — spec-shaped state a host may persist. Maps to plot::Viewport.

Widget#

PlotWidget on docs.rs.

Widgets are the TUI adapters.

In Rust the feature is ratatui, and it depends only on ratatui-core. Plot::widget() renders any plot into a Buffer as cells and styles. The stateless Widget impl is fire-and-forget. The StatefulWidget impl threads a PlotState — the interaction controller. It caches the render's Mapping for hit-testing, applies its Viewport on the next draw, and interprets the default mouse gestures from the backend-neutral Mouse vocabulary the host feeds it: hover crosshair, wheel zoom at the cursor, left-drag pan, and right-drag rubber-band zoom.

In JavaScript the same split lives in malevich/ink. PlotWidget paints a Raster as Ink Text cells. PlotState is the same controller, overlays and all. usePlotInteraction is an optional composition that enables mouse tracking.

The cursor snaps to the data. For every point-backed line and points layer, the readout lists the value of the datum nearest the cursor's x inside the visible window — axis-formatted, its cell highlighted, a gap shown as — rather than an interpolation. snap(false) returns to plain cursor coordinates.

A linked pane mirrors another pane's cursor by data x (PlotState::hover_x / hoverX): a vertical-only crosshair at its own column — an x but no honest row — with snapping and the readout working as they do under a real cursor.

The widget never reads the terminal. Event loops, mouse capture, and key policy stay in the host, and the gestures are a preset over the public physics — a host with different policy drives Viewport and Mapping directly.

Interaction chrome (crosshair, snap highlights, selection band, readout) draws into the buffer (or the raster) only. The plot value renders byte-identically with or without it.

With the pixel feature, widget().graphics(g) renders the panel as a real image. The buffer holds skip-reserved ground, the PlotState carries the encoded block, and the host emits it after terminal.draw with Graphics::present — one synchronized write, new placements created before old ones are retired. Graphics::retire deletes a view's images on the way out. Interaction chrome then becomes annotation marks drawn into the image, and hit-testing is unchanged — the mapping answers in cells regardless of what fills them.

The pixel render paces itself (~30 full encodes per second): within the window, an unchanged-view render reuses the image already on screen, so hover floods and tick redraws cost nearly nothing. A changed viewport or rectangle always renders.

Surface#

A surface is the subpixel grid that marks draw on during rasterization, before glyphs exist. The raster convention is origin top-left, y down; the data-space flip happens in the scales. A charset codec maps each cell's subpixel pattern to a glyph with independent foreground and background colors. Text shares the grid and wins over pixels. Drawing is infallible: out-of-surface clips, non-finite coordinates draw nothing, and control characters are dropped at the cell grid, so no input string can smuggle escape bytes into any encoder's output. Maps to render::Surface.

Raster#

A raster is the encoded cell grid of one render: glyphs and colors, chrome included, as a plain value. Marks draw on a Surface in subpixels; a charset codec maps each cell to a glyph; a raster is that snapshot. TUI hosts (ratatui, Ink) paint it into their own buffer instead of decoding an ANSI string. An ANSI round-trip loses cell identity: wide glyphs, independent fg/bg, and the continuation cell. Plot::render is rasterize-then-encode. Plot::raster stops after rasterize. The encoders are the second half — Raster::encode (glyphs and SGR for a tty), to_plain, to_html (the card a notebook draws), to_svg (the card an SVG host draws) — so every kind of terminal shares one grid. Continuation cells (columns == 0) sit to the right of a wide glyph; encoders skip them. Maps to render::Raster. The membership test: a second host demanded cells, and no composition of the public string renderer reproduces per-cell style.

Card#

Plot::to_svg on docs.rs.

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 12. 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.

A card is the cell grid encoded for a host that draws with markup rather than escape codes: the HTML card (Plot::to_html, a <pre> of colored spans) and the SVG card (Plot::to_svg, rectangles for block glyphs, text runs for everything else). A card is a picture of the raster and nothing more — the same grid a tty would print, with mark colors resolved to concrete RGB and default-colored chrome taking the card's foreground, on the card background the theme selects. Text is never rasterized: the host's font draws it, the offload the string render makes to the terminal. Both cards need no feature; the evcxr feature adds only the notebook protocol around them. A card that drew something the terminal would not draw would be a figure, and a figure is a different product.

With the pixel feature, Plot::to_svg_pixels encodes the hybrid render for the same kind of host. Chrome is still this card. The plot panel is the device-pixel raster the terminal would draw, as rectangles, not a second drawing from the data and not an embedded image. Plot::to_svg stays the cell card. A text-only plot, a zero cell size, or a frame with no panel degrades to it. In-panel text marks use the pixel font, as they do in render_pixels; axis text stays the host's font.

Charset#

Charset on docs.rs.

Octants — 2×4 blocks, Unicode 16

7.5 ┤ │ │ 5.0 ┤ │ 2.5 ┤ │ 0.0 ┤ └┬─────────────┬─────────────┬─────────────┬ 0 1 2 3

Sextants — 2×3 blocks, Unicode 13

7.5 ┤ │ │ 5.0 ┤ │ 2.5 ┤ │ 0.0 ┤ └┬─────────────┬─────────────┬─────────────┬ 0 1 2 3

Braille — 2×4 dots

7.5 ┤ ⢀⠤⠊ │ ⣀⠔⠁ │ ⢀⡠⠊ 5.0 ┤ ⣀⠤⠒⠉⠒⠤⢄⣀ ⢀⠔⠁ │ ⣀⠤⠒⠉ ⠉⠒⠢⠤⣀ ⡠⠊⠁ 2.5 ┤ ⢀⡠⠔⠊ ⠉⠑⠒⠤⣀⠔⠊ │⡠⠔⠊⠁ 0.0 ┤ └┬─────────────┬─────────────┬─────────────┬ 0 1 2 3

Quadrants — 2×2 blocks, the UTF-8 default

7.5 ┤ │ │ 5.0 ┤ │ 2.5 ┤ │ 0.0 ┤ └┬─────────────┬─────────────┬─────────────┬ 0 1 2 3

Half blocks — 1×2

7.5 ┤ │ │ 5.0 ┤ │ 2.5 ┤ │ 0.0 ┤ └┬─────────────┬─────────────┬─────────────┬ 0 1 2 3

ASCII — 1×1, the guaranteed fallback

7.5 + ** | *** | *** 5.0 + ****** ** | ***** ******* *** 2.5 + **** ***** |*** 0.0 + ++-------------+-------------+-------------+ 0 1 2 3
Plate 13. The first call is malevich::line over four values.

A charset is a glyph tier used to encode the surface. Glyph tables are data, not code. Maps to render::Charset: Ascii, HalfBlocks, Quadrants, Sextants (Unicode 13), Octants (Unicode 16), and Braille. Each tier also owns the shade ramp colorless patch output draws with — ░▒▓█, or .:#@ on Ascii — so a heatmap, a class region, a colorbar, and their legend swatches never carry a glyph the tier cannot show. Frame::detect sniffs the environment, never probes. Dense tiers are explicit, because a terminal name cannot establish the configured font's coverage. See Degradation is the contract.

Canvas#

A canvas is the drawing-target contract marks rasterize through, generic over fidelity: the cell Surface fills with eighth-block ramps and glyph textures, the pixel canvas with exact rectangles and real pixels — same mark code, monomorphized per target. The canvas is crate-private. Maps to render::Canvas.

Graphics#

Graphics is how to draw the plot panel as a real image (feature pixel): which protocol, at what cell size in device pixels. It is render state like Frame, and a plain value like everything else. None means the caller falls back to cells. Output stays hybrid: chrome as text, only the plot rectangle as pixels. Plot::to_svg_pixels is that hybrid for an SVG host. It reads cell size and stroke. The protocol is terminal transport and does not change the SVG. economical() is the slow-link trade: halve a Retina density, keep the ink weight, a quarter of the bytes. Sixel, with no placement scaling, stays native. Maps to pixel::Graphics. See the pixels guide.

Capabilities#

Capabilities are what the terminal can do, as a plain queryable value: the protocols it accepts, its cell size in device pixels, and how the answer was obtained (Source::Probed, Source::Sniffed, or Source::Declared). Sniffing reads environment variables — free, and wrong only by omission. Probing asks the terminal itself over one raw-mode round trip — ground truth, and only where writing escapes is safe. An unanswered probe is not evidence; it degrades to the sniff answer. A host that already knows its terminal declares the value through Capabilities::new and skips detection. Maps to pixel::Capabilities and pixel::Source.

Protocol#

A protocol is a terminal image protocol the panel can be emitted in: Sixel (DEC 1987, the most widely spoken), Kitty (raw RGBA with alpha, the most capable), and ITerm2 (an inline PNG). Encoders are hand-rolled and dependency-free. Maps to pixel::Protocol.

Theme#

Theme on docs.rs.

Palette::OKABE_ITO •• a •• b •• c •• d •• e •• f •• g •• h 10 ┤ │ │ │ 5 ┤ │ │ 0 ┤ └┬──────┬──────┬──────┬──────┬─────┬──────┬──────┬──────┬ 0 1 2 3 4 5 6 7 8 Palette::MUTED •• a •• b •• c •• d •• e •• f •• g •• h 10 ┤ │ │ │ 5 ┤ │ │ 0 ┤ └┬──────┬──────┬──────┬──────┬─────┬──────┬──────┬──────┬ 0 1 2 3 4 5 6 7 8
Plate 14. The categorical Palette lives in the spec. The Theme belongs to the frame.

A theme is colors and styles as a value you pass, never a global. Today it is the layer palette, with dark and light variants and COLORFGBG detection. Maps to Theme (a field of Frame). It is deliberately distinct from the categorical scale::Palette, which lives in the spec. Layer colors are presentation, and a frame adapts them to its background. Category → color assignments are closer to an encoding — they travel with a serialized spec so its legend keeps meaning wherever it renders. Two palettes, two homes, one recorded reason.

Grid#

Grid on docs.rs.

A grid is small multiples: independently rendered plots pasted side by side (escape-aware padding), cells filled left to right. Axis sharing is a composition — fix domains with Plot::x_domain/y_domain — never a hidden mode. Maps to plot::Grid (re-exported at the root).

Preset#

line on docs.rs.

│ 21 ┤ 20 ┤ │ 19 ┤ │ 18 ┤ │ 17 ┤ │ 16 ┤ │ 15 ┤ 14 ┤ │ 13 ┤ └┬──────────┬──────────┬───────────┬──────────┬──────────┬──────────┬ 30 35 40 45 50 55 60
Plate 15. scatter(x, y) is the first step through the front door: byte-identical to the one-layer plot.

A preset is a plain function composing the grammar into a named chart type: line(), hist(), scatter(), sparkline(), table(), describe() (with describe_with adding an inline histogram column), and the rest. It is the short spelling of that composition, and a test checks the two print the same bytes. Presets are the front door. The grammar can wait until you need it. _with means "configured with an options value". A try_ prefix identifies the checked twin of an otherwise identical convenience. Maps to functions and option types re-exported at the crate root. See Presets are packaging.

Stream#

stream on docs.rs.

Viewport::tail — the last 200 of 1,000 30 ┤ │ 25 ┤ │ │ 20 ┤ │ 15 ┤ │ └┬────────┬───────┬───────┬────────┬───────┬────────┬───────┬──────── 800 825 850 875 900 925 950 975 1000
Plate 16. Viewport::tail is the window that follows the stream.

A stream is the live data machinery, kept at the edge of the crate. stream::Ring is a sliding window shared across threads — the one lock in the library — or, from Ring::growing, a window that keeps every value since the start. stream::Rate turns counters into deltas. stream::Live is an in-place repaint: cursor up, erase down, one buffered write bracketed as a synchronized-output frame — flicker-free, scrollback-safe, never owning the screen. Live::detect repaints only on a terminal and appends plain frames to a pipe or file, so no escape byte lands where it is not safe. The core stays pure. Time enters only at the rims — this module, and the ratatui widget's pixel pacing (below), which uses a monotonic clock to skip redundant re-encodes. Every render that does run remains a pure function of its inputs.

Gap#

A gap is missing data, encoded as NaN in a series and rendered as a visible break — never interpolated across, never dropped silently. This is the de-facto convention of the terminal plotting field.

Further reading#

  • Wilkinson, The Grammar of Graphics (2005).
  • Talbot, Lin, Hanrahan, "An Extension of Wilkinson's Algorithm for Positioning Tick Labels on Axes" (InfoVis 2010).
  • Jugel, Fischer, Mahlmann, Markl, "M4: A Visualization-Oriented Time Series Data Aggregation" (PVLDB 2014).
  • Hyndman and Fan, "Sample Quantiles in Statistical Packages" (1996) — the type-7 estimator.
  • Wong, "Points of view: Color blindness" (Nature Methods 2011) — the Okabe–Ito palette.
  • d3-scale, Vega-Lite, and Observable Plot — the conventions the vocabulary follows.