benday
Terminal charts from a Vega-Lite-style JSON spec, drawn in braille dots. Built for AI agents: pipe query results in, get a chart the human can read in the transcript.

Install
Or from source: git clone https://github.com/fwojciec/benday && cargo install --path benday/crates/benday-cli.
Use
Pipe rows in, pass a spec:
|
The spec can instead carry its data inline and arrive on stdin by itself.
benday --help documents everything: the spec grammar, both stdin shapes,
the bar rules, and worked examples — an agent needs nothing else.
The spec
A strict subset of Vega-Lite:
{
"data"?: { // optional — omit to pipe rows in
"values": [ /* one JSON object per row */ ] // tidy rows, OR
// "columns": [ {"name":"day","type":"STRING"} ], "rows": [ ["mon",32] ] // columnar
},
"mark": "bar" | "line" | "point" | "area",
"encoding": {
"x": { "field": "...", "type"?: "quantitative" | "nominal" | "ordinal" },
"y": { "field": "...", "aggregate"?: "sum" | "mean" | "median" | "min" | "max" | "count" },
"color"?: { "field": "..." } // series split, or bar grouping
},
"title"?: "...", "width"?: 72, "height"?: 13 // plot area, in cells
}
Types are inferred from the data when omitted; a declared column type
(INT64, DATE, …) beats inference, and an explicit spec "type" beats
both. Two bar rules follow from the encoding, with no extra flags:
- Orientation. Quantitative
x+ categoricalydraws horizontal bars — built for rankings: one row per bar, height sized to the category count, names in a label column. Categoricalx+ quantitativeystays vertical. - Grouping.
colornaming a third field splits each category into a grouped cluster, one bar per value, with a legend.colornaming the category field itself just tints the bars.
Rows chart in arrival order, so ORDER BY in the producing query is the
sort. Unknown spec fields are errors that name the fix, never silently
ignored — a silently wrong chart is the one failure a caller can't detect.
Data on stdin
With --spec/--spec-file, stdin carries the data, auto-detected between
two shapes:
- Columnar envelope —
{"columns": [...], "rows": [[...]]}, the shape an MCP query tool emits asstructuredContent. Extra keys are ignored, so pipe it straight in;truncatedandtotal_rowsflow to--meta. - Bare array of row objects —
[{"day":"mon","n":32}, ...].
Declared dates chart as ordinal (ISO strings sort chronologically); date bucketing and label formatting belong to the SQL that produced the rows.
Flags and output
--marker braille|octant · --bar-style dots|blocks · --theme benday|lichtenstein|rotogravure · --width/--height · --no-color ·
--meta
Output is deterministic and color stays on when piped — the transcript is
the display. Errors are JSON on stderr with the fix in the message; exit
2 = invalid spec, 3 = data doesn't fit the encoding. --meta prints
scale domains, resolved series colors, and dropped-row counts to stderr,
so a caller can verify what was drawn without parsing dot art.
Development
Two-stage pipeline: compile(spec, data) -> Scene makes every data- and
layout-dependent decision; rasterize(scene) -> glyphs turns the geometry
into cells. A new chart type is compiler work; a new visual style is
rasterizer work. Three test layers: a spec→scene snapshot corpus
(crates/benday-core/tests/cases/), rasterizer unit tests, and a rendered
glyph gallery. make validate runs exactly what CI runs. Design records
live in docs/plans/.
Status
Works: bars (vertical, horizontal, grouped), line, point, area,
multi-series legends, aggregation, type inference, three themes. Planned:
stacked bars, value labels at bar ends, benday schema (JSON Schema
output). Deliberately absent: temporal scales and sort grammar — SQL owns
sorting, bucketing, and date formatting. Negative bars are a hard error,
not a silent miss.
Named for Ben-Day dots: images composed from a raster of small marks, which is what terminal cells are. MIT. Octant glyph table derived from ratatui (MIT).