draft
AI-assisted chart authoring is a session. A user sees a ranked list, picks one, adjusts the audience, renders, edits, replaces, exports — or abandons. None of those moves are first-class in any visualization library we know of. They live in chat transcripts and analytics events, separated from the chart that occasioned them.
The spine for treating that session as a thing the library knows about. Three composable surfaces, each shipping as a type contract today, with runtime helpers sequenced through the rest of the year. This post walks through what each one is, with two of them runnable inline below.
The arc itself
The first surface is a module-scoped event store with eight variants in a discriminated union:
type ConversationArcEvent = | { type: "suggestion-shown", components, intent?, topScore?, audience? } | { type: "suggestion-chosen", component, rank?, source? } | { type: "audience-set", audience, previous? } | { type: "chart-rendered", component, chartId? } | { type: "chart-edited", component, chartId?, changedProps? } | { type: "chart-replaced", from, to, reason? } | { type: "chart-exported", component, format } | { type: "chart-abandoned", component?, reason? }Default surface is a no-op. enableConversationArc() flips the store on and starts buffering. The default capacity is 1000; new events evict oldest. Subscribers see every event as it lands. There are no network sinks yet — those plug in through subscribe() for now, with first-partyLocalStorageSink,IndexedDBSink, andWebhookSink in the next milestone.
Enable it below and click the event buttons. The log is wired to a real subscriber on the real store:
0 events buffered
Horizontal dot plot. Each dot is one recorded arc event; x is the timestamp, y is the event type, color matches the button above.
Recording is off.
Why bother? Because the data nobody else is collecting is the arc itself:I saw five charts, picked the second, swapped the audience to "executive," edited two props, exported as JSX, and came back the next day to edit it again.That's a sequence, not a single event. Capturing it is what makes a serious recommender feedback loop possible — not just "did the user click on the suggestion" but "did they keep it after using it."
Anchored notes that know how old they are
The second surface is two optional blocks on any annotation:provenance (author, source, confidence, createdAt, stableId) and lifecycle (freshness, ttlHint, anchor). Both are additive — existing annotations arrays keep working.
The lifecycle answer is the Q&A backstop: when the data refreshes and the annotation's reference point shifts, what happens? The anchor modes spell out the four reasonable answers: fixed keeps the recorded coordinate,latest tracks the most recent point,sticky rides along until removed, andsemantic re-resolves throughstableId when new data arrives.
Freshness handles the orthogonal problem: a stale note shouldn't look as authoritative as a fresh one. The chart below has two annotations with different TTLs. Drag "now" forward and watch them fade through fresh → aging → stale → expired:
Hand-placed spikefresh
by alice · 22 days old · TTL P30D
AI anomaly tagfresh
by model-v3 · 0 days old · TTL P14D
The styling above is the shipped default: a single call toapplyAnnotationLifecycle(annotations, { now })classifies each annotation, dims aging, dashes stale, and drops expired from the array. The author's brand color survives the treatment — provenance stays visible while age signals layer on top.
Variants the library didn't think of
The third surface is a registration plug point for variant proposers. Today, every chart variant in Semiotic was hand-curated in a Foo.capability.tsfile. That scales to a few dozen variants. It doesn't scale to "given this specific data shape and this specific audience, propose a configuration nobody wrote down."
import { registerVariantDiscovery } from "semiotic/ai" registerVariantDiscovery((component, capability, context) => { if (component !== "BoxPlot") return [] if (!context.profile.fields[context.profile.primary.y ?? ""]?.bimodal) return [] return [ { id: "RidgelinePlot:bimodal", baseComponent: "RidgelinePlot", intentDeltas: { distribution: 1 }, buildProps: () => ({ bins: 40, amplitude: 1.8 }), rationale: "Distribution is bimodal — Ridgeline reveals the second mode.", source: "model", }, ] })A proposal mixes into the same ranked list suggestChartsproduces, scored against the same rubric. The discovery model can be a heuristic, an LLM call, a future research artifact — the API doesn't care. The scoring side (fit, novelty,risk) lands in M3.
Why these three together
The arc records what happened. The annotations preserve what the user said about it. Variant discovery keeps the system honest about what it doesn't yet know — and where the learning slots in. Together they make a complete AI-assisted authoring session something the library has a vocabulary for, not just something the chat transcript happens to mention.
For the full type contract, see/intelligence/conversation-arc in the docs. For the milestone sequencing through October, the roadmap and the variant-discovery design doc track each surface's M1 → M4 path.