createSpec, pipe, geom.*, scale.*, config — are convenience, not substance. Everything they produce is a plain, immutable object made only of objects, arrays, strings, numbers and booleans. A spec holds no functions, no class instances, no DOM references. That means it round-trips through JSON.stringify / JSON.parse unchanged: you can store a spec in a database, send it over the wire, generate it from another language, or write one by hand.
The builder is just sugar
pipe(...) and the geom.* / scale.* / config helpers just assemble and merge a plain object. This call:
JSON.stringify(spec) gives you exactly this JSON (fields left undefined drop out):
Every top-level key
createSpec seeds six keys — mapping, layers, scales, transforms, highlights and config — so they are present even when empty. The other three appear only once something is piped in:
A styled spec is still JSON
The stylesheet is the largest thing a spec carries, and it round-trips as plainly as the rest. Atoken('brand') reference is an ordinary object the compile stage inlines, and a light/dark colour pair is an ordinary object the renderer picks from at read time — so neither needs a function to survive the trip:
Writing the JSON by hand
Two things to know before you do. A layer’s aesthetic override is calledaes on the builder and mapping in the object: geom.line({ aes: { y: 'profit' } }) serializes as { "type": "layer", "geom": "line", "mapping": { "y": "profit" } }.
And a spec that uses a plugin geom, stat or transform still serializes — a custom layer is just { "geom": "candlestick", "params": { … } } — but the plugin itself is code, and it reaches the chart through the plugins array on GraphProvider rather than through the spec. Store the spec; ship the plugins with your app.
Next
- How a chart is built — the compile → render pipeline that consumes a spec
- Data structure — the table a spec references by column name
- Extending — custom geoms, stats and transforms, and the
pluginsthey ship in

