Skip to content

ADR 0003: Vega-Lite (validated subset) over Plotly for chart specs

Status: accepted (v0.1.0)

Context

generate_chart is an ordinary tool the model may call when a result is worth charting. The model writes the chart spec, so the spec is untrusted structured output that must be validated before anything treats it as a chart.

Decision

Charts are a subset of Vega-Lite, validated with pydantic (agent/tools/chart.py):

  • mark: one of bar, line, point, arc.
  • encoding: a mapping of channel name to {field, type}, where type is quantitative, nominal, ordinal or temporal.
  • data: a mapping from a key to a list of row objects (inline data).

Vega-Lite is a single declarative JSON grammar (mark + encoding + data), so a small fixed schema covers the common chart shapes. Plotly's figure schema is organised around many trace types, each with its own attribute set, which is a much larger surface to validate against arbitrary model output. An invalid spec is rejected and fed back as an observation so the model can retry. The CLI writes an accepted spec to chart.vega.json.

Consequences and limits

  • Validation is structural only. It does not check that encoding fields exist in data, and it does not validate against the real Vega-Lite JSON Schema. The emitted file has no $schema key, and its data shape (a keyed list of rows rather than {"values": [...]}) may need adapting before a Vega-Lite renderer accepts it. Rendering was not tested.
  • Unknown keys are dropped, not rejected (pydantic's default extra="ignore"): a spec with aggregate, scale or top-level extras validates and the extras vanish from the output. The chart tool therefore rejects wrong shapes and wrong enum values, but does not reject unsupported features.
  • The chart's data is whatever the model put in the spec. It is not tied to the rows returned by a SQL query, so a chart can disagree with the query results. Only the newest spec that passes validation is surfaced (run._last_valid_chart); a rejected spec is never returned.
  • No image is produced; consumers render the JSON themselves.