Docs · Chart spec

One JSON object in, a living chart out

Everything the engine draws is described by one spec. This page is the full reference — the same contract whether the spec arrives through a maker, an embed, an MCP tool call, or the render API.

{
  "type": "column",
  "theme": "editorial",
  "title": "Weekly signups",
  "source": "Product analytics",
  "data": {
    "labels": [
      "Mon",
      "Tue",
      "Wed",
      "Thu",
      "Fri"
    ],
    "series": [
      {
        "name": "Signups",
        "values": [
          12,
          19,
          14,
          27,
          22
        ]
      }
    ]
  }
}

That chart is this exact spec, rendered live.


The shape

Two required fields — type and data.series — and sensible defaults for the rest. Unknown chart types, missing series and non-numeric values are rejected with structured errors — nothing fails silently.

FieldRequiredWhat it is
typeyesOne of the nine types below
themeno (defaults to editorial)editorial · glass · ink · mint · neon (unknown names fall back to editorial)
data.labelsno (defaults to 1, 2, 3…)Category names, one per position
data.seriesyesOne or more { "name"?, "values": number[] } rows
title, sourcenoChrome above and below the chart
options.variantno"light" · "dark" · "auto" — see Themes and variants below
options.targetnoA dashed reference line across cartesian types
options.formatnoValue formatting: "percent" or "currency:XXX"

Negative values are rejected for every type except number — bars and areas measured from a zero baseline cannot show them honestly, and a structured error beats a misleading picture. A negative target is rejected everywhere.


The nine types

TypeData it readsLive examples
column labels + 1..n series see it live
bar labels + 1..n series, horizontal see it live
line labels + 1..n series see it live
area labels + 1..n series, filled to zero see it live
stacked labels + 1..n series, summed per label see it live
donut labels + the FIRST series only see it live
scatter labels + 1..n series, one dot per value see it live
dotplot labels + counts, one drawn dot each (fractional counts are floored) see it live
number the first value of the first series; signed see it live

Changing type on a mounted chart does not redraw it — the engine morphs the existing shapes into the new ones. That is the whole point of the project.


Themes and variants

Five themes, each in a light and a dark variant — ten looks in total. options.variant picks one: "light" and "dark" pin it, and "auto" follows the visitor's OS color scheme. Omitting the field does neither — each theme then shows its own canonical side (editorial, glass and mint are light by nature; ink and neon are dark). Note the honest boundary: "auto" reads the OS preference, not any theme toggle a host page might implement — a page with its own toggle should resolve the variant itself and pass "light" or "dark" explicitly, which is exactly what this site does.


Target lines

A goal, a limit, an SLA: options.target draws a dashed line at a value across column, bar, line, area, stacked, scatter and dotplot charts, with an optional label. The scale always stretches to include it.

"options": {
  "target": { "value": 120, "label": "Goal" }
}

Number formats

options.format runs through every surface at once — axis ticks, value labels, stacked totals, the big number, tooltips. "percent" adds the sign and turns off thousands shortening (70 means 70%, never 70k%); "currency:XXX" prefixes $, € or £ for USD, EUR and GBP and falls back to the code itself for anything else, keeping the k-shortening. Two deliberate exceptions stay plain: a donut slice's share (already a percentage) and a dot plot's counts (neither money nor a share).

"options": { "format": "percent" }        // 70 -> 70%
"options": { "format": "currency:USD" }   // 48200 -> $48.2k

Errors are structured

Parsing never throws and never guesses. A bad spec comes back as a list of {path, message, hint} objects pointing at the exact field — the same shape in the browser, over MCP and from the render API, designed to be read by people and agents alike.

{
  "ok": false,
  "errors": [{
    "path": "data.series[0].values[2]",
    "message": "Value \"abc\" is not a finite number",
    "hint": "Use numeric values, e.g. 42 or 3.14"
  }]
}