benday-core 0.2.0

Spec-to-chart rendering core for benday: terminal charts from a Vega-Lite-style JSON spec
Documentation

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.

Demo: rendering charts from JSON specs in the terminal

Install

cargo install benday

Or from source: git clone https://github.com/fwojciec/benday && cargo install --path benday/crates/benday-cli.

Use

Pipe rows in, pass a spec:

echo '{"columns":[{"name":"day","type":"STRING"},{"name":"n","type":"INT64"}],
       "rows":[["mon",32],["tue",78],["wed",51]]}' \
  | benday --spec '{"mark":"bar","encoding":{"x":{"field":"day"},"y":{"field":"n"}}}'

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" | "temporal" | "nominal" | "ordinal",
                "timeUnit"?: "year"|"quarter"|"month"|"week"|"day"|"hour"|"minute" },  // buckets time
    "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 + categorical y draws horizontal bars — built for rankings: one row per bar, height sized to the category count, names in a label column. Categorical x + quantitative y stays vertical.
  • Grouping. color naming a third field splits each category into a grouped cluster, one bar per value, with a legend. color naming 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.

Time on the x axis is a real scale, not evenly-spaced labels. A DATE, DATETIME, TIMESTAMP, or TIME column — or any string column whose every value is ISO — charts on a calendar axis: ticks land on week / month / quarter boundaries and irregular gaps take proportional space. Line, point, and area plot true positions; bars need a "timeUnit" to bucket first (calendar truncation, so "month" maps 2026-06-14 to 2026-06). count with no value field is an events-per-bucket histogram — the raw-logs story:

echo '[{"ts":"2026-06-14T08:03:00"},{"ts":"2026-06-14T08:41:00"},{"ts":"2026-06-14T10:12:00"}]' \
  | benday --spec '{"mark":"bar","encoding":{"x":{"field":"ts","timeUnit":"hour"},"y":{"field":"ts","aggregate":"count"}}}'

Timestamps without an offset read as UTC civil time; with Z or ±hh:mm they normalize to UTC. Explicit "type": "ordinal" restores evenly-spaced dates.

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 as structuredContent. Extra keys are ignored, so pipe it straight in; truncated and total_rows flow to --meta.
  • Bare array of row objects[{"day":"mon","n":32}, ...].

Declared dates chart on a calendar axis — true spacing, calendar ticks; "type": "ordinal" restores even spacing for periods SQL already bucketed.

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, temporal scales (calendar ticks, timeUnit bucketing), type inference, three themes. Planned: stacked bars, value labels at bar ends, benday schema (JSON Schema output). Deliberately absent: a sort grammar — SQL owns sorting, and owns bucketing when it is there; benday owns time when SQL is absent. 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).