A chart that an AI can pick correctly, that a screen-reader user can receive, and that carries its own provenance is more useful than one that merely looks right. Those properties are metadata, not pixels — and metadata is portable in a way a rendering engine is not. The IDID portability spec writes three of them down as library-neutral JSON Schemas so they can travel into ecosystems that have no concept of a Semiotic chart.
The payoff: a format adapter (Vega-Lite → chart, dbt test → annotation) stops being just a parser and becomes an export of these ideas. It doesn't reproduce a source's appearance — it carries the capability, audience, and provenance metadata the source never had. That's the difference between a chart that renders and a chart that communicates.
The three primitives
Chart Capability
chart-capability.schema.json
What is this chart good at? — declaratively, so a heuristic or an LLM can rank it against a dataset and a goal without running a chart library.
componentrubricintentScoresvariantscaveats
Audience Profile
audience-profile.schema.json
Who is reading, and what is the org trying to grow? — so a suggestion is calibrated to a real audience, not a generic baseline.
familiaritytargetsexposureLevelreceptionModality
Annotation Provenance & Lifecycle
annotation-provenance.schema.json
Where did this note come from, how much do we trust it, and how does it age? — so a claim on a chart carries its own evidence and expiry.
authorbasisconfidencefreshnessstatusanchor
Every field is labelled with x-idid-status so a reader can tell what's real today (shipped — Semiotic ships all v0.1 fields) from what the spec reserves for the future (proposed). Open string unions (e.g. provenance.source) stay open with a recognized-values list; genuinely closed unions (lifecycle.freshness, status, anchor) use a strict enum. Current spec version:0.1.
View the published schemas (live from /spec/v0.1)
JSON
{ "type": "object", "title": "Chart Rubric", "description": "Three 1–5 axes. familiarity = how well-known the chart is to a general audience; accuracy = how faithfully it represents the data; precision = how readable individual values are.", "required": [ "familiarity", "accuracy", "precision" ], "additionalProperties": false, "properties": { "familiarity": { "type": "integer", "minimum": 1, "maximum": 5 }, "accuracy": { "type": "integer", "minimum": 1, "maximum": 5 }, "precision": { "type": "integer", "minimum": 1, "maximum": 5 } } }
JSON
{ "type": "string", "enum": [ "visual", "screen-reader", "sonified", "agent" ], "description": "The channel this audience receives charts through. Orthogonal to familiarity: a reader can be highly familiar with a chart yet unable to RECEIVE it in their channel (an 8-slice pie is familiar but illegible to a screen reader). A non-visual modality instructs a host to down-rank charts whose meaning does not survive that channel. Defaults to \"visual\" (no receivability bias).", "x-idid-status": "shipped" }
JSON
{ "freshness": { "type": "string", "enum": [ "fresh", "aging", "stale", "expired" ], "description": "Temporal band. When omitted, a host derives it from ttlHint and the data's current temporal extent. Closed union.", "x-idid-status": "shipped" }, "status": { "type": "string", "enum": [ "proposed", "accepted", "disputed", "retracted" ], "description": "Editorial standing, orthogonal to freshness. proposed = placed but unreviewed; accepted = confirmed; disputed = contested; retracted = withdrawn (treat like an expired note). Closed union.", "x-idid-status": "shipped" }, "supersedes": { "type": "string", "description": "provenance.stableId of the annotation this one replaces. Forms a revision chain; the superseded note is typically hidden once its replacement is accepted.", "x-idid-status": "shipped" }, "ttlHint": { "description": "How long this annotation should be considered fresh. Either an ISO 8601 duration string (\"PT24H\", \"P7D\") or a number of milliseconds. Freshness walks fresh → aging → stale → expired as 'now' advances past createdAt + ttlHint.", "oneOf": [ { "type": "string", "description": "ISO 8601 duration" }, { "type": "number", "description": "milliseconds" } ], "x-idid-status": "shipped" }, "anchor": { "type": "string", "enum": [ "fixed", "latest", "sticky", "semantic" ], "description": "Anchor resolution strategy across renders. fixed (default) = pinned to datum coordinates; latest = re-pins to the most recent datum each frame; sticky = stays at last known pixel position after the target scrolls off; semantic = re-resolves via provenance.stableId when new data arrives. Closed union.", "x-idid-status": "shipped" } }
Bidirectional Vega-Lite — the portability proof
Vega-Lite is the closest thing the ecosystem has to a neutral chart interchange format. Semiotic already reads it (fromVegaLite); the spec adds the inverse (toVegaLite), so a chart round-trips through the dominant format with its IDID metadata preserved. Pick a spec and watch it travel in and back out:
1 · Vega-Lite spec (input)
JSON
{ "mark": "bar", "title": "Revenue by region", "data": { "values": [ { "region": "North", "revenue": 128 }, { "region": "South", "revenue": 92 }, { "region": "East", "revenue": 145 }, { "region": "West", "revenue": 71 } ] }, "encoding": { "x": { "field": "region", "type": "nominal" }, "y": { "field": "revenue", "type": "quantitative", "axis": { "title": "Revenue ($k)" } } } }
2 · Semiotic ChartConfig — fromVegaLite()
JSON
{ "component": "BarChart", "props": { "title": "Revenue by region", "categoryAccessor": "region", "valueAccessor": "revenue", "valueLabel": "Revenue ($k)", "data": [ { "region": "North", "revenue": 128 }, { "region": "South", "revenue": 92 }, { "region": "East", "revenue": 145 }, { "region": "West", "revenue": 71 } ] } }
3 · Back to Vega-Lite — toVegaLite()
JSON
{ "mark": "bar", "encoding": { "x": { "field": "region", "type": "nominal" }, "y": { "field": "revenue", "type": "quantitative", "axis": { "title": "Revenue ($k)" } } } }
4 · Rendered Semiotic chart (from the ChartConfig)
Carrying the IDID metadata on the spec
The round trip above loses nothing the format can express — but plain Vega-Lite has no place for capability, audience, or provenance. The binding rides them under usermeta.idid (which every Vega-Lite renderer ignores) so the spec and its meaning travel together. Toggle enrichment to see the metadata appear on the round-tripped spec:
JSON
{ "usermeta": "// toggle above to attach IDID metadata" }
TS
import { unstable_toVegaLite as toVegaLite, unstable_attachIDID as attachIDID, unstable_attachIDIDAnnotations as attachIDIDAnnotations, } from "semiotic/experimental" let spec = toVegaLite(config) // chart → Vega-Lite spec = attachIDID(spec, { capability, audience }) // ride under usermeta.idid spec = attachIDIDAnnotations(spec, [provenancedNote]) // + a note with its evidence // A plain Vega-Lite renderer ignores usermeta and still draws the chart. // An IDID-aware host reads readIDID(spec) and acts on it — see below.
The metadata is actionable, not decorative
Because the audience profile travels on the spec, any IDID-aware host can read it back and route it through the suggestion engine — the same audience that shipped with the chart now calibrates what else to recommend. Here the carried Quarterly exec review profile (which de-prioritizes pie charts in favor of exact comparison) is read off the enriched spec and fed tosuggestCharts for the intentcompare-categories:
suggestCharts(data, { intent, audience: readIDID(spec).audience })
- BarChartscore 6.00
Strong fit for compare-categories (5/5) · 4 rows is in the sweet spot for this chart
- DotPlotscore 4.84
Strong fit for compare-categories (5/5)
- semiotic.recipe.waffle.v0score 3.25
Strong fit for compare-categories (3/5) · Repeated units directly expose how categories compose a total.
What this enables
- Adapters export ideas, not just formats. A Vega-Lite, Mermaid, or dbt adapter that targets these schemas carries legibility/receivability/provenance the source format lacks.
- Round-trip without metadata loss.
fromVegaLite ⇄ toVegaLite proves a chart can pass through the dominant interchange format and come back whole — with its IDID metadata intact under usermeta. - Implementable without this library. The schemas are plain JSON Schema 2020-12, and a zero-dependency reference binding ships at
/spec/bindings/vega-lite.mjs — it carries the IDID metadata on a Vega-Lite spec without importing Semiotic, and its output is byte-compatible with the helpers above. A Python notebook, a BI tool, or a competing chart library can copy it (or the schemas) and have it all mean the same thing.
The runtime helpers above ship behind the unstable_ prefix insemiotic/experimental while the surface is proven against real consumers; the JSON Schemas themselves are the stable artifact. The canonical copies live in the repository's /spec/v0.1 directory.