github shergin/malevich v1.20.0
1.20.0 (Dynamic Suprematism)

3 hours ago

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); Viewport is zoom/pan as pure domain arithmetic over
    x_domain/y_domain (decade space on log axes, tail for streams),
    seeded from mapping.viewport(); stat::nearest snaps a cursor to the
    datum that exists. On top, the ratatui adapter grows a StatefulWidget:
    PlotState caches 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-neutral Mouse vocabulary 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 ratatui pans and zooms through millions of points with the
    drawn line pixel-identical to plotting every point — measured: the new
    widget bench 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.md
    is 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.
    fred wears 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;
    sysmon pins its streaming axes with tail, holding a stable
    two-minute window while the rings fill. pan_left/pan_right join 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 the pixel feature, 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 the PlotState; Graphics::present emits
    the pending blocks after terminal.draw in one synchronized write with
    atomic kitty replacement, Graphics::retire/invalidate_pixels handle
    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::retire deletes 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 (p toggles pixel drawing live, --fast halves
    the density for slow links, --cells forces 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_area answers with
    a named Panel value instead of a four-tuple; x_categories /
    y_categories expose a bands axis's labels (not a bare count);
    y_span_at joins x_span_at; Plot::viewport takes its Copy value
    by value; the pixel presenters became Graphics::present and
    Graphics::retire, because the value that names the protocol owns its
    choreography; and the Mouse vocabulary grew ScrollLeft/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 of cell_at, and PlotState::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 --fast is now one call.

  • Docs say the quiet parts. hist2d documents 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::rgb discloses 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 for
    Plot::viewport, which reads only the windows; transform after
    re-seeding, which is the gesture lifecycle anyway. The interaction guide
    gains the modifier-gesture pattern — the Mouse vocabulary 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_domain
    now say what they always did: bounds in either order, the axis always
    ascends.

  • Bars complete the catalog: a base channel (Bars::base) starts each bar
    at a per-bar base instead of zero — bar i spans base[i] .. base[i] + value[i], so the value keeps encoding the segment's length — and
    Bars::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 of
    stat::stack is 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 gains segments, both compositions in one chart pair.

  • Plot::mapping costs 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 (new
    plot/mapping_10m_80x20 row), 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.md and SERDE.md moved 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.

Don't miss a new malevich release

NewReleases is sending notifications on new releases.