Observable Plot is the modern default for fast exploratory analysis in a notebook. The handoff from that notebook to a production app, though, is a known rewrite tax: the Plot chart was perfect for thinking, and now it has to be rebuilt as a styled, responsive, accessible component. fromObservablePlot — a sibling to the shippedVega-Lite translator — collapses that tax. It maps a Plot spec to a Semiotic ChartConfig, and the chart arrives carrying everything the throwaway version never had.
Why this matters
A chart sketched in a notebook is built for one reader who can see it and already knows the data. A chart in production is read by people on screen readers, by people on phones, by an agent summarizing the dashboard — none of whom the notebook chart ever considered. The expensive part of the rewrite was never the marks; it was re-acquiring the accessible table, the keyboard navigation, the description, the theme tokens, the SSR path. Semiotic already has all of that. So the adapter's job is narrow and high-leverage: translate the encoding faithfully, and let everything past the bare picture come along for free. That's the Land→Expand move made concrete — meet the analyst where they already work, then hand them the capabilities they'd otherwise rebuild.
From a Plot spec to an accessible component
Pick a chart you might have sketched in a notebook. The Plot spec on the left becomes the live, themed, accessible Semiotic chart on the right — same encoding, more capability.
Observable Plot (notebook)
JS
Plot.plot({ marks: [ Plot.lineY(data, { x: "week", y: "users", stroke: "plan" }) ], color: { scheme: "tableau10" } })
Semiotic LineChart (production)
The ChartConfig it compiled to
JSON
{ "component": "LineChart", "props": { "xAccessor": "week", "yAccessor": "users", "lineBy": "plan", "data": "…8 rows", "colorScheme": "category10" } }
What it gained — a generated description (the notebook chart had none)
A line chart of users by week, split by plan. users ranges from 90 (1) to 360 (4), with a mean of 239.88 across 8 points.
Beyond the description: an accessible data table, keyboard navigation, a focus ring, theme tokens, annotation provenance, and an SSR render path — all from the same config, none of it in the original Plot chart.
Wiring it up
TS
import { unstable_fromObservablePlot } from "semiotic/experimental" import { configToJSX } from "semiotic/ai" // The declarative shape of a Plot spec — { marks, x, y, color, … }, each mark // as { type, data?, options? }. (Plot's API is imperative JS; the adapter reads // this declarative form.) const config = unstable_fromObservablePlot({ marks: [{ type: "lineY", data, options: { x: "month", y: "users", stroke: "plan" } }], color: { scheme: "tableau10" }, }) configToJSX(config) // → "<LineChart data={…} xAccessor=\"month\" yAccessor=\"users\" lineBy=\"plan\" … />"
Faithful, or it refuses
The honest hard part is that Plot's API is imperative JS, not declarative JSON, and some of it has no faithful Semiotic equivalent. Where that happens, the adapter warns instead of approximating — a function-valued channel can't be serialized; a layered multi-mark plot translates only its first data mark; a faceted plot is declined towardLinkedCharts. A 70%-faithful adapter that announces its 30% gap is an asset; a 95%-faithful one that hides its 5% is a liability, because the failures are exactly where a non-expert reader and an LLM are both deceived. Every produced config is also checked to emit only props the schema knows, so it round-trips cleanly through fromConfig.
Where this goes
The notebook→production handoff is one instance of a general pattern: a chart authored in a fast, low-ceremony tool needs to graduate into a context with real readers. The same adapter shape serves a docs site embedding a notebook figure, a BI tool exporting an analyst's exploration, or an agent that drafts in Plot and ships in React. In each case the translation is the cheap part; the capabilities the destination demands are the value.
Related