Reference
Changelog
Every release, written for a person.
Notable changes, written so a person can read them. Since 1.0, a breaking change means a major release. The pre-1.0 entries below recorded breakage freely, without apology.
Unreleased#
1.24.1 — 2026-10-03#
A fix to the pixel card that 1.24.0 introduced.
- The pixel card's panel resamples smoothly. It inherited the card's
crispEdges, which snaps every device-pixel run to the screen grid, so wherever a device pixel is not one screen pixel — Retina-density cells on a 1× screen — anti-aliased lines came out stepped. The panel now sits in ageometricPrecisiongroup, and the chrome stays crisp. One path per color keeps fills free of seams.
1.24.0 (Black Circle) — 2026-10-03#
A card can carry real pixels. A page that draws with SVG gets the hybrid a graphics terminal gets: the cell card for chrome, the device-pixel raster for the panel, with no image and no new dependency. The gallery shows every example as its card, and the site checks its own links into the API.
Plot::to_svg_pixelsandPlot::try_to_svg_pixels(featurepixel) encode the hybrid pixel render as an SVG card. Chrome stays the cell card. The panel is the device-pixel raster a graphics terminal would show, one path of rectangles per color, with no embedded image and no new dependency. Cell size and stroke shape it; the graphics protocol does not. A plot with no panel to draw getsPlot::to_svg, which is unchanged. The site's pixels guide shows both cards.- The gallery examples take
--svgand print the dark quadrant card of the same plot values, in the same frames, as their pipe text; a grid prints one card per pane at the size the grid gives it.cargo run --example regen_docswrites them toexamples/cards/and checks them likeEXAMPLES.md, and the site's gallery shows the card with the pipe text one switch away. The README's SVG figure lives there too.regen_docsbuilds every example once, in parallel, and runs the binaries directly. - The site has a masthead with its four sections, a front page that leads with what the library draws, and a "Which chart" page from a shape of data to its preset. The principles are reached through Vision. The fonts are served with the site, so a page makes no third-party request. Pages carry social preview tags, search results land on their heading, and each concept links to its item on docs.rs. The build checks those links against a local rustdoc, and reads the crate version, minimum Rust, and dependencies from the manifest.
1.23.1 — 2026-10-02#
A documentation release: no library code changed since 1.23.0. The README and the crate manifest point at the documentation site.
- The documentation site at https://shergin.github.io/malevich/ is
generated by a workspace member,
site/(cargo run -p malevich-site): a guide with a plate for every mark, stat, scale, and piece of furniture, the gallery with its sources, the principles and the terminology contract illustrated, a playground, and the browser figures the oldgallery/held. Every figure is a plot insite/figures/*.rs, rendered by the library at build time — the code shown beside a plate is the block that ran — and the repository's own docs are the text of most pages, with plates inserted under their headings.gallery/is folded into it. - The benchmark record has a current-state table and a second architecture.
BENCHMARKS.mdopens with every row at its latest measurement on each machine that has one, ahead of the dated history, and the recording protocol is written down in one place. The new entry is the whole bench suite on an x86_64 Linux machine: the ten rows the M1 Pro record carries, and the seventeen the suite always had but no record named. Those are the scatter, M4 on its own, histogram binning, the KDE, ANSI encoding, the layout sweep, the streaming frame, the plot clone, tick placement, and the three pixel encoders.docs/performance.mdquotes the 1.23.0 numbers it had drifted from and shows both machines side by side. cargo run --example bench_recordprints a record block from Criterion's saved results: revision, machine, OS, compiler, sample count, and one row per benchmark with Criterion's own estimate and 95% interval, in its units and rounding, plus the change against the previous run when it kept one. A benchmark table is program output, like every chart in the docs.- The allocation contract is measured again at 1.23.0 on the CI compiler and on current stable, beside the 1.17.0 revision it recorded before. The counts reproduce across machines exactly. The code between the two releases added 40 allocations to the 10k line render, and a bisect names the commit: the per-label tick formatting that units introduced. The headroom under the CI ceiling is known and attributed, not assumed.
1.23.0 (Taking in the Rye) — 2026-09-24#
Nothing the field ships needed a ninth mark or a fifth scale. This release harvests what a survey of a hundred-odd terminal plotters, chart widgets, and grammars agreed on, as options inside the closed grammar: normalized and cumulative histograms, one-sided domains, units and whole-number axes, the note an axis prints for what its labels leave out, colormap domains and bands, color math in OKLab, interval bars and calendar bins, tick fallbacks that stay exact, and a CLI that catches up. Every preset still equals its expansion byte for byte.
-
The ASCII tier is ASCII everywhere. Heatmap cells, class regions, the colorbar strip, and the class legend swatches drew the Block Elements shades (
░▒▓█) onCharset::Ascii; they now draw the tier's own density ramp (.:#@), so a chart onTERM=dumbnever carries a byte its terminal cannot show. -
A dark grey in 256-color output no longer panics.
Color::Rgbgreys between 4 and 7 — an image's near-black pixels, a colormap's low end — underflowed the grey-ramp quantizer (an overflow panic in debug builds, a red cell in release). Every grey level now lands on its nearest ramp entry. -
Cells::reducechecks its percentile at construction, like every other builder, and rendering a spec whose percentile has no position (only deserialization can retain one) sheds the layer instead of asserting mid-draw —Plot::rendernever panics, as promised.Window::reduceandstat::binneddocument the same panicReducer::reducealways had. -
A stat table never reads a small number as zero.
NumberFormatformats a value the column's resolution would misstate — one it would round to zero or to a single inexact digit — at the value's own resolution: adescribeover a loss near one and a byte count near a billion prints the loss's mean as1.000, not0. Alignment yields for that value alone; every other value keeps the column's decimals and prefix. -
Capabilities::new(protocols, cell_size)builds the plain value a host that already knows its terminal was promised — the struct is#[non_exhaustive], so no literal could — withSource::Declarednaming how the answer was obtained. -
A
Rulespan that starts at or below zero on a log axis washes its visible part, from the axis floor up to its top, and grows the axis to that top; it used to vanish whole, because zero has no logarithmic position. -
An annotation's promoted background on a two-sample heatmap cell blends the halves in OKLab, like every other mix in the crate.
-
HistogramOptions::normalizationandcumulativeare#[must_use], like every other builder. -
The README's SVG figure is a
<picture>pair, dark and light cards both regenerated by the doc generator;docs/notebooks.mdstates the terminal-card contract (the grid is the chart, nothing external, stripped styles still read, plain text is agent-legible);docs/recipes.mdanswers the requests the field files most — benchmarks throughjq, the pie as a waffle (thewafflegallery example) or a breakdown, tornado bars, aGridwith a shared window instead of twin axes, and the clip-versus- squish rule for out-of-range positions and colors; the README's refusals name each with its reason. -
ndarraysupport moves to 0.17. -
stream::Ring::growing()is the window that keeps every value pushed — the chart of a run from its start, where the x axis lengthens instead of sliding.kaz --live --window 0uses it. -
Detection reads what the field agreed on.
FORCE_COLOR(non-empty, not0) forces color likeCLICOLOR_FORCE; aTERMending in-directis truecolor;TERM=unknownis as dumb asdumb; ascreen*TERMcaps color at 256;COLUMNSandLINESsize a render with no terminal to measure.MALEVICH_GRAPHICS=kitty|sixel|iterm2|nonenames the pixel protocol, outranking the sniff and skipping the probe, likeMALEVICH_CHARSETdoes for glyphs; the sniff table learnsLC_TERMINAL=iTerm2(which ssh forwards),WEZTERM_EXECUTABLE,GHOSTTY_BIN_DIR,KITTY_PID,MLTERM, Rio, and Warp. The iTerm2 payload carriesdoNotMoveCursor=1, so an image ending at the screen's bottom row no longer scrolls before the cursor is restored. -
Bars::intervals(starts, ends, values)is the fourth placement: every bar between its own two edges on a continuous axis — the histogram with irregular bins, sideways a Gantt-style row. A gap in either edge skips the bar; intervals draw whole, exempt from the per-column thinning dense uniform bars get.stat::calendar_bins(values, TimeUnit)counts unix timestamps per hour, day, ISO week, month, or year over their extent, empty buckets kept, months of their true length;CalendarBins::heightsnormalizes likeBins::heights, a density by each bucket's own length. Thecalendargallery example draws commits per month. -
Color math runs in OKLab: colormap stops interpolate perceptually, so the halfway color between two stops looks halfway and a ramp between two saturated stops no longer dips through grey; the 16-color tier picks by OKLab lightness and hue — the two things sixteen colors can carry — so a teal drops to green, never to the grey that is nearer in RGB. The named maps' stops are unchanged, so their ends render as before. The perceptual mix costs a gamma encode per sampled heatmap cell: the
widget/dashboard_200x50benchmark row is a third higher, recorded inBENCHMARKS.md. Paul Tol'sPalette::BRIGHT(seven colors) andPalette::MUTED(nine) join Okabe–Ito. -
Colormap::domain(lo, hi)fixes the value range a colormap spans instead of the data's extent — two heatmaps on one scale, a live grid whose colors hold still — and the colorbar shows it.under(color)andover(color)disclose values outside the range in their own colors rather than clamping them into the ends.steps(n)quantizes the ramp into equal bands andthresholds(values)splits it at given values; the colorbar draws the bands and labels their boundaries.Colormap::sampleapplies all of it at once.contourfandcontourf_withare filled contours as a composition:contour's levels,heatmap's drawing, a colormap thresholded between them — thecontourfgallery example fills the peaks function. A plain colormap's wire form is unchanged. -
Axes print what their tick labels leave out, once. A numeric axis whose values agree in four or more leading digits places its ticks on the residuals around a round base and prints the base above the y labels or at the right end of the x title row (
+1.000G) — matplotlib's offset text, so a near-constant sensor trace or a unix-seconds axis reads as0.001, 0.002instead of twelve-digit labels or two endpoints. A time axis whose first label omits its date or year prints that part in the same place (Aug 1 2026under hour labels,2026under day or month labels) — theConciseDateFormatteroffset and Bokeh'scontext. An automatic layout rule: the y note takes one row above the plot and is shed like the legend, the x note shares the title row and is shed with it, and colorbars never offset.Ticks::contextexposes the note;TickOptions::contextturns the numeric base on for callers placing their own ticks. Theintradaygallery example shows the day once. -
Plot::x_min,x_max,y_min,y_maxfix one end of an axis and fit the other: the fixed end is honored exactly, the free end grows to its outer tick as an automatic axis does — a rate chart floored at zero whose top follows the data, matplotlib'sylim(bottom=0). Two calls compose into the two-sided domain; a floor above the data clips everything rather than running backwards; bars keep their baseline in view when only the top is fixed. On the wire a two-ended domain is the[min, max]pair it always was, a one-ended one{"min": v}or{"max": v}, so v1 documents are byte-stable. sysmon floors its network chart at zero. -
HistogramOptions::normalization(Normalization)and.cumulative(bool)rescale a histogram's bars: counts (the default, on theIntegeraxis), probability, percent (the axis reads%), or density per unit of x, the scale akdeline shares;cumulativeaccumulates the bins so the last bar carries the total.stat::Bins::heights(normalization, cumulative)is the one function behind it, sokaz hist --normalize percent --cumulativeand the preset cannot disagree — a cumulative density is the distribution function ending at one, as matplotlib defines it. Thecumulativegallery example reads the share of requests served within a latency off the axis. -
Scale::Integeris a linear axis whose tick step never drops below one: counts, ranks, sizes. A tall frame over0..3labels0, 1, 2, 3, never0.5.hist,kaz hist, andkaz countcount on it; the packaging witness now names it in the expansion. -
Plot::x_unit(Unit)/Plot::y_unit(Unit)put a unit on a linear or integer axis's labels:Unit::si("B")places the axis's one SI prefix before the unit (2.5 MB,100 µs,0 kB),Unit::Byteschooses ticks nice in the binary unit and labels512 KiB,1.5 GiB,Unit::suffix("%")appends a bare suffix and never a prefix. The ticks stay the extended Wilkinson ticks — only the labels change — and theMappingreadout speaks the same unit.Ticks::linear_with(min, max, target, &TickOptions)exposes both options to callers who place their own ticks. sysmon labels memory in bytes and throughput inB/s. -
describe_with(names, groups, DescribeOptions::new().histogram(bins))adds a ninth column to the summary table: each group's distribution asbinseighth-block glyphs scaled to its fullest bin, empty bins blank — the shape beside the numbers, as skimr prints it. The defaults reproducedescribeexactly; the gallery'sdescribeexample shows the column. -
Plot::axes(false)omits the axes — no axis lines, ticks, tick labels, or gutter; the data fills the frame and the domains are the data's own extent.sparkline(values)is the preset over it: bars from zero, one per value, eighth-block heights in a single row, gaps blank, a series of zeros blank, proven byte-identical to its expansion.kaz sparkrenders it one row tall from any column of numbers. -
Bar ends map to subpixel edges instead of subpixel centers on cell targets, so a bar of
vunits over av-row axis fills exactlyvrows and the maximum reaches the panel's edge. At ten rows the difference is a sixteenth of a cell in the partial glyph; at one row it was half the sparkline. -
Bars denser than the raster's cell columns thin to one per column — the bar farthest from the baseline keeps its value, the rest become gaps — so a spike among a thousand bars survives instead of the last bar drawn overprinting the others: the bars' answer to M4. Stacks, grouped categories, bands, and sideways bars draw whole.
-
Rule::v_span(x0, x1)andRule::h_span(y0, y1)wash the band between two values across the whole plot in the rule's color — recessions, warm-up phases, tolerance windows, the annotation matplotlib callsaxvspanand Bokeh aBoxAnnotation. On cell targets the wash is a light checkerboard of subpixels that marks drawn after it show through; on pixel targets it is a quarter-opacity fill. Spans extend the axis to include them, legend as a shade swatch, and serialize asVerticalSpan/HorizontalSpanbeside the unchanged line forms. fred's recession shading becomes one span per recession behind the data, with no carved-out strip and no exception for log axes; theannotatedexample washes its warm-up. -
BoxStats::of_with(values, Whiskers)andbox_plot_with(…, BoxOptions)choose the whisker rule: Tukey'sk × IQRat any reach, two type-7 percentiles (Percentiles(0.05, 0.95)), or the full range with no outliers. The quartiles stay the one type-7 estimate whatever the rule, and the defaults reproducebox_plotexactly. -
stat::jitter(positions, width)spreads coincident points with a van der Corput offset — an evenly filled strip that renders the same every time, with no seed to forget — the "rain" beside a box or a violin. Theraincloudgallery example composes cloud, box, and rain from the grammar. -
stat::steps(x, y, StepDirection::{Post, Pre, Mid})is the step expansionstairsused to inline, as a stat: hold each value and change it after, before, or midway between samples; feed it toArea::xyfor a filled step chart.stairs_with(values, StairsOptions)chooses the direction;stairsis proven equal to thePostexpansion. -
stat::kde_with(values, points, KdeOptions)chooses the bandwidth (Bandwidth::Silverman,Scale(factor),Fixed(width)), bounds the support — kernels reflect at a bound, so a latency density keeps its mass above zero and a fraction stays inside[0, 1]— sets the padding in bandwidths (cut), and evaluates the cumulative density.DensityOptionsandViolinOptionscarry akdefield for the same choices; the defaults reproducekde,density, andviolinexactly. Thelatencygallery example shows the bounded estimate beside the leaking one. -
Window::anchor(WindowAnchor::{Start, Middle, End})places the window ahead of, around, or behind each position — a centered moving average sits on the data instead of trailing it by half a window — andWindow::strict()answers only complete windows, a gap elsewhere. The default stays the trailing, partial-window form, byte-identical for every existing caller.stat::cumsum,diff,rank, andnormalize(values, basis: Reducer)are the series maps besideewma: cumulative totals, day-over-day deltas, ranks, and index-to-first or percent-of-peak lines through the one reducer vocabulary. Thebollingergallery example draws a centered window's mean ± 2σ from these parts. -
stat::stack_with(series, StackOptions)chooses a stack's offset and order:StackOffset::Normalizescales every position's bands to fill[0, 1](the 100 % stack — breakdown bars, share-of-total areas),Centershifts each position so its stack straddles zero (a streamgraph's silhouette), andStackOrder::Sumpiles the largest series at the baseline. Bands come back in input order whatever the stacking order.stackitself now stacks negative values below the baseline on their own side, the way every grammar does, instead of running them through the positive total; a position whose total is zero normalizes to zero-height bands, never toNaN. Thebreakdowngallery example draws regional electricity shares as horizontal 100 % stacks. -
ReducergainsDeviation,Variance,StdErr(sample statistics,n − 1in the denominator, a gap below two values) andFirst,Last— inherited at once byWindow,Agg,binned, andCells::reduce, so a Bollinger band is a rolling mean and a rolling deviation, and a group's mean ± se is two reductions in the one vocabulary.Momentsexposes the same assample_varianceandsample_standard_deviation. -
describe'ssdcolumn is now the sample standard deviation — what pandas and R print — and its rustdoc says so; the population form stays onMoments::standard_deviation. -
Bins::autocomputes its Freedman–Diaconis quartiles with the crate's one type-7 estimator (selected in O(n), not sorted) — raw order statistics read the IQR of four values 2.7× too wide — and treats whole-number data as whole: a whole nice width of at least 1 and edges on half-integers beside the nice start, so every bin holds the same number of consecutive integers, no bin straddles two, and the maximum never shares the last bin with its neighbor (1..=50is five bins of ten, not9, 10, 10, 10, 11). -
The y axis re-searches instead of dropping labels: when two chosen ticks would round to one cell row, layout walks the tick target down until every tick has its own row — the rule the x axis already followed — so a short plot shows a complete sparser set, never a subset of a denser one.
-
Strided decade ticks prefer multiples of the stride (
1, 10³, 10⁶rather than10¹, 10⁴, 10⁷) whenever that phase keeps as many ticks as any other, matching the placement matplotlib settled on in 3.11. -
stream::Live::detect(out)repaints in place only whenoutis a terminal and appends plain frames otherwise, so a redirected stderr (2>log) receives charts and never a cursor escape — the rule-4 gap the unconditional repaint left.Live::newkeeps its explicit always-repaint contract. Every repaint is now one synchronized-output frame (DEC private mode 2026), the bracketGraphics::presentalready used, so terminals that speak it paint the chart at once and the last flicker source is gone.kaz --livedetects the same way and hides the cursor only on a terminal. -
Every tick label comes from one formatter. The three fallback paths that formatted with Rust's
Display— equal bounds, a span the search cannot cover, timestamps outside the calendar — now format at the shared significant-digit budget, so an axis never reads-0,0.0000001, or a 309-digit label. Sets beyond the SI prefix table (T,p) write against one power of ten (1.797e308,8.796e-100), inNumberFormatand on axes alike;NumberFormatrounds the shortest round-trip digits with integer arithmetic, so no magnitude passes through a lossy power of ten. The search's coverage score no longer overflows past1e154, where it fell back to endpoints after enumerating its whole candidate space. A deterministic 20,000-case sweep over every magnitude pins the contract: finite, ascending, distinct labels that decode to their ticks.
1.22.0 (Lady at the Poster Column) — 2026-09-14#
The terminal is a category, not a device: any host that draws a cell grid is one. This release adds the encoder for hosts that draw with SVG, turns bars sideways, and packages the grouped-bar arithmetic as a stat.
Plot::to_svg(&frame)andRaster::to_svg(theme)render the cell grid as a self-contained SVG terminal card — the picture a README on GitHub (which strips the HTML card's styles), a notebook export, or a static page draws. Block glyphs (Block Elements, sextants, octants) become crisp rectangles from their defined geometry; braille, box drawing, and labels stay text the host's font draws, pinned to the cell grid withtextLengthso the layout survives any font. Colors resolve exactly as in the HTML card; the card's background and foreground follow the theme. No dependency, no feature, no rasterized text: nothing is drawn that a glyph terminal would not draw.Plot::to_htmlneeds no feature any more, andRaster::to_html(theme)is its raster half — one grid, one encoder per kind of terminal (encode,to_plain,to_html,to_svg). Theevcxrfeature keeps only what names the frontend:evcxr_display,mime_bundle,card_colors.evcxr_displaynow bundlesimage/svg+xmlaftertext/html, so nbconvert to PDF gets the card too; JupyterLab still shows the HTML.Bars::horizontal()turns any bars layer sideways: named bands (or spans, or free positions) run down the y axis, band 0 at the top in reading order, values extend along x from the zero baseline or the per-barbase, and long category names take the measured label gutter instead of a band's width. Under an automatic y scale a horizontal bands layer makes the y axis categorical, the twin of the x rule; explicit numeric or bands scales on the wrong axis are rejected as conflicts, as they are for vertical bars. Cell fills use the left-anchored eighth blocks rightward and the coarse right-anchored blocks leftward, mirroring the vertical ramp's asymmetry; pixel targets fill exact rectangles. This closes thebarhrow of the catalog. The wire format is byte-stable for existing specs:horizontalserializes only when set.stat::dodge(series, step)returns one position series per value series — each series' indices shifted by(k − (n − 1) / 2) · step— forBars::at, one layer per series: the sibling ofstat::stackfor bars beside each other. A bar width below the step leaves a gap that keeps the series apart without color. Grouped bars remain a composition; the arithmetic now lives in one place, and thesegmentsexample composes through it.Mapping::y_categoriesanswers for a y axis made categorical by a horizontal bands layer, not only by an explicitScale::Bands.- The doc generator writes whole files from an example's stdout
(
examples/regen_docs.rs,FILES), checked for staleness like every spliced block. The README's first colored chart,examples/speedup.svg, is program output regenerated in CI — thespeedupgallery example drawn as an SVG card. Frame's rustdoc shows sizing a detected frame with struct update syntax; no builder was needed.- A GitHub Pages gallery (
gallery/) runs the wasm in the browser: ascii cells beside the pixel panel, Rust and TypeScript listings, and a live M4 plate that times zooms through a million-point series.
1.21.0 (Desk and Room) — 2026-09-07#
The table release: the summary that usually precedes any chart, rendered by the grammar that renders the charts. A table is text on band scales — not a ninth mark, not a widget — and a table column is an axis you read instead of see.
Rasteris the encoded cell grid of one render — glyphs and colors, chrome included — as a plain value.Plot::raster/try_rasterproduce it;Raster::encodeis the string half, so a TUI host that paints cells and a caller that wants aStringshare one grid. Continuation cells (columns == 0) sit to the right of a wide glyph. This is the host-neutral snapshot Ink (and any future cell-buffer host) consumes; the ratatui adapter may keep its private path in 1.x.- The JS rim (
js/, npmmalevich) is public. 0.3 covers the eight-mark grammar, shared goldens against this crate, Mapping/Viewport as JS values,npx malevich, and pixel encoding (Plot.renderPixels) with detection in JS. See js/CHANGELOG.md and js/README.md.cargo run --example js_goldens --features serde -- --checkis the shared oracle gate. - With
serde, a series may deserialize from{ "col": N }insidedata::with_columns, so a render request can keep large buffers out of the JSON document. Stored documents still encode series as arrays of numbers (gaps asnull); a column reference without a bind is an error. describe(names, groups)renders the first-look summary —count mean sd min p25 p50 p75 max, one row per group — fromMomentsand the box plot's type-7 quantiles;table(rows, columns, values)(andtry_table) lays out any numeric matrix the same way. Row labels ride the y band axis, column headers the x band axis; each column is padded to its own width so numbers meet at the decimal point and centered on its band by the same rule the header uses, so a column and its header land in lockstep. A table is tightest when the plot rows equal its row count (frame heightrows + 2, one more with a title). Both presets are proven byte-identical to their grammar expansions, like every preset.table_withtakes aTableOptionscolormap: each value colored at its position within its own column's finite extent — the heatmap reading, column by column; a diverging map centered at zero splits sign. The digits still carry the value, so a colored table survives a colorless pipe undamaged.Textgains thealignchannel (Align::Left, the unchanged default;Center;Right). On a bands x axis the band nearest the anchor is the box, with chrome's own header geometry — the rounded band center, a step-wide budget — and aligned text also snaps its row the way chrome snaps a band label's, so cell text never drifts a line or a column from its labels. Text wider than the box clips to it, ending with a truncation.— a number is shortened visibly, never garbled by a neighbor's digits. Centered text overCellson band axes annotates heatmaps: confusion matrices with counts, from the grammar, no preset. In pixel panes the same anchors shift the ink by its own width and center it vertically in its row, so a table's values sit on their labels' lines in the image exactly as in cells. Serialized specs omit the default alignment, so existing documents decode and render unchanged.- Annotations keep the field they land on: a
Textglyph over filled patch ink — a heatmap band, a bar body, a class region — takes the underlying color as its background instead of punching a hole in it; two-sample cells blend their halves. Pixel panes already composited ink over the field; plain output still lets the glyph replace the shade, so nothing is lost in a pipe. The gallery's correlation matrix shows the pattern — every coefficient printed in its cell, ink picked from the luminance underneath. - The resolution ladder is content-aware:
render_bestand the capability-context paths keep a text-only plot — a stat table — on cells even when pixels are offered. For pure text the terminal's own font is the best tier, and the glyphs stay selectable; an explicitrender_pixelscall is still honored. scale::NumberFormatis an axis's label discipline for any value set: one fraction width, one SI prefix, exact decimals, whole labels for whole-number sets,—for gaps. It formats everytablecolumn and is public for any readout.
1.20.0 (Dynamic Suprematism) — 2026-08-28#
The interaction release. A chart becomes an instrument without malevich ever
touching input: the core gains the physics — a queryable Mapping, a
Viewport that is pure domain arithmetic — and the ratatui widget grows the
controller: gestures, a crosshair that snaps to the data, linked panes that
share a window and mirror a cursor, pixel panels that answer the mouse with
atomic, self-pacing repaints. Bars finish the basic catalog on the way:
stacked, grouped, and floating bars are now plain compositions.
-
The widget becomes interactive — without malevich handling input. The core gains the physics:
Plot::mapping(&frame)exposes the resolved geometry of a render as a queryable value (plot rectangle, cell ↔ data both ways, per-cell data spans, values formatted the way the axes format their labels);Viewportis zoom/pan as pure domain arithmetic overx_domain/y_domain(decade space on log axes,tailfor streams), seeded frommapping.viewport();stat::nearestsnaps a cursor to the datum that exists. On top, the ratatui adapter grows aStatefulWidget:PlotStatecaches the render's mapping for hit-testing, applies its viewport on the next draw, and interprets the default gestures — hover crosshair with an axis-formatted readout, wheel zoom anchored under the cursor, left-drag pan, right-drag rubber-band zoom — from a backend-neutralMousevocabulary the host feeds it (a six-line match from crossterm, shown in the docs). The cursor snaps to the data: for every point-backed line and points layer the readout lists the datum nearest the cursor's x —label: value, axis-formatted, its cell highlighted, gaps as—and never an interpolation — instead of the cursor's own coordinates;snap(false)returns plain coordinates. Overlays draw into the buffer only: the plot value and the stateless widget render byte-identically as before. Because a zoom is just a domain window, M4 re-aggregates to the visible window every frame —cargo run --release --example zoom --features ratatuipans and zooms through millions of points with the drawn line pixel-identical to plotting every point — measured: the newwidgetbench records a two-pane dashboard frame at 1.3 ms, a zoomed ten-million-point frame at 18.6 ms, and a hovered one (the snap scan included) at 25 ms on the BENCHMARKS.md baseline.docs/interaction.mdis the guide: the three-layer model, the gesture table, and the patterns that need no library support — linked panes (a view is a value; sharing it is assignment), selection → statistics (a rubber-band window summarized with the ordinary stat vocabulary), follow-the-stream.fredwears all of it on its series view: the full gesture set, a year-over-year context strip linked to the main chart by mirroring the x window, and a footer that describes the visible window when zoomed;sysmonpins its streaming axes withtail, holding a stable two-minute window while the rings fill.pan_left/pan_rightjoin the keyboard sugar (h/l in fred, arrow keys in the zoom example), and gestures stacked between renders compound instead of re-reading a stale mapping — drag pans included, which an event-drained burst previously collapsed to a single step. And with thepixelfeature, the interactive widget draws real images:widget().graphics(g)reserves the area in the buffer (skip cells, fresh ground on layout change) and stores the hybrid chrome-plus-image block in thePlotState;Graphics::presentemits the pending blocks afterterminal.drawin one synchronized write with atomic kitty replacement,Graphics::retire/invalidate_pixelshandle view switches. Interaction chrome upgrades to annotation marks drawn into the image — anti-aliased crosshair rules, snap markers, in-panel readout — with automatic axes pinned to the last frame so hovering never jitters them (viewport-fixed axes are never pinned: the window a gesture just set always renders). The pixel render paces itself: within a ~33 ms window an unchanged-view render reuses the image already on screen, so hover floods and tick redraws cost nearly nothing and even a one-event-per-frame host loop stops falling behind (a changed viewport always renders — a zoomed window never shows stale). And repaints never flicker and never rely on replacement semantics: each panel's kitty image data travels transmit-only (a=t) under a stable per-panel image id, and the presenter then creates a fresh placement under an alternating placement id before retiring the one on screen — create-before-delete in one synchronized write, so there is no gap on any terminal, including those whose same-placement replacement misbehaves; a panel whose content already matches the screen is not transmitted at all;Graphics::retiredeletes images by id, never touching other applications'. fred renders its series view this way wherever the terminal speaks sixel, kitty, or iTerm2 — at native device-pixel density, so the image is crisp on Retina cells (ptoggles pixel drawing live,--fasthalves the density for slow links,--cellsforces glyphs from the start) — and both fred and the zoom example drain their event queues before redrawing, collapsing input bursts into one repaint. -
The hand-rolled deflate truncates every LZ77 match at 32 KiB window-aligned output boundaries. Bounded blocks alone never protected a streaming inflater — block output positions drift, so matches still straddled the drain boundaries where Zig ≤0.15's flate (Ghostty ≤1.3.1) aborts a fixed-Huffman transmission mid-match; a real chart stream carried four hundred straddling matches. (Measured against Zig 0.15.2's exact buffered decode path, those particular streams happened to survive — the truncation is defense in depth against the documented abort, not the explanation of any observed freeze.) Costs one shortened match per 32 KiB (+0.2% on a measured chart stream); an emission-site assertion and a five-window regression test pin the invariant.
-
fred goes fully graphical: every view — the six-pane overview, the linked series pair, distributions, the seasonality heatmap, relations — renders as pixel panels through per-pane
PlotStates, presented and retired as a unit on view switches, and every pane answers the mouse (hover, wheel zoom, drags) through one generic router. The ink came along: glowing lines over translucent washes filled from each series' own floor (never a zero baseline that would crush GDP's axis), accumulated-ink Phillips scatters, a dashed inversion rule, and the seasonality heatmap gone bilinear — and the palette is designed, not ANSI: a muted, luminance-balanced categorical set (validated for CVD separation and 3:1 dark-surface contrast, assigned in fixed order), a soft lavender for context panes, a reserved rose for recessions and inversions, and the seasonality map diverging around zero, because signed change is polarity, not magnitude. -
The interaction API, reviewed whole:
Mapping::plot_areaanswers with a namedPanelvalue instead of a four-tuple;x_categories/y_categoriesexpose a bands axis's labels (not a bare count);y_span_atjoinsx_span_at;Plot::viewporttakes itsCopyvalue by value; the pixel presenters becameGraphics::presentandGraphics::retire, because the value that names the protocol owns its choreography; and theMousevocabulary grewScrollLeft/ScrollRight(horizontal swipes pan) behind#[non_exhaustive]input enums. -
Linked panes share a crosshair, not just a window.
Mapping::column_at(x)is the x-only half ofcell_at, andPlotState::hover_x(x)places a mirrored hover from a data x: the widget draws a vertical-only crosshair at that pane's own column — an x but no honest row to claim — while snapping and the readout work exactly as under a real cursor, in cells and in pixel panels alike; a non-finite or off-window x clears the mirror, and a real cursor placed later simply replaces it. fred mirrors the cursor both ways on its series pair and sweeps one date crosshair across all six overview panes. -
Graphics::economical()lifts the slow-link density trick pixel hosts carried by hand: halve a Retina cell size, keep the ink weight through a stroke override, a quarter of the bytes — sixel, with no placement scaling, stays native. fred's--fastis now one call. -
Docs say the quiet parts.
hist2ddocuments its equal-length panic, and the convenience presets that can panic on data (trend,hist2d,heatmap) panic with the caller's mistake instead of "default options are valid".Cells::rgbdiscloses that an image stretches to the plot panel — shape the frame when shape matters. Terminology's Theme entry records why the layer palette is theme state while the categorical palette travels with the spec. -
The viewport's wire form carries only its windows: which space an axis transforms in (decade or value) is derived when a viewport is seeded from a
Mapping, never persisted — a stored flag could disagree with a plot whose scale has since changed. A restored viewport is complete forPlot::viewport, which reads only the windows; transform after re-seeding, which is the gesture lifecycle anyway. The interaction guide gains the modifier-gesture pattern — theMousevocabulary stays modifier-free by decision (terminals report modifiers unevenly, and a default grammar must not half-work per terminal), and a shift-wheel y zoom is a five-line drive of the public physics.x_domain/y_domainnow say what they always did: bounds in either order, the axis always ascends. -
Bars complete the catalog: a
basechannel (Bars::base) starts each bar at a per-bar base instead of zero — barispansbase[i] .. base[i] + value[i], so the value keeps encoding the segment's length — andBars::at(x, width, values)centers bars at free numeric positions, on a continuous axis or in band-index space on a bands axis. Stacked bars are now a composition (each layer based on the running total; the low half ofstat::stackis exactly that, and the stacked render is asserted cell-identical to a single layer of the totals), grouped bars are two positioned layers offset around the band centers, and waterfalls fall out of the same channel. A gap (NaN) in a base or a position skips that bar; zero stays pinned into the y domain only while some bars layer still rises from the zero baseline; the wire format is byte-stable for existing specs. The gallery gainssegments, both compositions in one chart pair. -
Plot::mappingcosts a layout, not a render: the geometry pass now stops after the extent probe and the layout it yields, instead of also running the mapped M4 aggregation for a raster nobody draws. A ten-million-point mapping drops from 30.4 ms to 2.1 ms on the BENCHMARKS.md baseline (newplot/mapping_10m_80x20row), and a parity test pins the layout-pass mapping to the render pass's, query for query. -
The design argument is public.
docs/now carries the vision and its five rules, seven principle files — each arguing one constraint and ending in a "Spelled today" section that may rot while the argument must not — and guides for terminals, pixels, notebooks, performance, and serde.TERMINOLOGY.mdandSERDE.mdmoved there (stubs remain at the old paths). The gallery now reads as a ladder of sections, and principle files demonstrate their claims with generated witness charts, spliced and CI-verified like every chart in the docs.
1.19.0 (White on White) — 2026-08-27#
The ink release. The pixel canvas learns coverage, and everything drawn on it turns from stamped rectangles into graded light: anti-aliased strokes, glow, translucent washes, dashes that flow through joints, gradient trajectories, density scatters, bilinear heatmaps. Every effect degrades honestly on glyph targets, the wire format is byte-stable, and the whole gallery wears the new ink.
-
The pixel canvas learned coverage: pixels are straight RGBA with alpha as coverage, and everything drawn on it is anti-aliased — strokes with round caps and sub-pixel endpoints, discs, rings. Fringes composite over any terminal background; kitty and iTerm2 transmit the raster verbatim, sixel thresholds to solid ink.
-
New ink, mark by mark:
Line::glow(a soft halo fading from the stroke),Line::dashandRule::dash(dashes and dots whose phase flows through polyline joints),Line::grade(gradient strokes through a colormap),Area::opacity(translucent fills and bands),Points::opacity+Points::density(accumulated ink — overplotting reads as brightness),Cells::smooth(bilinear heatmaps), and text annotations that ride their exact data anchor on pixel targets. -
Two long-standing fill bugs died on the way: area fills stopped one gutter short of the plot's far edge, and isolated line points (the first point of every NaN-jointed contour segment) drew with the marker pen instead of the stroke's weight.
-
The showcase tour opens an effects corner: glow over a wash with dashed annotations, a trajectory graded by step, fifteen thousand points as accumulated ink, and a smooth loss landscape under its contours.
1.18.6 — 2026-08-27#
Graphics::strokeoverrides the cell-derived line width in device pixels. Hosts that transmit reduced-density rasters into a scaled placement rectangle (c=/r=, since 1.18.4) can keep the ink weight they had at native density instead of inheriting a hairline from the smaller cell.
1.18.5 — 2026-08-27#
- Pixel blocks anchor every text row, including at column 0. Flush-left blocks used to stay escape-free, which is only safe in cooked mode: raw-mode LF does not return the carriage, so a TUI printing a column-0 block watched its chrome staircase across the screen.
1.18.4 — 2026-08-27#
- Kitty images now carry their placement rectangle (
c=/r=): the image is pinned to the panel's cells and the terminal scales it as needed. This makes transmitted resolution a host-side knob — send a standard-density raster into a Retina-sized panel for a fraction of the decode and upload cost — and keeps placement correct even when cell-size detection was off. iTerm2 already behaved this way; sixel has no placement scaling and is unchanged.
1.18.3 — 2026-08-27#
- The pixel encode path is ~4.5× faster (a 2744×1230 kitty panel drops
from ~81 ms to ~18 ms on an M-series laptop). Deflate compares matches
a word at a time and indexes only the fringes of long matches (zlib's
max_insert_lengthtrick — hashing every byte of a flat run dominated the whole compressor), and the kitty encoder crops the canvas straight into RGBA, skipping the intermediateImagebuffer. Compressed size is unchanged within a fraction of a percent.
1.18.2 — 2026-08-27#
- The deflate stream now splits into bounded blocks (16 KiB of input each) instead of one stream-length fixed-Huffman block. The old shape was valid DEFLATE but crashed terminals built on Zig ≤ 0.15's inflater — Ghostty 1.3 aborts the moment a compressed kitty image arrives, because a fixed-Huffman block that decodes past the 32 KiB drain window hits unreachable code (fixed on Zig master). Blocks share one LZ77 window, so the split costs ~10 bits per block: a 700 KB panel grew by 54 bytes.
1.18.1 — 2026-08-27#
-
Pixel transport is now compressed: a dependency-free zlib/DEFLATE compressor (LZ77 over a 32 KiB window into fixed-Huffman blocks) rides under both image encoders — kitty transmits
o=zdeflated RGBA and the iTerm2 PNG carries a real IDAT instead of stored blocks. A Retina-sized panel drops from ~22 MB to ~175 KB per repaint, which turns multi-second redraws (the terminal's escape parser pays for every byte) into imperceptible ones. Sixel was already compact and is unchanged. -
Hybrid pixel blocks (
render_pixels,render_pixels_at) now own their full rectangle: every text row spans the frame's width instead of trimming trailing spaces, so a block reprinted in place fully replaces the previous one — a shorter title no longer leaves the old title's tail visible, and in-place hosts (TUIs repainting a panel) need no manual blanking. Ordinary cell renders keep trimming trailing spaces.
1.18.0 (The Knife Grinder) — 2026-08-26#
The machine-learning release. Band scales on both axes, a logarithmic
colormap, two new cells channels, and bucket-exact matrix reduction turn the
charts ML actually reads — confusion matrices, attention maps, decision
boundaries, images, loss landscapes — into grammar compositions; roc,
auc, and ewma fill out the statistical set, and a nine-chart gallery
wave plus a showcase ML corner prove the vocabulary was sufficient.
Everything is additive, and a long-standing sub-decade log-axis bug died on
the way through.
- The showcase tour grows an ML corner: a confusion matrix on band axes, a
log-colormap attention head with a colorbar, a learned filter as rgb cells,
1-NN decision regions with the training scatter, a momentum trajectory over
a bucket-reduced loss landscape, seed-variance bands with EWMA smoothing,
and a spectrogram — every panel upgrading to real pixels beside its cells
under
--features pixel, like the rest of the tour. - The loss example's training log now credits topos by its current name —
the library was renamed from poorgrad — and the data file moved to
examples/data/topos_loss.csvaccordingly. - Fixed: a log axis over a range narrower than one decade no longer loses its data. The linear-fallback ticks of a sub-decade log range can include zero; the domain grew to that tick, zero has no logarithmic position, and the whole scale collapsed. Log domains now refuse to grow to a non-positive bound, and a tick without a position on its scale is dropped instead of drawn at a fabricated column.
stat::ewma: debiased exponentially weighted smoothing — TensorBoard's scalar smoothing, early outputs unbiased instead of dragged toward zero, gaps passing through without disturbing the state. A scan over the ordered series, documented as a batch transform rather than pretending a merge law. Gallery gainsseeds: five runs pooled into per-step quantile bands via the existing reducers, with the smoothed median on top.stat::rocandstat::auc: the classifier threshold sweep (standard step construction, ties grouped, one-class data returns empty rather than invented rates) and the trapezoid area under a polyline, gaps contributing no area. Batch transforms in theecdffamily — order statistics, not mergeable accumulators — with hand-computed fixtures. Gallery gainsroc.- Gallery:
spectrogram— time × frequency power as dense Cells with a log frequency axis and a log colormap; the exponential chirp is a straight ridge. The energy is synthesized analytically: no FFT enters the crate. - Gallery:
ridgeline— distributions over training epochs as lifted KDE rows, painter's algorithm back to front, no camera and no new machinery: the TensorBoard histogram view and the honest terminal answer to a 3D surface. - Gallery:
calibration— a reliability diagram fromstat::binnedwith aMeanreducer over 0/1 outcomes; the overconfident model's curve sags under the diagonal. No new API: the reducer vocabulary was sufficient. - Gallery:
landscape— a loss landscape with a momentum trajectory, composed entirely from existing marks (dense Cells on a log ramp, Line and glyph Points on top): the gradient-descent chart, no new machinery. - Cells grids denser than the raster now reduce honestly instead of sampling:
every screen bucket owns the cells whose centers fall inside it (adjacent
buckets partition the centers, proven by a property test) and shows a
reduction over all of them —
Reducer::Meanby default,Cells::reduceto choose;Maxkeeps sparse spikes visible that sampling silently dropped. Rgb grids box-filter per channel and class grids reduce to the modal class with deterministic ties. 4.19 million cells reduce in ~44 ms on the recorded baseline (BENCHMARKS.md). Buckets owning no cell center keep the old center-sampling, so ordinary small grids render as before. Gallery gainsattention-full: one million attention weights rendered twice, the mean pane dissolving the long-range spikes the max pane keeps. Cells::classesdraws categorical regions: a grid of class labels colored through the plot's categoricalPalettewith a categorical legend — the decision-boundary chart. Labels intern in first-appearance order exactly likecolor_by; in plain output each class keeps a stable shade-ramp glyph and the legend swatches carry the same glyphs, so regions stay separable with no color at all. Gallery gainsboundary, 5-NN decision regions with the training scatter on top.Cells::rgbdraws a grid of direct colors — an image. Raw row-major pixel buffers only (decoding files stays the host's job), no colormap and no colorbar, honest quantization down the color ladder, and in plain output each pixel falls back to its luma on the shade ramp so images survive a pipe. With thepixelfeature the grid blits at device resolution. The serde encoding is additive; value grids encode exactly as before. Gallery gainsfilters, an AlexNet-style Gabor bank rendered asCells::rgbsmall multiples.Colormap::log()makes any ramp logarithmic: equal color steps for equal factors, so attention weights, gradient magnitudes, and spectral power that span decades stay distinguishable instead of collapsing into the low end of a linear ramp. Values at or below zero have no logarithmic position and render as gaps — the same rule log axes follow — and the colorbar places decade ticks logarithmically. Logarithmic andcentered_atare mutually exclusive (validation catches the combination, including deserialized specs); the serde encoding stays byte-identical for existing maps. The gallery gainsattention— token-labeled bands on both axes and a log MAGMA ramp.Scale::Bandsnow works on the y axis: continuous marks position y against band indices exactly as they do on x, and aCellsmatrix maps row k onto band k, top-down — band 0 is the top band, so labeled matrices read in matrix order. Cells grids must match their band axes cell-for-band (extents do not apply there), Bars still require a numeric y, andPlot::y_scaleno longer panics onBands. Confusion matrices and attention maps are now three-line grammar compositions; the gallery gainsconfusionas the proof.
1.17.0 (The Carpenter) — 2026-08-24#
A code-quality release: invalid retained data now reaches one checked boundary, the hottest statistical and rendering paths carry less incidental structure, and the cell, pixel, and CLI front ends prepare a chart once. The public API remains compatible; new checked twins make the remaining convenience assertions avoidable.
Cells::try_matrixandCells::try_extentsexpose typed failures for invalid matrix geometry and extents. Configured histogram, heatmap, and 2D-histogram presets use those checked paths end to end, while their infallible conveniences still degrade safely for trusted literals.- Finite-range arithmetic is overflow-safe, calendar tick generation has an explicit work budget, and retained mark values share one validity policy. These close hangs and pathological allocations around enormous finite domains without changing ordinary axes or plots.
- M4 downsampling preserves gaps as path topology, including gaps between buckets and across merged chunks. Its streaming state, tests, and merge contract now say exactly when ordered partial reductions are equivalent to one pass. The ordinary affine map is selected once and each bucket updates its current run directly; ten million points now render in 31.9 ms on the recorded machine, 5.9% below 1.16.
- Reducers compile once into execution state. Sum, count, mean, min, and max stream in constant space; rolling sum and mean use their specialized sliding state; percentile-like reducers retain only the samples they require. Aggregation keys are interned in stable first-seen order, and KDE reuses moments and sample storage.
- Categorical channels are interned once per mark and rendered directly. They no longer expand into one masked layer per category, while stable legend order, palette assignment, marker cycling, line transitions, and category-aware M4 retain the prior output contract.
- Cell and device-pixel rendering share one prepared plot. The CLI likewise shares
one typed recipe and one set of parsed series channels between rendering and
--emit-code, removing duplicate resolution and keeping both outputs in lockstep. - Invalid civil timestamps and out-of-range numeric column selectors are rejected with actionable CLI errors. CI now builds and tests both demo crates, terminal dependency versions are aligned, and the public failure and statistical execution models are documented explicitly.
1.16.0 (Red Square) — 2026-08-15#
Color speaks data. One categorical channel, a curated color vocabulary, a least-squares stat, and one shared reducer close the gaps between malevich and the charts science asks for first — and the CLI grows a bridge out of the shell. Everything is additive, and the re-measured render baseline came out slightly faster than 1.15.
- One vocabulary for every aggregation:
stat::Reducer—Count,Sum,Mean,Median,Min,Max,Percentile(q)(type-7, exactly the box plot's estimator) — is now whatAgg::reduceandWindow::reducetake (their named methods are sugar over it), what the newstat::binneduses to reduce a paired series per histogram bin, and whatstat::quantilesevaluates in batch over one sort. Rolling p95s, per-group percentiles, binned medians, and Q–Q plots (see the gallery) all fall out with no new API shapes. - The line of best fit:
stat::Fitis streaming ordinary least squares — bivariate Welford accumulation, mergeable like every other aggregator, with slope, intercept, R², prediction, and the standard error of the mean response. Thetrendpreset draws a scatter with its fitted line;trend_withadds a confidence band around the mean response through the existing band mark, at a caller-chosen standard-error multiplier. Measured: one million pairs fit in ~5 ms single-threaded (BENCHMARKS.md). - Interval polish:
error_bars_asymmetric(x, y, minus, plus)covers two-sided deviations, andecdf_withgrows the Dvoretzky–Kiefer–Wolfowitz confidence band as a checked option — both compositions of existing marks, both proven equal to their grammar expansions. - Color speaks data:
color_by(categories)onLine,Points,Bars, andRangecolors a layer by a categorical series. Distinct categories (first appearance first) take colors from the newscale::Palette— Okabe–Ito by default, colorblind-safe, replaceable withPlot::palette— and name themselves in the legend. In colorless output the default point markers cycle shapes per category, so groups separate in a pipe as well as a terminal. The channel is proven bit-identical to its masked-layer expansion; grouped scatter, volcano, Manhattan, and candlestick compositions join the gallery. PointStylegains portableAsterisk(*) andCircle(o) markers, in cells and as geometric pixel shapes; the ASCII legend swatch forDotis now.., freeing**for the asterisk.Colormapgrows a curated named set — sequentialVIRIDIS(the default, now named),MAGMA,CIVIDIS,GREYSand divergingRED_BLUE,PURPLE_ORANGE— selected to stay distinguishable after the 256- and 16-color quantizers, withColormap::namedresolving the canonical names for CLIs and configuration.Colormap::centered_at(mid)anchors a map to a data midpoint: signed and centered data (correlations, log fold changes) renders with the neutral color at the midpoint and the value range spanned symmetrically, and the colorbar labels the symmetric span.heatmap_withjoins the checked_withpresets so heatmaps take a colormap without abandoning the one-call default; the correlation example now demonstrates the honest encoding.- The control-character-dropping behavior of the cell grid is now a stated, regression-tested contract: hostile escape bytes in titles, labels, categories, or annotations can never reach ANSI, plain, or HTML output.
1.15.0 (The Aviator) — 2026-08-10#
A hardening release: the paths that used to trust their inputs now refuse impossible geometry instead of reaching for the allocator, validation rejects combinations that never meant anything, and Windows joins CI. What opens up alongside it is reach — the notebook helpers, the charset policy, and the capability query stop being malevich's private business and become things a host can drive.
- The
evcxrfeature now exposes a publicevcxrmodule:mime_bundlewrites Evcxr's stdout protocol andcard_colorsreturns the background and foreground a plot card paints itself with. Both were internal, which left a crate rendering its own types beside a chart with nothing to do but hardcode the colors and reimplement the framing, then drift on the next theme change.Plotnow draws through the same two functions, so the exported values cannot disagree with what it paints. - Rendering and statistics now reject overflowing or over-budget geometry through
typed fallible paths; the infallible conveniences degrade without attempting giant
allocations. The
kazCLI applies corresponding bounds to user-controlled sizes. - Automatic UTF-8 output now conservatively uses quadrants.
Frame::portableandMALEVICH_CHARSETmake deterministic and explicit charset policy available to hosts; braille, sextants, and octants remain opt-in dense tiers. - Mark/scale validation now rejects meaningless combinations and
Cellscorrectly inverts logarithmic axes.Grid::validate/try_renderand exhaustive tiny-frame layout keep every rendered row and column inside the requested frame. Documentis a validated, versioned serde envelope with committed v1 and legacy fixtures. Additive fields default safely; malformed specs remain representable only until the strict validation boundary.- Pixel capability detection is destination-aware through
Capabilities::detect_for; explicit capability-driven rendering stays pure. Probe replies are bounded and their parser is covered by deterministic arbitrary streams. - Runtime-owned colormaps and checked option structs now configure histogram, 2D histogram, KDE, violin, and contour presets without abandoning their short defaults.
- Heatmaps render two vertical colors per terminal cell through independent foreground/background half-blocks. ANSI transitions, HTML spans, and ratatui styles preserve both channels; plain output keeps an averaged shade-ramp fallback.
PointStyle::{Dot, Plus, Cross}gives point layers and legends portable shapes that remain distinguishable without color, with corresponding geometric pixel markers.- Windows joins Linux and macOS CI; boundary sweeps cover extreme ticks, tiny rasters, image encoders, grids, and terminal replies.
BENCHMARKS.mdrecords the dated, reproducible performance baseline and CI enforces structural allocation ceilings. Implicit coordinates no longer allocate, profiled layout metadata is reused, and the plain encoder keeps the richer cell model at the prior end-to-end timing.
1.14.3 — 2026-08-05#
- A compact Suprematist composition joins the examples and README, rendering the
same layered
Area,Bars,Range,Line, andPointsmarks side by side in octants and pixels.
1.14.2 — 2026-08-05#
- With the
pixelfeature also enabled,Plot::evcxr_display's terminal representation becomes a real sixel/kitty/iTerm2 image in a graphics-capable terminal (theevcxrREPL), and stays cells everywhere else — so a plot in a pixel-capable terminal renders as an image, not braille. - Cell-size detection falls back to
/dev/ttywhen stdout is piped (as under evcxr, or a mid-pipelinekaz), so pixel strokes are weighted for the terminal's real cell size instead of defaulting to a hairline.
1.14.1 — 2026-08-05#
Plot::evcxr_displaynow emits atext/plaincell plot alongside the HTML card, so the plot shows in frontends that cannot render HTML — notably the terminalevcxrREPL, where it previously appeared blank. Jupyter still renders the card; each frontend picks the richest representation it supports.
1.14.0 (An Englishman in Moscow) — 2026-08-04#
Rich display in Rust notebooks, as the same complete cell grid malevich already renders everywhere else. Additive, dependency-free, and behind an opt-in feature; terminal and default-feature output are unchanged.
- New
evcxrfeature: aPlotending an Evcxr Jupyter cell now renders as a self-containedtext/htmlterminal card through the conventionalevcxr_displaymethod. The default is a notebook-sized 100×26 braille frame;Plot::to_html(&frame)is the pure deterministic path for explicit size, charset, and light/dark theme control. - The cell surface has an HTML encoder parallel to its ANSI encoder: concrete RGB
colors collapse into
<span>runs, default-colored chrome inherits the card foreground, row whitespace trims identically, and every glyph escapes HTML content. Inline card styling contains wide plots with horizontal scrolling and keeps braille rows tightly connected. No new dependency and no duplicated plot rendering logic. - New inspectable example:
cargo run --example evcxr --features evcxr > plot.html.
1.13.0 (The Knife Grinder) — 2026-08-04#
Enablers for the kaz CLI (crate malevich-cli, released alongside — a
stdin-pipe plotter built entirely on the public API). Additive and
cell-output-neutral.
Frame::detect_for(&impl IsTerminal): the fulldetect()ladder, but with the color gate keyed to the destination the caller actually writes to rather than always stdout. A tool that plots to stderr while data flows on stdout detects against stderr — otherwise a piped stdout would strip color from a plot going to a live terminal.detect()is nowdetect_for(&std::io::stdout());NO_COLOR/CLICOLOR_FORCE/TERM=dumbkeep their precedence, and size still comes from whichever standard stream is a terminal.
1.12.0 (Cow and Violin) — 2026-08-03#
The pixel release. Malevich painted a cow onto a cubist composition to collide
two systems of representation in one picture; this release does the same to the
terminal — text chrome and a real image, woven into one deterministic string.
Cell output remains the product; pixels are the new top rung of the resolution
ladder, behind the pixel feature, and everything below it is untouched.
-
Strokes scale with cell density: line width is
round(cell_height / 16)device pixels (minimum 1) and point markers are one step heavier, derived the same way the in-panel font scale already is. Classic 8×16 cells keep the exact 1-pixel ink they always had; on retina/high-DPI terminals — where a cell spans ~20×44 device pixels and a 1-pixel line was a hairline — lines weigh what they do in cell output and scatter dots are visible again. -
Plot::render_pixels_at(frame, graphics, column): pixel output anchored at a cell column — every text row and the image cursor walk start with an absolute-column jump (CHA; rows stay relative, so scrollback is safe), letting hosts paste a pixel plot beside other content. The showcase uses it: with thepixelfeature in a capable terminal, every chart incargo run --example showcase --features pixelrenders as a side-by-side comparison — cells on the left, the same plot as a real image on the right. -
pixel::Capabilities: terminal capabilities as a plain queryable value — the protocols the terminal accepts (best first), its cell size in device pixels, and whether the answer wasProbedorSniffed.Capabilities::detect()now actively probes the terminal where that is safe (a real tty, no tmux/screen,TERMnot dumb): one raw-mode/dev/ttyround trip carrying the kitty graphics query, XTVERSION, XTSMGRAPHICS, andCSI 16 t, with DA1 as the ordering barrier — ground truth that, unlikeTERM_PROGRAMsniffing, survives ssh. The probe runs at most once per process (~100 ms on answering terminals, 300 ms budget otherwise), an unanswered probe degrades to the sniff answer, andGraphics::detect()is now sugar forCapabilities::detect().best()— sorender_bestand the examples pick up probing for free. Trycargo run --example pixels --features pixel -- --capabilities. -
Plot::render_best(&frame): renders at the best graphics tier the terminal offers — the plot panel becomes a real image when thepixelfeature is on and a protocol is detected, and is exactlyrender(&frame)everywhere else (pipes, unknown terminals, tmux, or without the feature). The gallery examples now use it, socargo run --example sine --features pixel(or any other example) upgrades to pixels in a capable terminal while the deterministic gallery output stays byte-identical.Displayis unchanged:println!("{plot}")stays cells-only. -
Pixel graphics (new feature
pixel):Plot::render_pixelsrenders the plot panel as a real image — sixel, kitty graphics, or iTerm2 inline PNG — while title, axes, tick labels, and legend stay text cells. Marks draw at device-pixel resolution through the same generic pipeline (render::Canvas, new): M4 buckets per pixel column, heatmap cells sample per pixel, bars fill exact rectangles, box-plot medians read as cleared gaps, and in-panelTextmarks blit a baked public-domain 8×8 font. Undrawn panel area is transparent (sixelP2=1, kitty alpha, PNG alpha) so the terminal background shows through; output remains a deterministicStringwoven with DECSC/DECRC relative cursor moves.pixel::Graphics::detect()sniffs the terminal's best protocol (kitty/ghostty → kitty; iTerm2/WezTerm → iTerm2; foot, Konsole ≥ 22.04, Windows Terminal → sixel; tmux, pipes, unknown →None) and reads the cell size fromTIOCGWINSZ. All three encoders are hand-rolled — including the stored-deflate PNG with its checksums — adding zero required dependencies (rustix, already in-tree, joins as an optional dep for the cell-size ioctl). Trycargo run --example pixels --features pixel. -
Second demo app:
sysmon, a live system monitor (cargo run -p sysmon) — a sampler thread streams CPU, memory, and network readings throughstream::Ringsliding windows (network counters viastream::Rate) into a dashboard of pinned-axis area charts, an SI-prefixed bytes/s network chart, and a per-core utilization heatmap with colorbar. Demos now live in per-app crates (demos/fred,demos/sysmon). -
New demo app (
demos/, a separate unpublished workspace member):fred, a Federal Reserve economic-data browser in ratatui with five views — small-multiples overview, a series view (line/step/corners styles, calendar axis, log and year-over-year transforms, NBER recession ribbon, a 2% target rule on inflation), change histograms with decade box plots, a month-by-year seasonality heatmap with colorbar, and the Phillips-curve scatter plus the 10y-minus-fed-funds spread. Pure data and view layers (unit-tested) under a thin TUI shell; live refresh from FRED; heavier deps stay out of the malevich crate and CI. Run:cargo run -p malevich-demos --bin fred. -
New gallery entry
charsets: the same curve rendered across the whole charset ladder — octants, sextants, quadrants, half blocks, braille, ASCII — so the subpixel-density trade-off is finally visible in the docs, not just described. -
Grid (side-by-side plots) now leaves a blank row between stacked rows, matching the blank column already between neighbors. A lower row's title no longer butts against the row above's axis labels — multi-row small multiples read as distinct plots.
1.11.1 — 2026-08-02#
- Declared MSRV: Rust 1.88 (
rust-versioninCargo.toml), verified by a pinned CI job — the crate's let-chains and edition 2024 set the floor. - Stability guardrails in CI now that the crate is 1.x:
cargo-semver-checkscompares the public API against the last published release (a break requires a major bump), andcargo-deny(seedeny.toml) scans dependency advisories, licenses, and sources.
1.11.0 (White on White) — 2026-08-02#
Crossing into 1.x. The version lineage is kept (major bumped, minor/patch as they
were) rather than reset — this is the same crate, matured, not a rewrite. The API is
what the Polish sweep settled; semver discipline begins here, so breaking changes now
mean a 2.0. (The remaining 1.0-hygiene items — a declared MSRV, cargo-semver-checks
and advisory scanning in CI — are tracked as follow-ups, not blockers.)
- Colorbars:
Plot::colorbar()draws the colormap as a labeled strip down the right edge, legending aCellslayer's value range. Theheatmapandhist2dpresets turn it on by default (a color-coded grid with no value scale is half a chart); the bareCellsgrammar stays uncolored-legend for full control. Sheds on narrow frames.
0.11.0 (Polish) — 2026-08-02#
The API review before the 1.0 freeze: the breaking changes are settled here, while the crate is still pre-1.0 and cheap to move on. A fallible boundary makes external specs safe; M4's headline guarantee is real again; a few names stop lying.
Scale::Autois the new default, distinct fromScale::Linear. An automatic axis adapts to its layers (categorical when a bars or band-range layer is present, linear otherwise); an explicitly chosen scale is now always honored rather than silently overridden by a categorical layer.Plot::validaterejects a categorical layer under a numeric x scale, and categorical layers that disagree on their bands.- Renames (breaking, landed early so downstream churn is minimal):
stat::Grid→stat::Histogram2d— it was a second publicGrid, unrelated to the small-multiplesGridat the crate root; the name now says what it is.Ticks::step()returnsOption<f64>instead off64—Nonefor a lone tick or the non-uniform ticks of a log/time axis, rather than a0.0sentinel a caller could mistake for a real spacing.
- M4 is pixel-exact again — and honestly so. Large lines are now reduced in mapped raster space: a cheap min/max probe fixes the layout, then M4 buckets by the exact column each point renders into, so the downsampled raster is bit-identical to drawing every point (verified against a raw-render oracle across index and xy lines at several sizes). The extra probe pass trades a little speed — ~45 ms for ten million points, up from ~28 — for the restored guarantee; a single-pass path is tracked for later.
- A fallible validation boundary:
Plot::validatechecks a spec's invariants (paired channel lengths, rectangular grids, valid colormaps, finite manual domains, scale/domain compatibility) and returns the first problem as a typedError;Plot::try_rendervalidates then renders.renderstays infallible and lenient — this is the strict counterpart for deserialized or configured specs.
0.10.1#
Correctness hardening from an external audit. Most fixes make existing guarantees real under composition, deserialization, and extreme inputs.
- Fixed domains (
x_domain/y_domain) are now honored exactly — they no longer widen to the tick range — and every mark is clipped to the plot rectangle, so out-of-range data can no longer leak ink into the axes or a neighboring grid cell. - Off-screen bar and area spans are clamped before rasterizing, so distant finite data under a narrow domain can no longer spin a near-unbounded draw loop.
Bins::autoalways covers the data and respects its cap: it widens the bin instead of dropping observations, so counts sum to the finite input count.Moments::default()now equalsMoments::new()(extrema start unset, not0).- M4 preserves a gap that falls inside a raster column — a
NaNbetween two values no longer reconnects them. Downsampling is described honestly as silhouette- preserving; true pixel-exactness (mapped-space bucketing) is tracked for later. - Deserialized specs that violate constructor invariants (empty colormap, zero-column grid, ragged range/area channels) now render defensively instead of panicking.
Ticks::linearno longer panics or hangs on extreme finite bounds;kdedeclines a degenerate large-magnitude sample instead of over-allocating;hist2dof constant data renders instead of coming out blank; a log axis with a non-positive manual domain is clamped rather than panicking, and a value that maps off a log axis is treated as a gap.lttbandm4assert equal-length inputs, like the mark constructors; thecontourpreset validates its geometry and treats all non-finite values as gaps.- Range body values now participate in y-axis fitting, so a body reaching past the whiskers is no longer clipped.
0.10.0 (Reach)#
- Contour lines:
stat::contours(marching squares — canonical shared-edge interpolation, center-average saddles, NaN gaps) and thecontourpreset with tick-chosen levels, colormap-graded and legend-labeled. quiverpreset: a vector field as arrows drawn in data coordinates.serdefeature: every spec type round-trips (plots, marks, scales, themes, frames, grids). Series gaps encode asnullin JSON and decode back to gaps; function-backed lines refuse to serialize honestly.ndarrayfeature: one-dimensional arrays and views ingest directly, zero-copy when contiguous.Colormapstops are copy-on-write (Colormap::newis still const);Colormapis no longerCopy.
Deliberately not added: a pie preset (no x/y scales — it fights the marks-over-scales
grammar; part-to-whole is served by bar) and a polars dependency (too large;
polars already reaches a chart with no dependency through the zero-copy slice path —
see the README).
0.9.0 (Red Cavalry) — 2026-08-02#
Riding into the ratatui ecosystem.
- The ratatui adapter (feature
ratatui, depending only onratatui-core):plot.widget()renders any chart straight into aBuffer— no ANSI round-trip, colors map onto cell styles, the host application keeps the terminal. Charset and theme are widget options;cargo run --example tui --features ratatuishows a live dashboard. - The gallery now runs on real data (
examples/data/, with provenance and licenses): the Keeling curve (NOAA, public domain), Palmer penguins (CC0), and a genuinely real training log — 1,000 per-step losses captured from poorgrad's bigram model. Six entries converted; mathematical examples stay mathematical. - The corners line style (
LineStyle::Corners): the classic asciichart look — one box-drawing glyph per column,╭╮╰╯elbows,│runs — with real axes underneath, and an honest+/-/|fallback in ASCII charsets. - Retained-plot cloning measured at ~10 µs for 12 layers × 5k points
(
plot/clone_12x5k_owned) — cheap enough that no copy-on-write machinery is warranted.
0.8.0 (Black Cross) — 2026-08-02#
The layout release: the charset ladder completes, and plots compose into grids.
- Sextant (2×3, Unicode 13) and octant (2×4, Unicode 16) charsets: braille density
with solid ink.
Frame::detectnow auto-selects octants on terminals known to render them (kitty, ghostty, WezTerm, foot, recent VTE, Windows Terminal) — sniffed, never probed. - Small multiples (
Grid): plots pasted side by side with escape-aware padding; share axes by fixing domains, not by a mode. - Manual axis domains (
Plot::x_domain,Plot::y_domain): matplotlib's xlim/ylim; data outside clips honestly.
0.7.0 (Eight Red Rectangles) — 2026-08-02#
The quality release: typed scales, named axes, and honest ASCII — driven by the first full audit.
- The scale specification (
Scale:Linear | Log | Time | Bands, viaPlot::x_scale/y_scale): one typed axis spec replaces the three boolean flags (which remain as sugar); an explicitScale::Bandsdeclares a categorical axis without needing a bar layer — the violin preset now uses it instead of a data-free range. - Axis titles (
Plot::x_label,Plot::y_label): x centered under the tick labels, y written vertically along the left edge; both shed when the frame is tight. - Internal: the plot pipeline split into stage modules (resolve → layout → chrome → draw) — verified byte-identical by the golden suite; crate-level rustdoc rewritten (it had been six releases stale).
0.6.0 (The Knife Grinder) — 2026-08-02#
Time and motion: calendar axes, rolling windows, and live charts.
- Time axes (
Plot::time_x,Ticks::time): unix seconds in, calendars out — a 1s-to-decades interval ladder aligned to real boundaries (Mondays, month firsts), multi-scale labels (14:05, butAug 2at midnight and2027at January), exact Gregorian arithmetic, UTC, no dependencies. - Rolling windows (
stat::Window): trailing mean/sum/min/max with partial starts (no warm-up gap) and gap-aware reductions. - Streaming (
stream::Ring,stream::Live,stream::Rate): a thread-shared sliding window (the library's one lock — producers push, renderers snapshot), an in-place repaint handle (cursor up, erase down, one buffered write: flicker-free, scrollback-safe), and a counter-to-delta helper. One live frame renders in well under a millisecond (seebenches/render.rs).
0.5.0 (Sportsmen) — 2026-08-01#
The statistics release: the mark family is complete, and the statistical charts no terminal library ships are here.
- Range (
mark::Range): the eighth and final mark — vertical intervals with optionalbodyandmarkerchannels, so error bars, boxes, and candles are one mark with channels, not three marks. Band placement (Range::over) shares the categorical axis machinery with bars. - Box plots (
stat::BoxStats,malevich::box_plot): type-7 quartiles, Tukey 1.5×IQR whiskers, outliers as dots. - Densities (
stat::kde,malevich::density,malevich::violin): Gaussian KDE with Silverman bandwidth over linear binning (no FFT); violins as mirrored densities via the new horizontal area orientation (Area::horizontal). - Error bars (
malevich::error_bars): capped Range intervals around measured points.
0.4.0 (Suprematist Composition) — 2026-08-01#
The daily driver: the mark family grows to seven, and the statistical presets with it.
-
Cells (
mark::Cells,scale::Colormap,malevich::heatmap,malevich::hist2d,stat::bins2): value grids as a shade ramp (░▒▓█) colored by a colormap (viridis-like default) — value carried by glyph and color, readable at every tier including plain; grids map onto data coordinates viaCells::extents; empty 2D bins stay honestly blank. -
Area (
mark::Area): baseline fills and between-bands, drawn as vertical subpixel runs — solid in every charset, subpixel edge precision, gap-breaking.stat::stackturns series into cumulative bands for stacked areas. -
Annotations (
mark::Rule,mark::Text): reference lines and notes at data coordinates; both extend the axis domains, draw in the default foreground, and never consume palette slots. -
Steps (
malevich::stairs,malevich::ecdf,stat::ecdf): step charts and empirical distributions as presets over the line mark.
0.3.0 (Airplane Flying) — 2026-08-01#
The pipeline release: the stat layer lands, and ten million points become cheap.
- Histograms (
stat::Bins,mark::Bars::spans,malevich::hist): automatic bin counts (Sturges/Freedman–Diaconis) with nice decimal edges, mergeable bin counts, and contiguous span bars on a numeric axis. - Group-by (
stat::Agg): string-keyed grouping with the shared reducer vocabulary —count,sum,mean,min,max,median— feedingBars::newdirectly. - Log axes (
Plot::log_x,Plot::log_y,Ticks::log10): decade ticks with superscript labels; values at or below zero become gaps, because a log axis cannot place them honestly. - The aggregation pipeline (
stat): M4 downsampling (stat::M4,stat::m4) — min/max/first/last per raster column, pixel-exact for line rendering, mergeable across chunks, gap-preserving — inserted automatically for line layers past four points per subpixel column. Ten million points render end to end in ~28 ms (measured; seebenches/render.rs). Alsostat::lttb(count-targeted, shape-preserving) andstat::Moments(Welford + Chan merge). - SI-prefixed tick labels: axes reaching ±10⁴ (or below 10⁻³) share one prefix
(
20k,2.5M,100µ); the numeric part times the prefix still equals the value exactly, and zero stays bare.
0.2.0 (Red Square) — 2026-08-01#
Color and the next two marks: the chart, the dots, and the bars now look considered at every color tier.
- Half-block (
▀▄█, 1×2) and quadrant (▘▚▟…, 2×2) charsets: solid-block alternatives to braille, selectable per frame. - Legends:
.label("…")on any mark grows a legend row with per-kind colored swatches, shed first when the frame is short. - Themes (
Theme,Frame::theme): the palette as a value —DARK(default),LIGHT(readable on white),COLORFGBGdetection, or any custom palette. - Bars (
mark::Bars,scale::Band,malevich::bar): categorical bar charts from a zero baseline with eighth-block partial tops, coarse below-baseline fills for negative values, band-fitted category labels, and continuous layers (trend lines) positioning over band centers. - Points (
mark::Points,malevich::scatter): unconnected dots; marks now join under the closedmark::Markenum andPlot::layer(impl Into<Mark>). - Color ladder (
Color::{Ansi256, Rgb},ColorMode::{Plain, Ansi16, Ansi256, TrueColor}): honest downhill quantization (RGB → 256-cube → nearest-16), named colors stay palette-relative, run-length encoding merges colors that quantize equal. Detection addsCLICOLOR_FORCE,COLORTERM,256colorterms,TERM=dumb, and non-UTF-8 locale sniffing. - Display-width discipline: labels measured in terminal columns (CJK-safe), wide
glyphs pair with continuation cells and never corrupt alignment, truncation uses an
ellipsis. New dependency:
unicode-width.
0.1.0 (Black Square) — 2026-08-01#
The vertical spine: one mark, every layer of the architecture, done properly.
- The plot pipeline (
Plot,Frame,mark::Line,malevich::line): layered line charts over shared scales with measured (never fixed) layout, collision-aware x labels, chrome shedding in undersized frames, function sampling at raster resolution, a default palette, andDisplayviaFrame::detect. Presets are asserted bit-identical to their grammar expansion. - The examples gallery (
EXAMPLES.md+regen_gallery): deterministic, CI-checked — the showcase and the system test in one artifact. - Rendering (
render::Surface,render::Charset,render::Color): one generic subpixel surface over charset codecs (braille 2×4 and ASCII for now), clipped infallible drawing, text sharing the grid with pixels, plain and run-length ANSI encoders. - Data ingestion (
data::Series,data::IntoSeries): zero-copy fromf64slices, copy-once conversion from all primitive numeric types,NaNpreserved as the gap encoding. - Tick placement (
scale::Ticks): extended Wilkinson (Talbot–Lin–Hanrahan) with exact-decimal labels — labels parse back to their values, share one fraction width per axis, and never show float artifacts. Placement runs in microseconds. - Project scaffold: crate skeleton, terminology contract, CI.