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
Public domain fields are 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 (packed with semiotic under /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 (toVegaLiteResult), so the tested supported single-view subset round-trips through the dominant format. Unsupported semantics return a typed refusal rather than a plausible fallback. 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 — toVegaLiteResult()
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 supported round trip above preserves its tested data, mark, and encoding subset — 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_toVegaLiteResult as toVegaLiteResult, unstable_attachIDID as attachIDID, unstable_attachIDIDAnnotations as attachIDIDAnnotations, unstable_bindPortableCapability as bindPortableCapability, unstable_readIDID as readIDID, } from "semiotic/experimental" import { getCapability, suggestCharts } from "semiotic/ai" const result = toVegaLiteResult(config) // chart → Vega-Lite if (result.status === "refused") throw new Error(result.diagnostics[0].message) let spec = result.spec 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 binds portable policy to executable host safety logic: const carried = readIDID(spec) const binding = bindPortableCapability( carried.capability, getCapability(carried.capability.component) ) if (binding.status === "refused") throw new Error(binding.diagnostics[0].message) const suggestions = suggestCharts(data, { intent, audience: carried.audience, capabilities: [binding.capability], })
The metadata is actionable, not decorative
Because the capability and audience profile travel on the spec, an IDID-aware host can activate both in its suggestion engine. The portable descriptor deliberately contains no executable data-fit or prop-building functions, so bindPortableCapability overlays its rubric and intent scores onto the host's registered capability while preserving the host's safety gates. Here the carried Quarterly exec reviewprofile and carried BarChart descriptor are both fed to suggestCharts for the intentcompare-categories:
capability binding:success · suggestCharts(data, { intent, audience, capabilities: [bound, …registry] })
- 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.
- Tested supported-subset round trip.
fromVegaLite ⇄ toVegaLiteResult proves a chart can pass through the dominant interchange format with its IDID metadata intact under usermeta; unsupported constructs refuse with diagnostics instead of rendering an approximation. - 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 ship in the npm package under /spec/v0.1 and live in the repository at the same path.