When the model draws the chart
A model writes chart specs for the same question. Lint them: wrong aggregation, truncated axes, bad encodings — and see the honest chart.
GoalLearn why generated charts need a spec, a validator and data provenance, not just a renderer.
“Is the Pro plan growing faster than Basic?”
Data: monthly signups and trial conversion for two plans, Jan–Dec 2025 (24 rows, invented). A promo inflated Basic signups in July.
Show the 24 rows
| month | plan | signups | conv. % |
|---|---|---|---|
| Jan | Basic | 1,200 | 11.8 |
| Jan | Pro | 180 | 22.0 |
| Feb | Basic | 1,230 | 12.1 |
| Feb | Pro | 195 | 22.5 |
| Mar | Basic | 1,250 | 12.0 |
| Mar | Pro | 210 | 23.1 |
| Apr | Basic | 1,270 | 12.3 |
| Apr | Pro | 230 | 23.0 |
| May | Basic | 1,300 | 12.2 |
| May | Pro | 248 | 23.8 |
| Jun | Basic | 1,320 | 12.5 |
| Jun | Pro | 265 | 24.2 |
| Jul | Basic | 1,900 | 9.1 |
| Jul | Pro | 280 | 24.0 |
| Aug | Basic | 1,360 | 12.6 |
| Aug | Pro | 300 | 24.6 |
| Sep | Basic | 1,380 | 12.4 |
| Sep | Pro | 322 | 25.1 |
| Oct | Basic | 1,400 | 12.8 |
| Oct | Pro | 345 | 25.0 |
| Nov | Basic | 1,420 | 12.7 |
| Nov | Pro | 368 | 25.5 |
| Dec | Basic | 1,450 | 13.0 |
| Dec | Pro | 395 | 26.1 |
- error
rate-summedconversionRate is a rate (%). Summing 12 monthly rates produces numbers like 289%, which mean nothing.
FixUse a mean weighted by trials, or plot the rate per month as a line.
- warn
question-mismatchThe question is about growth over time; this chart has no time axis and measures conversion, not signups.
FixPlot signups (or an index of them) against month.
Pro converts twice as well
Conversion rate (%)
Three ways assistants draw charts
Products such as ChatGPT, Claude and Gemini mix these routes, and the mix changes often. The trade-off is the same one as in Lab 01: more freedom for the model, less you can check.
Code executed in a sandbox
The model writes plotting code (Python, JavaScript), a sandbox runs it, and the result comes back as an image or an interactive widget. Maximum flexibility; the chart is only as checkable as the code. ChatGPT's data analysis is the familiar example.
A declarative spec rendered natively
The model emits a chart spec (Vega-Lite, ECharts options, or an in-house grammar) and the client renders it with its own components. Interactive, on-brand, and lintable before drawing, which is what this lab does.
A chart component called as a tool
The model calls something like showChart({ metric, groupBy, range }) and your code fetches the data and picks the encoding. The model never touches numbers or axes. Least expressive, hardest to get wrong.
How it worksclick to expand
The renderer is honest about dishonest specs.It draws what each spec says: a y-axis starting at 1,350, alphabetical months, a summed rate. A renderer that silently “fixed” specs would hide the model's mistake from you.
The linter never looks at the picture. It reads three things: the spec, what each field means (a rate is not additive, months are ordered), and the question. That is enough to catch every flaw here before anything is drawn.
Field metadata is the leverage.The model didn't know conversion is a rate. Your semantic layer does. Put that knowledge in the validator, not in the prompt alone.
function lint(spec, fields, question) {
const f = []
if (spec.y.aggregate === "sum" && !fields[spec.y.field].additive)
f.push("rate-summed")
if (spec.mark === "bar" && spec.y.scale?.zero === false)
f.push("bar-baseline")
if (fields[spec.x.field].kind === "temporal" && spec.x.type !== "temporal")
f.push("time-as-nominal")
if (asksAboutGrowth(question) && isAbsolute(spec.y))
f.push("absolute-for-growth")
return f // block, repair, or ask the model to retry
}