malevich

Look up

Frames and terminals

The charset and color ladders, what detection reads, the overrides, and a live explorer.

How a chart meets a terminal: the charset and color ladders, what detection reads, and the overrides. The argument is Degradation is the contract. This file is the mechanics.

The charset ladder#

Octants — 2×4 blocks, Unicode 16

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

Sextants — 2×3 blocks, Unicode 13

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

Braille — 2×4 dots

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

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

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

Half blocks — 1×2

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

ASCII — 1×1, the guaranteed fallback

training -- loss -- target 4 + ---+ | +---+ | +----+ 2 + +------+ | +------------+ |***********************************+--------------------- 0 + ++------+------+------+------+------+------+------+------+ 0 1 2 3 4 5 6 7 8 epoch
Plate 1. One plot value, at every rung. Block tiers become crisp rectangles on this page, because the SVG card draws them as the rectangles they denote. Braille and box drawing stay text, and the font draws them.

A charset is the glyph tier the subpixel surface encodes through. Glyph tables are data, not code.

charsetsubpixels per cellstanding
Octants2×4 solid blocksUnicode 16; densest ink, explicit opt-in
Sextants2×3 solid blocksUnicode 13; explicit opt-in
Braille2×4 dotsdense opt-in; dots, not blocks
Quadrants2×2 solid blocksthe conservative UTF-8 default
HalfBlocks1×2the lowest Unicode rung
Ascii1×1the guaranteed fallback

The dense tiers are opt-in. No environment variable can prove the configured font covers them. A terminal name is not a font. Frame::detect picks quadrants in any UTF-8 environment, and ASCII otherwise. Octants, sextants, and braille are choices you make for fonts you know.

The gallery's charsets example renders one curve at every rung: cargo run --example charsets.

The color ladder#

TrueColor — 24-bit RGB

                      a heatmap and a line
                           ── a line
10.0 ┤████████████████████████████████████████████████       █ 8
     │████████████████████████████████████████████████       █
 7.5 ┤████████████████████████████████████████████████       █ 6
     │████████████████████████████████████████████████       ▓
     │████████████████████████████████████████████████       ▓ 4
 5.0 ┤▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ▒
     │▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ▒ 2
 2.5 ┤▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ░
     │▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ░ 0
 0.0 ┤▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ░
     └┬────────────┬─────────────┬────────────┬────────────┬
      0           10            20           30           40

Ansi256 — the xterm 256-color palette

                      a heatmap and a line
                           ── a line
10.0 ┤████████████████████████████████████████████████       █ 8
     │████████████████████████████████████████████████       █
 7.5 ┤████████████████████████████████████████████████       █ 6
     │████████████████████████████████████████████████       ▓
     │████████████████████████████████████████████████       ▓ 4
 5.0 ┤▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ▒
     │▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ▒ 2
 2.5 ┤▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ░
     │▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ░ 0
 0.0 ┤▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ░
     └┬────────────┬─────────────┬────────────┬────────────┬
      0           10            20           30           40

Ansi16 — the sixteen named colors, picked in OKLab

                      a heatmap and a line
                           ── a line
10.0 ┤████████████████████████████████████████████████       █ 8
     │████████████████████████████████████████████████       █
 7.5 ┤████████████████████████████████████████████████       █ 6
     │████████████████████████████████████████████████       ▓
     │████████████████████████████████████████████████       ▓ 4
 5.0 ┤▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ▒
     │▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ▒ 2
 2.5 ┤▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ░
     │▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ░ 0
 0.0 ┤▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀       ░
     └┬────────────┬─────────────┬────────────┬────────────┬
      0           10            20           30           40

Plain — no escape byte at all

                      a heatmap and a line
                           ── a line
10.0 ┤░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░       █ 8
     │░░░░░░░▒░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒░░░░░░░░░░░       █
 7.5 ┤░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒░░░░░░░░░       █ 6
     │░░░░▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓███████████▓▓▓▓▒▒▒░░░░░░░░░       ▓
     │░░░░▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓█████████████▓▓▓▒▒▒░░░░░░░░░       ▓ 4
 5.0 ┤░░░░▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓███████████▓▓▓▓▒▒▒░░░░░░░░░       ▒
     │░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒░░░░░░░░░       ▒ 2
 2.5 ┤░░░░░░░▒░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒░░░░░░░░░░░       ░
     │░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░       ░ 0
 0.0 ┤░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░       ░
     └┬────────────┬─────────────┬────────────┬────────────┬
      0           10            20           30           40
Plate 2. The encoder's own SGR bytes, at every color mode, decoded the way a terminal would draw them. Truecolor, the 256-color cube, the sixteen named colors picked in OKLab, and the plain shade ramp with marker cycling.
Charset
Color
Chart
Loading the engine…

Plate 3. The same plot value, re-encoded live by the wasm build for the charset and color mode you choose: nothing is redrawn, only the frame changes.

Four tiers, quantized honestly downhill: TrueColor, Ansi256, Ansi16, Plain. The sixteen-color pick is made in OKLab, by lightness and hue, the two things sixteen colors can carry. A teal drops to green, never to the grey nearer in RGB. Heatmap half-blocks carry independent upper and lower colors. Plain output keeps an averaged shade on the charset's own ramp (░▒▓█, or .:#@ in ASCII). In colorless output, color_by categories cycle portable marker shapes (•, +, x, *, o), so groups never vanish in a pipe.

What detection reads#

Frame::detect sniffs. It never writes to the terminal. The rules, in order:

  • Charset. MALEVICH_CHARSET wins if set to a known name. TERM=dumb or unknown means ASCII. Then the locale, POSIX precedence: LC_ALL, LC_CTYPE, LANG. A locale without utf means ASCII. Otherwise quadrants.
  • Color. NO_COLOR (any value) means plain. Output that is not a terminal means plain, unless CLICOLOR_FORCE or FORCE_COLOR is set and not 0. TERM=dumb or unknown means plain. COLORTERM=truecolor or 24bit, or a TERM ending in -direct, means truecolor. A TERM starting with screen caps at 256, since the multiplexer re-encodes what passes through it. A TERM containing 256color means 256. Otherwise 16.
  • Size. The terminal's reported size. The plot takes a third of the height. No terminal: COLUMNS and LINES when a shell exports them, else 80×16.
  • Theme. COLORFGBG distinguishes dark from light backgrounds.

A pipe is clean plain text by default. Detection sees a non-terminal and drops color, and the charset never emits anything a file cannot hold.

Overrides#

  • MALEVICH_CHARSET — ascii, half, quad, sextants, octants, braille, or auto.
  • MALEVICH_GRAPHICS — kitty, sixel, iterm2, or none: the pixel protocol, outranking the sniff and skipping the probe (feature pixel).
  • NO_COLOR — force plain output (no-color.org).
  • CLICOLOR_FORCE or FORCE_COLOR — keep color when piping.
  • COLUMNS / LINES — size a render that has no terminal to measure.
  • An explicit Frame — set in code. It consults nothing.

Text discipline#

  • CJK labels are measured in display cells and stay aligned.
  • Combining marks are deliberately dropped at the cell grid.
  • Control characters are dropped at the cell grid. A title, label, or category carrying escape bytes can never smuggle them into any encoder's output. The only escapes in ANSI output are the encoder's own SGR sequences. A regression test pins this.
  • NaN is always a visible gap, never interpolated away.

Small frames#

Loading the engine…
Plate 4. Shrink the frame and watch the furniture shed — legend, then titles, then tick density — while the data region is the last thing standing. Grow it and the ticks re-search for a denser labeling.

When the frame shrinks, furniture sheds before data: legend, then titles, then tick density. The data region is the last thing standing, and TERM=dumb at any width still gets a correct chart.