malevich

Look up

Notebooks and cards

Evcxr, the HTML card, the SVG card, and the contract a terminal card keeps.

Malevich runs in Evcxr, the Rust Jupyter kernel and REPL. There is no wrapper API. The evcxr feature adds rich cell output. Everything else is the ordinary crate.

cargo install --locked evcxr_jupyter
evcxr_jupyter --install

First cell:

:dep malevich = { version = "1.24", features = ["evcxr"] }
use malevich::{Line, Plot};

End a cell with a Plot and Evcxr renders it:

let values = [1.0, 5.0, 2.0, 8.0];
Plot::new().layer(Line::y(&values[..])).title("training")

What a cell shows#

                          training
                     ── loss  ── target
4 ┤   ───╮
  │      ╰──╮
  │         ╰────╮
2 ┤              ╰─────╮
  │                    ╰───────╮
  │▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄╰────────────────────────────
0 ┤
  └┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬
   0      1      2      3      4      5      6      7      8
                             epoch
Plate 1. The HTML card itself, embedded in this page exactly as Plot::to_html emitted it. One <pre> of colored spans, and nothing external.

The chart arrives as a self-contained HTML terminal card: the exact cell grid malevich would print to a terminal, as a <pre> with colored spans. Quadrants and box-drawing stay crisp. Mark colors become RGB spans. Chrome follows the card foreground, and plot text is HTML-escaped. The default frame is 100×26 quadrants on the dark card. Theme::LIGHT selects the light card. The same card travels as image/svg+xml in the bundle, so an export that cannot carry HTML — nbconvert to PDF — still shows the chart. JupyterLab itself prefers the HTML.

HTML rather than an image is a consequence of the design, not a shortcut. malevich owns no font rasterizer, so it hands text drawing to the browser, the same offload it makes to the terminal. The SVG card keeps that rule. Block glyphs become the rectangles they denote. Everything else is text the host's font draws. The adapter adds no dependency.

Quadrants are the default for the same reason they are the terminal default: a notebook's monospace font is a gamble, and 2×2 blocks plus box-drawing are in virtually every one. Denser tiers are one frame away.

Custom frames#

<svg xmlns="http://www.w3.org/2000/svg" width="406.4" height="184" viewBox="0 0 406.4 184" font-family="ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace" font-size="13" fill="#e6edf3" shape-rendering="crispEdges">
<rect width="406.4" height="184" rx="8" fill="#0d1117"/>
<rect x="374.8" y="20" width="7.8" height="8" fill="#e6edf3"/>
<rect x="382.6" y="12" width="7.8" height="8" fill="#e6edf3"/>
<text x="16" y="24.5" textLength="39" lengthAdjust="spacingAndGlyphs" xml:space="preserve">7.5 ┤</text>
<rect x="355.3" y="36" width="3.9" height="8" fill="#e6edf3"/>
<rect x="359.2" y="36" width="3.9" height="8" fill="#e6edf3"/>
<rect x="363.1" y="28" width="3.9" height="8" fill="#e6edf3"/>
<rect x="367" y="28" width="7.8" height="8" fill="#e6edf3"/>
<text x="47.2" y="40.5" textLength="7.8" lengthAdjust="spacingAndGlyphs" xml:space="preserve">│</text>
<rect x="331.9" y="52" width="3.9" height="8" fill="#e6edf3"/>
<rect x="335.8" y="52" width="7.8" height="8" fill="#e6edf3"/>
…
</svg>
training ── loss ── target 4 ┤ ───╮ │ ╰──╮ │ ╰────╮ 2 ┤ ╰─────╮ │ ╰───────╮ │ ╰──────────────────────────── 0 ┤ └┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬ 0 1 2 3 4 5 6 7 8 epoch
Plate 2. The SVG card on the light theme. Every figure on this site is this card, inlined into the page.

Plot::to_html(&frame) and Plot::to_svg(&frame) are the pure, deterministic paths, snapshot-testable like every render path. They need no feature, because a notebook, a README, or a static page is one more terminal:

plot.to_html(&Frame::portable(120, 30))
plot.to_svg(&Frame::portable(120, 30))

With the pixel feature, Plot::to_svg_pixels(&frame, &graphics) is the hybrid pixel render for the same kind of host: chrome stays this card, and the panel is the device-pixel raster. See pixels.md.

Redirect cargo run --example evcxr > plot.html for a standalone fragment you can inspect in a browser, or cargo run --example speedup -- --svg > plot.svg for the SVG card. That is the one the README embeds as an image, because GitHub strips the HTML card's styles.

The terminal-card contract#

                          training
                     ── loss  ── target
4 ┤   ──╮
  │     ╰───╮
  │         ╰────╮
2 ┤              ╰────╮
  │                   ╰──────╮
  │                          ╰──────────────────────────────
0 ┤⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉
  └┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬
   0      1      2      3      4      5      6      7      8
                             epoch
Plate 3. The same grid, with no color: what a stripped card, a log, or a language model reads.

What a host may rely on, for the HTML card and the SVG cell card (Plot::to_html, Plot::to_svg) alike:

  • The grid is the chart. A card is the exact cell grid the terminal renderer would print for the same plot and frame. One <pre> (HTML) or one <text>-and-<rect> group (SVG), one cell per character, box-drawing and block glyphs included. Byte-identical for equal inputs. A snapshot test pins it.
  • Nothing external. No stylesheet, script, font file, or image reference leaves the card. Colors are inline on spans. The font is whatever monospace the host has. A card renders the same in a static page, a README, a notebook, or a mail client.
  • Stripped styles still read. Sanitizing hosts drop inline styles: GitHub's markup, nbconvert with sanitize_html, nbviewer's bleach pass. The HTML card then loses its colors and keeps every glyph in place, so the chart survives as the plain text it also is. Where color must survive, use the SVG card. Hosts that strip styles keep images, and the README's figures are SVG for that reason.
  • Plain text is agent-legible. The same grid, colorless, is what Frame::plain renders and what a language model or a log reader sees: an axis with labels, marks in rows. No card carries information that the plain grid lacks except color, and color never carries a value alone.

The terminal REPL#

The same cell renders in the evcxr terminal REPL through a text/plain fallback: a plain 80×24 plot. With the pixel feature also enabled, that fallback upgrades itself. The REPL is a real terminal, so the plot arrives as a sixel, kitty, or iTerm2 image where one is spoken.

For other crates#

A crate that renders its own types beside malevich charts can join the same card. malevich::evcxr::card_colors(theme) returns the exact background and foreground to_html paints with. malevich::evcxr::mime_bundle emits the stdout protocol. Both are pure functions. Output built on them stays snapshot-testable.

Rough edges#

  • The kernel cannot know the notebook's width. The 100×26 default plus an explicit Frame is the honest interface.
  • A cell is a real compile. The first :dep is the slow one. Evcxr's :cache 500 helps.
  • Views are static: no hover, no zoom. The terminal thesis, kept.