-
Notifications
You must be signed in to change notification settings - Fork 137
Expand file tree
/
Copy path.windsurfrules
More file actions
397 lines (308 loc) · 81.4 KB
/
Copy path.windsurfrules
File metadata and controls
397 lines (308 loc) · 81.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# Semiotic — AI Assistant Guide
## Quick Start
- Install: `npm install semiotic`
<!-- semiotic-bundle-sizes:start -->
<!-- Auto-generated by scripts/sync-bundle-sizes.mjs — do not edit by hand. -->
- **Use sub-path imports** — `semiotic/xy` (138KB gz), `semiotic/ordinal` (117KB gz), `semiotic/network` (123KB gz), `semiotic/geo` (97KB gz), `semiotic/realtime` (150KB gz), `semiotic/realtime/core` (145KB gz), `semiotic/realtime/react` (1KB gz), `semiotic/server` (190KB gz), `semiotic/server/node` (190KB gz), `semiotic/server/edge` (208KB gz), `semiotic/utils` (76KB gz), `semiotic/utils/core` (74KB gz), `semiotic/utils/react` (6KB gz), `semiotic/recipes` (77KB gz), `semiotic/recipes/core` (77KB gz), `semiotic/recipes/react` (2KB gz), `semiotic/themes` (7KB gz), `semiotic/themes/core` (7KB gz), `semiotic/themes/react` (6KB gz), `semiotic/data` (4KB gz), `semiotic/value` (8KB gz), `semiotic/physics` (133KB gz), `semiotic/physics/matter` (1KB gz), `semiotic/physics/rapier` (1KB gz), `semiotic/ai` (432KB gz), `semiotic/ai/core` (70KB gz), `semiotic/controls` (12KB gz), `semiotic/rough` (4KB gz). Full `semiotic` is 349KB gz.
<!-- semiotic-bundle-sizes:end -->
- CLI: `npx semiotic-ai [--schema|--compact|--examples|--doctor|--audit-a11y]` · MCP: `npx semiotic-mcp`
## Architecture
HOC Charts (simple, default) → Stream Frames (full control). Use HOCs unless you need control they don't expose. Stream Frames pass `RealtimeNode`/`RealtimeEdge` wrappers in callbacks, not your data. Every HOC accepts `frameProps`, has an error boundary + dev-mode validation. TypeScript `strict: true`.
## Common Props (all HOCs)
`title`, `description` (aria-label), `summary` (sr-only), `width` (600), `height` (400), `responsiveWidth`, `responsiveHeight`, `margin`, `className`, `color` (uniform fill), `stroke`, `strokeWidth`, `opacity`, `enableHover` (true), `tooltip` (boolean | "multi" | function | config), `showLegend`, `showGrid` (false), `frameProps`, `onObservation`, `onClick`, `chartId`, `loading`, `loadingContent` (ReactNode; `false` suppresses), `emptyContent`, `legendInteraction` ("none"|"highlight"|"isolate"), `legendPosition` ("right"|"left"|"top"|"bottom"), `emphasis` ("primary"|"secondary"), `annotations`, `accessibleTable` (true), `hoverHighlight` (requires `colorBy`), `hoverRadius` (30), `animate` (boolean | {duration?, easing?, intro?}), `axisExtent` ("nice"|"exact" — pins first/last tick to data min/max; XY x/y + ordinal value axis only).
**Primitive styling** (`color`/`stroke`/`strokeWidth`/`opacity`): apply to any shape the chart draws. Precedence: top-level prop > `frameProps.*Style` fn return > HOC base > theme. Use CSS vars (`stroke="var(--semiotic-border)"`) for cascade-overridable theming. Per-datum: use `frameProps.pieceStyle`/`pointStyle`/`lineStyle` fn form.
`onClick` receives `(datum, { x, y })`. `onObservation` receives `{ type, datum?, x?, y?, timestamp, chartType, chartId }`.
## XY Charts (`semiotic/xy`)
**LineChart** — `data`, `xAccessor` ("x"), `yAccessor` ("y"), `lineBy`, `lineDataAccessor`, `colorBy`, `colorScheme`, `curve`, `lineWidth` (2), `showPoints`, `pointRadius` (3), `fillArea` (boolean|string[]), `areaOpacity` (0.3), `lineGradient`, `anomaly`, `forecast`, `band` ({y0Accessor, y1Accessor, style?, perSeries?, interactive?} or array for fan charts; participates in yExtent; non-interactive by default), `directLabel`, `gapStrategy`, `xScaleType`/`yScaleType` ("linear"|"log"|"time"), `styleRules` (per-series), `tooltip="multi"` for hover-anywhere
**AreaChart** — LineChart props + `areaBy`, `y0Accessor`, `gradientFill`, `areaOpacity` (0.7), `showLine` (true), `band`, `tooltip="multi"`
**DifferenceChart** — Two-series A/B. Fills between with `seriesAColor` where A>B, `seriesBColor` where B>A; crossovers interpolated. `data`, `xAccessor`, `seriesAAccessor` ("a"), `seriesBAccessor` ("b"), `seriesALabel`/`seriesBLabel`, `seriesAColor` (var(--semiotic-danger))/`seriesBColor` (var(--semiotic-info)), `showLines` (true), `lineWidth` (1.5), `showPoints` (false), `pointRadius` (3), `curve` ("linear"), `areaOpacity` (0.6), `gradientFill`, `xExtent`/`yExtent`, `pointIdAccessor`, `windowSize`. Push via `ref.push({x,a,b})`. Accessor outputs coerce through `toNumber`.
**StackedAreaChart** — flat array + `areaBy` (required), `colorBy`, `normalize`, `baseline` ("zero"|"wiggle" streamgraph|"silhouette" centered), `stackOrder` ("key"|"insideOut"|"asc"|"desc"). Streamgraph: `baseline="wiggle"` + `stackOrder="insideOut"`. `baseline` ⊥ `normalize`. No `lineBy`. `tooltip="multi"` interpolates between samples.
**Scatterplot** — `xAccessor`, `yAccessor`, `colorBy`, `sizeBy`, `sizeRange`, `symbolBy` (categorical field → glyph **shape**: each mark becomes a d3-shape glyph; size still tracks `sizeBy`/`pointRadius`), `symbolMap` ({category → shape}; unmapped auto-assign — pass it for legend-matchable shapes), `pointRadius` (5), `pointOpacity` (0.8), `marginalGraphics`, `styleRules` (per-point; `axis:"x"`/`"y"` thresholds), `regression` (boolean | "linear"|"polynomial"|"loess" | RegressionConfig — sugar for trend overlay)
**BubbleChart** — Scatterplot + `sizeBy` (required), `sizeRange` ([5,40]), `regression`
**ConnectedScatterplot** — + `orderAccessor`, `regression`
**QuadrantChart** — Scatterplot + `quadrants`, `xCenter`, `yCenter`
**MultiAxisLineChart** — Dual Y-axis. `series` (`[{yAccessor, label?, color?, format?, extent?}]`). Falls back to multi-line if ≠ 2 series.
**Heatmap** — `xAccessor`, `yAccessor`, `valueAccessor`, `colorScheme`, `showValues`, `cellBorderColor`
**ScatterplotMatrix** — `fields` (numeric field names)
**MinimapChart** — Overview + detail with linked zoom. Wraps an XY chart.
**CandlestickChart** — `xAccessor`, `highAccessor` (req), `lowAccessor` (req), `openAccessor`+`closeAccessor` (optional → OHLC; high/low only → range). `candlestickStyle` ({upColor, downColor, wickColor, rangeColor, bodyWidth, wickWidth}). Honors `mode`.
## Ordinal Charts (`semiotic/ordinal`)
**BarChart** — `categoryAccessor`, `valueAccessor`, `orientation`, `colorBy`, `sort`, `barPadding` (40), `roundedTop`, `gradientFill` (true | {topOpacity, bottomOpacity} | {colorStops}; tip→base), `styleRules`, `regression`
**StackedBarChart** — + `stackBy` (required), `normalize`, `sort` (false default — insertion order), `styleRules`
**GroupedBarChart** — + `groupBy` (required), `barPadding` (60), `sort` (false default), `styleRules`
**SwarmPlot** — `colorBy`, `sizeBy`, `symbolBy` (categorical field → glyph shape, like Scatterplot), `symbolMap`, `pointRadius`, `pointOpacity`
**BoxPlot** — + `showOutliers`, `outlierRadius`
**Histogram** — + `bins` (25), `relative`. Always horizontal.
**ViolinPlot** — + `bins`, `curve`, `showIQR`
**RidgelinePlot** — + `bins`, `amplitude` (1.5)
**DotPlot** — + `sort` ("auto"), `dotRadius`, `showGrid` (true default), `regression`
**PieChart** — `categoryAccessor`, `valueAccessor`, `colorBy`, `startAngle`
**DonutChart** — PieChart + `innerRadius` (60), `centerContent`
**FunnelChart** — `stepAccessor`, `valueAccessor`, `categoryAccessor?`, `connectorOpacity`, `orientation`
**SwimlaneChart** — `categoryAccessor`, `subcategoryAccessor` (req), `valueAccessor`, `colorBy` (defaults to subcategoryAccessor), `orientation`, `roundedTop` (pixel radius on outer ends of each lane; middles stay square; single-segment lanes round all four)
**LikertChart** — `categoryAccessor`, `valueAccessor`|`levelAccessor`+`countAccessor`, `levels?`, `orientation`, `colorScheme`
**GaugeChart** — `value` (req), `min`, `max`, `thresholds`, `arcWidth`, `cornerRadius` (rounded segment ends), `sweep`, `fillZones`, `showNeedle`, `centerContent`
All ordinal: `colorBy`, `colorScheme`, `categoryFormat` (string|ReactNode), `showCategoryTicks` (true).
**`styleRules`** (declarative threshold-aware styling — **every chart family**) — an ordered `StyleRule[]` where each `{ when, style }` matching a mark contributes its style, merged in list order so **the last applicable rule wins per property** (CSS-cascade model). Wired on: **ordinal** BarChart/StackedBarChart/GroupedBarChart (bars/segments); **XY** LineChart/AreaChart/StackedAreaChart (per-series) + Scatterplot/BubbleChart/QuadrantChart/ConnectedScatterplot (per-point); **network** ForceDirectedGraph/SankeyDiagram/ChordDiagram (nodes); **geo** ChoroplethMap (features) + ProportionalSymbolMap/DistanceCartogram (symbols); **physics** GaltonBoardChart/PhysicsPileChart/CollisionSwarmChart/EventDropChart (particles). `when` = predicate `(datum, ctx) => boolean`, a declarative threshold (`{ axis?: "x"|"y"|"value", field?, gt, gte, lt, lte, eq, ne, within:[min,max], outside, in:[...] }`), or `true`/omitted (always). `ctx` channels by family: bars `{ value, category }`; XY `{ value(=y), x, y }` (use `axis:"x"`/`"y"` to target either axis regardless of accessor field name); network/geo/physics `{ value, category }` (category = the colorBy/group). A rule's `style.fill` may be a color string or a **`HatchFill`** descriptor (`{ type:"hatch", background?, stroke?, spacing?, angle?, lineWidth?, lineOpacity? }`) resolving to a `CanvasPattern` on canvas and an SVG `<pattern>` in SSR — one declaration, both backends. Precedence: top-level primitives > per-mark style fn (`pieceStyle`/`pointStyle`/`nodeStyle`) > `styleRules` > base `colorBy`/`color`/theme. Works through `renderChart`/MCP for all wired families (use the declarative-threshold form, not predicates, when the config must serialize). `HatchFill` is also a `band`/`x-band` annotation `fill` and any scene `style.fill`. Caveats: line/area rules resolve per-series (sample datum, not per-vertex); network hierarchy charts (Tree/Treemap/CirclePack/Orbit) are NOT wired (their `colorByDepth` owns fill); MultiAxisLineChart/MinimapChart/CandlestickChart not wired. Exported from `semiotic` + each family entry + `semiotic/utils`: `StyleRule`, `resolveStyleRules`, `matchesThreshold`, `makeRuleValueResolver`, `makeXYRuleContext`, `makeNodeRuleContext`, `composeStyleRules`, `HatchFill`, `isHatchFill`. See `/features/style-rules`.
## Network Charts (`semiotic/network`)
**ForceDirectedGraph** — `nodes`, `edges`, `nodeIDAccessor`, `sourceAccessor`, `targetAccessor`, `colorBy`, `nodeSize`, `nodeSizeRange`, `edgeWidth`, `iterations` (300), `forceStrength` (0.1 — link-attraction multiplier), `layoutExecution` ("auto" default | "worker" | "sync" — auto runs big layouts in a Web Worker by estimated cost, sync fallback everywhere), `layoutLoadingContent` (ReactNode while worker layout pends; `false` suppresses), `onLayoutStateChange` (`"pending"|"ready"|"error"`), `showLabels`, `nodeLabel`, `styleRules` (style groups of nodes — rules see the raw node; `ctx.category` = colorBy group)
**SankeyDiagram** — `edges`, `nodes`, `valueAccessor`, `nodeIdAccessor`, `colorBy`, `edgeColorBy`, `orientation`, `nodeAlign`, `nodeWidth`, `nodePaddingRatio`, `showLabels`
**ProcessSankey** — temporal sankey with real time x-axis. `nodes`, `edges` (each with `startTime`/`endTime`), `domain` (req `[t0, t1]`), `axisTicks?`, `xExtentAccessor` (optional `[start, end]` lifetime per node), `colorBy`/`colorScheme`/`showLegend`/`legendPosition`, `pairing` ("value"|"temporal"), `packing` ("off"|"reuse"), `laneOrder` ("crossing-min"|"inside-out"|"crossing-min+inside-out"|"insertion"), `lifetimeMode` ("full"|"half"), `ribbonLane` ("source"|"target"|"both"), `showLaneRails`, `showLabels` (true), `showQualityReadout`, `showParticles` + `particleStyle`, `timeFormat`/`valueFormat`, push API via ref. Static-graph cycles OK as long as edges move forward in time. Use ProcessSankey for time-stamped events; SankeyDiagram for static snapshots.
**ChordDiagram** — `edges`, `nodes`, `valueAccessor`, `edgeColorBy`, `padAngle`, `showLabels`
**TreeDiagram** — `data` (root), `layout`, `orientation`, `childrenAccessor`, `colorBy`, `colorByDepth`
**Treemap** — `data` (root), `childrenAccessor`, `valueAccessor`, `colorBy`, `colorByDepth`, `showLabels`
**CirclePack** — `data` (root), `childrenAccessor`, `valueAccessor`, `colorBy`, `colorByDepth`
**OrbitDiagram** — `data` (root), `childrenAccessor`, `orbitMode`, `speed`, `animated` (true), `colorBy`
## Geo Charts (`semiotic/geo`)
Import from `semiotic/geo` only — avoids d3-geo in non-geo bundles.
**ChoroplethMap** — `areas` (GeoJSON Feature[] or "world-110m"), `valueAccessor`, `colorScheme`, `projection` ("equalEarth"), `graticule`, `tooltip`, `showLegend`, `styleRules` (flag features — rules see the feature with `properties` flattened; `fill` may be a HatchFill)
**ProportionalSymbolMap** — `points`, `xAccessor` ("lon"), `yAccessor` ("lat"), `sizeBy`, `sizeRange`, `colorBy`, `areas?`
**FlowMap** — `flows`, `nodes`, `valueAccessor`, `edgeColorBy`, `lineType`, `showParticles`
**DistanceCartogram** — `points`, `center`, `costAccessor`, `strength`, `showRings`
All geo: `fitPadding`, `zoomable`, `zoomExtent`, `onZoom`, `dragRotate`, `graticule`, `tileURL`, `tileAttribution`. Helpers: `resolveReferenceGeography("world-110m"|"world-50m")`, `mergeData(features, data, {featureKey, dataKey})`.
## Physics Charts (`semiotic/physics`)
Import from `semiotic/physics` only — not re-exported from the root `semiotic` entry (keeps the physics kernel out of default dashboards).
Process/arrival/distribution charts backed by `StreamPhysicsFrame`. The settled projection is the chart; motion is explanatory context. Use when the movement has data semantics, not as decoration.
**GaltonBoardChart** — `data`, `valueAccessor` ("value"), `bins` (21), `mode` ("sample"|"mechanical"), `pegRows`, `mechanicalCount`, `branchProbability`, `ballRadius`, `colorBy`, `styleRules` (per-particle; `ctx.category` = colorBy group), `seed`, `size`/`width`/`height`, `paused`, `frameProps`. Renders a deterministic Galton/Plinko-style distribution that settles into bins; mechanical mode can generate a seeded no-data demonstration.
**EventDropChart** — `data`, `timeAccessor` ("time"), `arrivalAccessor` ("arrivalTime"), `windows` ({size}), `watermark` ({delay}|fn), `timeScale`, `ballRadius`, `colorBy`, `seed`, `size`, `paused`, `frameProps`. Use for event-time arrival, lateness, and watermark-window stories.
**PhysicsPileChart** — `data`, `categoryAccessor` ("category"), `valueAccessor` ("value"), `mode` ("sample"|"mechanical"), `mechanicalCount`, `mechanicalCategories`, `unitValue` (1), `ballRadius`, `colorBy`, `seed`, `showProjection`, `size`, `paused`, `frameProps`. Unitizes values into repeated bodies that settle into category piles; mechanical mode can generate a seeded no-data capacity sketch; the default projection overlay keeps exact totals readable.
**CollisionSwarmChart** — `data`, `xAccessor` ("x"), optional `groupAccessor`, `radiusAccessor`, `pointRadius`, `xExtent`, `collisionIterations`, `settle`, `showProjection`, `colorBy`, `seed`, `size`, `paused`, `frameProps`. Uses springs plus collisions to separate overlapping dots while preserving the quantitative axis and optional group lanes.
**PhysicalFlowChart** — `nodes`, `links`/`edges`/`data`, `nodeIdAccessor` ("id"), `nodeXAccessor` ("x"), `nodeYAccessor` ("y"), `sourceAccessor` ("source"), `targetAccessor` ("target"), `throughputAccessor` ("value"), `pathAccessor` ("path"), `coordinateMode` ("auto"|"normalized"|"pixels"), `particleRate`, `maxParticles`, `particleRadius`, `flowSpeed`, `pathConstraint` ("path"|"none"), `reducedMotion`, `showStaticFlow`, `showNodeLabels`, `showSensors`, `paused`, `seed`, `size`. Experimental physics-backed flow chart where packets move along authored node coordinates or link paths while a static throughput layer keeps route quantities readable.
**ProcessFlowChart** — multi-body workflow lane. `data`, `stages` (required: `[{id, label?, force?, damping?, capacity?, pressure?, portal?, absorb?, share?}]`), `stageAccessor` ("stage"), `idAccessor`, `groupBy` (optional feature key; completion when all members hit an absorb stage), `groupLabelAccessor`, `workAccessor`, `radiusAccessor`, `ballRadius` (6), `colorBy`, `groupCompletion` ("allAbsorbed"|"none"), `groupAnchorAlong` (0.55), `showProjection` (true), `showChrome` (true — processChrome kit), `liveCapacity` (true — FIFO queues at `unitsPerSecond`), `onCapacityChange` (queue depth / processed), `bodyLimit` (soft stream budget + oldest eviction), `bodyMark` ("circle"|"halo"|"faceted"|"pill"|"diamond"|"square" or per-row `datum.__physicsMark`), `selection`, `settle`, `seed`, `size`, `paused`, `frameProps`. Settled projection is stage occupancy + capacity badges; use for review queues / triage / merge pipelines. Prefer **GauntletChart** for one compound plan with timed gate effects.
**Physics controllers** (`createCapacityQueueController`, `createPortalController`, `composePhysicsControllers`) — process plugins via `controllers`. Capacity `getSnapshot()` → queueDepth/processedCount; emit `physics-capacity-processed`. **processChrome** (`semiotic/physics` / `semiotic/recipes`) — stage bays, capacity badges, feature sockets (theme: `--semiotic-process-*`). `PhysicsCustomChart`: `layout()` may return `regionEffects`, `controllers`, `bodyForces`; `layoutConfig` hot path without re-enqueue. Guide: `/features/physics-process-guide`. Contracts: `PhysicsContracts.test.tsx`.
**GauntletChart** — compound project core + tethered positive/negative property bodies + timed gate events. `positiveProperties`/`negativeProperties` (req), `gates`, `events`, `showChrome` (true), `showProjection` (true — viability/outcome strip), `showTethers` (true), `onStateChange`, `frameProps`. Bodies clamp inside walls (`clampGauntletPoint`). Not for multi-item factory floors (use ProcessFlowChart).
**Pop (body-removal burst)** — every physics HOC ref is a `PhysicsFrameHandle` (extends the shared push handle) exposing **`popBodies(ids, options?)`** (`StreamPhysicsPopOptions` = `{ color?, durationMs?, radius? }`): removes the bodies and plays a burst — expanding ring + inner glow + radial sparks fading over `durationMs` (`drawPopAnimations`) — returning the removed ids. It reads as a *departure*, the physics/exit-emphasis counterpart to realtime **`pulse`**'s data-*arrival* glow (the same transient-emphasis metaphor on opposite ends of a datum's life). GauntletChart also fires it internally on gate-driven property removal; `/examples/nimby` (civic-value balloons) and `/examples/merge-pressure` (merge-risk traits) drive it that way.
## Value Charts (`semiotic/value`)
Single-focal-value displays — when one number is the answer, a chart is the wrong abstraction. Plain React (no Stream Frame); SSR-clean; ~7KB gz. **Ships no chart-family dependency** — embed your own Semiotic chart via two slots picked by aspect ratio.
**BigNumber** — `value` (required), `label`, `caption`, `format` ("number"|"currency"|"percent"|"compact"|"duration"|fn), `locale`, `currency`, `precision`, `prefix`/`suffix`/`unit`, `comparison` ({value, label?, format?, direction?}), `target` ({value, label?, format?, direction?}), `delta` (explicit override), `deltaFormat`, `showDeltaPercent` (true), `direction` ("higher-is-better" default | "lower-is-better" | "neutral"), `sentiment` ("auto" default | "positive" | "negative" | "neutral"), `thresholds` ([{at, level: "success"|"warning"|"danger"|"info"|"neutral", color?, label?}] — resolved by highest `at` ≤ value, painted via `--semiotic-{level}`), `windowSize` (60 — caps push buffer surfaced via `getData()` / `slotCtx.pushBuffer`), `mode` ("tile" default | "presentation" | "inline" | "thumbnail"), `align`, `padding`, `emphasis`, `color`/`background`/`borderColor`/`borderRadius`, `animate` (boolean | {duration?, easing?, intro?} — tweens between value changes), `stalenessThreshold` (ms; dims after no-push interval), `staleLabel`, slot overrides (`headerSlot`/`valueSlot`/`deltaSlot`/`footerSlot`/`trendSlot`/`chartSlot` — ReactNode or `(ctx) => ReactNode`), `chartSize` (px reserved for `chartSlot`; defaults to inner card height).
**Two chart slot positions, picked by chart aspect:**
- **`trendSlot`** — wide / rectangular charts beneath the value (LineChart, AreaChart, DifferenceChart in `mode="sparkline"`). Renders at full card width.
- **`chartSlot`** — square charts beside the value (DonutChart, PieChart, Scatterplot, Treemap, CirclePack). Splits the card horizontally: text-on-left, chart-on-right.
- Pair both: square chart anchors top-right, wide trend stretches across the bottom.
- Slot context `(ctx) => ReactNode` exposes `{ value, formattedValue, level, color, delta, deltaFormatted, deltaPercent, sentiment, isStale, pushBuffer }` — embedded charts read `ctx.color` to theme-link to the resolved threshold.
Push API via `forwardRef`: `ref.current.push(value | {value, time?, comparison?})`, `pushMany`, `clear`, `getValue()`, `getData()`. Stable across renders (refs back the imperative handle).
ARIA: auto sentence-form label combining `{label}: {formatted} {unit}, {up|down} {delta} ({percent}) from {comparison.label}, {target%} of {target.label}[, stale]`. Override via `description`; supplement via `summary` (sr-only).
Semantic classes: `semiotic-bignumber` root + `--mode-{...}` / `--level-{...}` / `--sentiment-{...}` / `--stale` modifiers; `__text-region`, `__value`, `__delta`, `__delta-row--{up|down|flat}`, `__arrow--{up|down|flat}`, `__trend` (wide slot wrapper), `__chart` (square slot wrapper), etc.
Helpers exported: `buildFormatter`, `formatSignedDelta`, `formatDeltaPercent`, `formatDuration`, `resolveThreshold`, `colorForLevel`, `buildSparklinePath` (for custom-slot rendering).
## Realtime Charts (`semiotic/realtime`)
Push API: `ref.current.push({time, value})`. All pushed data must include a time field.
**RealtimeLineChart**, **RealtimeHistogram** (+ `brush`, `onBrush`, `linkedBrush`, `direction`; **stacked** via `categoryAccessor` + `colors` — bars sum by category within each bin; **mirrored/diverging** via `direction="down"` flipping the value domain — pair two halves with a shared `timeExtent`/`valueExtent` for an up/down detail view, and overlay extra instances on the same extent for layered envelopes), **TemporalHistogram** (static sibling — same props minus `windowSize`/`windowMode`), **RealtimeSwarmChart**, **RealtimeWaterfallChart**, **RealtimeHeatmap**, **Streaming Sankey** (StreamNetworkFrame + `showParticles`).
Encoding: `decay`, `pulse`, `transition`, `staleness` — compose freely.
### Push API on HOC charts
Most HOCs support push via `forwardRef`. **Omit** `data` — do NOT pass `data={[]}`.
```jsx
const ref = useRef()
ref.current.push({ id: "p1", x: 1, y: 2 })
ref.current.pushMany([...points])
ref.current.replace([...points]) // ordinal only — bounded-ingest, preserves category order + transitions
ref.current.remove("p1" | ["p1","p2"]) // requires ID accessor
ref.current.update("p1", d => ({ ...d, y: 99 })) // requires ID accessor
ref.current.clear()
ref.current.getData()
ref.current.getScales() // {o, r, projection} (ordinal) | {x, y} (XY) — null if unmounted
ref.current.getCustomLayout() // custom charts: the most recent layout(ctx) result (readback — don't re-run the layout host-side); null before first layout / on built-ins
<Scatterplot ref={ref} xAccessor="x" yAccessor="y" pointIdAccessor="id" />
```
ID accessor: `pointIdAccessor` (XY/realtime), `dataIdAccessor` (ordinal), `nodeIDAccessor`/`edgeIdAccessor` (network). `replace()` is ordinal-only — used by aggregator HOCs like LikertChart. Network HOC refs operate on nodes; for edges use `StreamNetworkFrameHandle` directly: `removeNode(id)`, `removeEdge(sourceId, targetId)` or `removeEdge(edgeId)`, `updateNode(id, updater)`, `updateEdge(sourceId, targetId, updater)`.
**Controlled→push bridge**: `useSyncedPushData(ref, rows, { id, resetKey })` (from `semiotic` / `semiotic/realtime`) reconciles a controlled React array into the push buffer — diffs by id, issues the minimal push/update/remove, and clears + rebuilds on `resetKey` change. Reach for it instead of hand-rolling the mirror when rows live in React state; pass rows to the hook, not `data`. Pairs with `useStreamStatus` (live/stale badge). Pure core `syncPushBuffer` is exported for testing.
Not supported: Tree, Treemap, CirclePack, Orbit, ChoroplethMap, FlowMap, ScatterplotMatrix.
## Custom Charts (escape hatch)
When the catalog doesn't fit, four HOCs take a layout function emitting scene primitives. Frame still owns hit testing, transitions, decay, theme, SSR.
- **`XYCustomChart`** (`semiotic/xy`) — waffle, calendar heatmap, custom point/line/area
- **`OrdinalCustomChart`** (`semiotic/ordinal`) — marimekko, parallel coords, bullet, fan, slope
- **`NetworkCustomChart`** (`semiotic/network`) — flextree, dagre, custom force/radial, packed-cluster beeswarm matrix
- **`GeoCustomChart`** (`semiotic/geo`) — isometric landmark boards, custom geographic tessellations
Layout signature differs by family:
- **XY/Ordinal**: `layout: (ctx) => { nodes, overlays? }`. `ctx`: `data`, `scales` ({x,y} XY | {o,r,projection} ordinal), `dimensions` (plot rect — center-anchored for radial ordinal, top-left otherwise), `theme`, `resolveColor(key)`, `config`.
- **Network**: `layout: (ctx) => { sceneNodes?, sceneEdges?, labels?, overlays?, htmlMarks? }`. `ctx`: `nodes`, `edges`, `dimensions`, `theme`, `resolveColor(key)`, `config`, `selection` (shared-selection predicate `{ isActive, predicate(datum) }` from `LinkedCharts`, `null` when unwired — dim/highlight by it). Run external positioners (`d3-flextree`, `dagre`) then emit network scene primitives (circle/rect/arc/**symbol**/**glyph** nodes; line/bezier/curved edges). The `symbol` node is the per-datum **shape** channel — a `d3-shape` glyph (`circle`/`square`/`triangle`/`diamond`/`star`/`cross`/`wye`/`chevron`, or a custom `path`) sized by `size` (area), rendered on canvas + SVG/SSR and hit-tested + keyboard-navigated as a unit. **The `symbol` mark is cross-pipeline**: XY and ordinal custom layouts emit it too (`{type:"symbol", x, y, size, symbolType}` — note `x`/`y`, vs the network variant's `cx`/`cy`), and Scatterplot/SwarmPlot expose it as the `symbolBy` encoding. One shared `symbolPath` implementation backs canvas/SVG/hit-test across all three families.
- **The `glyph` node — composite pictograms (ALL FOUR families incl. geo)**: where `symbol` is one path, `glyph` stamps a multi-part vector pictogram — a `GlyphDef` (`{viewBox?, anchor?, parts: [{d, fill?, stroke?, strokeWidth?, opacity?}]}`) whose parts declare **role paints** (`"color"`/`"accent"`/literal) resolved per node (`color`/`accent` props), so one definition recolors per category like a pictogram plate reused in many inks. `{type:"glyph", x, y, size, glyph, color?, accent?, fraction?, fractionStart?, fractionDirection?, ghostColor?, rotation?, style, datum, pointId?}` (network variant uses `cx`/`cy` + `id`/`label`). `size` = rendered **height** px (width follows viewBox aspect); `anchor: [0.5, 1]` stands a sign's feet on a baseline/terrain. **Partial fills**: `fraction`/`fractionStart` clip a `[start, end]` window (horizontal or bottom-up vertical) with an optional full-extent `ghostColor` silhouette — the ISOTYPE partial-symbol convention, fed directly by `unitize`. Full pipeline citizen: canvas + SVG/SSR, hit-test + keyboard nav over the drawn bounds, `pointId`/`id` annotation anchoring, and enter/move/exit transition identity (which `symbol` lacks). Datum-less glyph nodes (`datum: null`) paint but don't hit-test/navigate — use one `hitTarget` per logical mark under a multi-sign tally. `<Glyph def size color …/>` (from `semiotic/recipes`) renders the same definition as React SVG for overlays/legends/chrome; `glyphPlacement`/`glyphExtent` expose its geometry for layout math.
- **Geo**: `layout: (ctx) => { nodes?, overlays? }`. `ctx`: `areas`, `points`, `lines`, fitted `GeoScales`, `dimensions`, `theme`, `resolveColor(key)`, `config`, `selection`. Emit `geoarea`/`point`/`line`/`glyph` nodes; use overlays for labels and sprites.
- **`htmlMarks`** (network only): `NetworkHtmlMark[]` = `{ id, x, y, width, height, content: ReactNode }`, positioned in the **same plot space as `sceneNodes`** and rendered into one real-DOM layer the framework places **above the canvas and SVG `overlays`** (stack: canvas → `overlays` → `htmlMarks`). Reach for it over an SVG `<foreignObject>` when a mark is **text-heavy/rich and dims or animates on hover** — a real `<div>` composites `opacity`/`transform`/`visibility` changes instead of re-rasterizing text (the `foreignObject` stall on large graphs). Framework owns the margin (and future zoom/pan) transform so marks stay pixel-aligned; each mark is its own element, keyed by `id` (position-only re-runs reposition without remounting). `pointer-events: none` by default — keep a transparent hit-rect `sceneNode` per mark so canvas hit-testing/`onObservation` stays authoritative. Mark `content` can read `useCustomLayoutSelection()` to dim on shared selection without a relayout. Additive: omit it and no extra DOM renders. Class hooks: `.semiotic-network-html-marks` (layer) / `.semiotic-network-html-mark` (each).
**Custom-chart authoring kit** (`semiotic/recipes`; `hitTarget*` also from `semiotic/xy`/`ordinal`/`network`). The shape every hand-built custom chart converges on — draw real marks in `overlays`, emit a **transparent scene node per mark** for interaction — is first-class:
- **`hitTargetPoint`/`hitTargetRect`** (XY/ordinal) + **`networkHitTarget`** (circle or rect) + **`geoHitTarget`** (geo — same transparent `PointSceneNode`; project lon/lat via `ctx.scales.projectedPoint` first): a zero-opacity, fully-transparent, hit-tested node from `{x, y, (r|width,height), datum, id}`. The `id` becomes the node's `pointId`/`id` (annotation anchor + nav-tree leaf) **and** its transition key. This is how a custom chart inherits **accessibility** (keyboard nav, focus ring, data table), **annotation** anchoring, **AI**/`onObservation` + shared selection, and **chart-mode** transitions for free — replaces the `rgba(0,0,0,0)`+`opacity:0`+`pointId`+`_transitionKey` boilerplate. The visible glyph lives in `overlays`; the focus ring still draws on the invisible target. A keyboard-focused **geoarea** outlines its polygon (a shape focus ring) rather than a centroid dot.
- **Radial coordinate kit**: `polarToXY`/`xyToAngle` (0 = up, clockwise), `angleScale`/`radiusScale`, `ringArcPath` (annular-sector / wedge / full-ring path), `TAU` — angle ⟂ radius for two-continuous-channel radial charts (the radial analogue of the chrome kit).
- **Edge-router kit** (custom network edges): `curvedEdgePath` (S-curve + near-level side-bow), `orthogonalEdgePath`, `boxEdgeAnchors` (box exit/entry by direction), `fanOutBend` (fan parallel edges apart). Plus cubic-Bézier evaluation — `cubicPoint`/`cubicTangent` (sample a point/tangent along a `CubicCurve` to *seat a mark on the curve* — a node mid-edge, an arrowhead at the end) and `cubicPath` (serialize to SVG).
- **2D vector kit**: `addPoints`/`subtractPoints`/`scalePoint`/`pointMagnitude`/`normalizePoint` — the point math any hand-built radial or network layout re-derives (an edge offset normal to its tangent, a spoke, a leader line). Operates on the shared `Point`, composes with the radial + edge kits.
- **Interval/timeline**: `packIntervals` (greedy Gantt sub-track packer), `activeCountOverDomain` (concurrency step series).
- **`runs`/`runLengthEncode`**: collapse a per-step categorical/boolean series into drawable runs (condition strips, status timelines, calendar ribbons).
- **Cyclical math** (day-of-year, hour, compass bearing): `wrapValue`, `shortestArcDelta`, `cyclicRangeContains`, `selectCyclicRange`.
- **`axisFixedForcePositions`** (+ shared **`rectCollide`** positioner): pin one axis from a data field, relax the other with edge attraction + an anchor spring + **rectangular** (label-box) collision — the "time is structural, the graph settles the cross-axis" family that hierarchical recipes don't cover. `axisFixedForceLayout` wraps it as a ready `NetworkCustomLayout`.
- **Chrome**: `linearAxis` (tick axis + gridlines from *any* scale — the bespoke-scale escape hatch `showAxes` can't cover), `legendSwatches` (portable SVG legend for `overlays` — fill/line/shape/hatch swatches; sibling to `legendGroupsFrom` which feeds `frameProps.legend`), `hatchFill` (`{def, fill}` SVG `<pattern>` for percentile/uncertainty bands — the SVG analogue of `createHatchPattern`).
- **`unwrapDatum`** (`semiotic/recipes` + `semiotic/utils`): collapse the wrapped-vs-raw datum split — always the raw user object (handles both `.data` wrappers and `.datum` nesting). **The** unwrap path for `onObservation` handlers AND `frameProps.tooltipContent` renderers: call it once on the incoming value; never pre-unwrap the argument (`unwrapDatum(x?.data ?? x)` double-unwraps).
`semiotic/recipes` ships pure layout functions (`waffleLayout`, `calendarLayout`, `marimekkoLayout`, `bulletLayout`, `parallelCoordinatesLayout`, `intervalLanesLayout`, `flextreeLayout`, `dagreLayout`, `lineageDagLayout`, `axisFixedForceLayout`, `packedClusterMatrix`, `isometricLandmarkLayout`, `forceLayout`, `arcLayout`, `adjacencyMatrix`, `circularLayout`). **Network-analysis kit** (`semiotic/recipes`, pure graph algorithms): `buildAdjacency`, `bfsDistances`, `shortestPath`, `egoNetwork`, `degree`/`betweenness`(Brandes)/`closeness`/`clustering` (+ `normalizeScores`), and `proximityProblem` — the "spatial problem" layout diagnostic that flags nodes drawn closer than their graph distance warrants. Pair with network charts to size by centrality, highlight an ego network on hover, trace a shortest path, or diagnose a misleading layout. `forceLayout(nodes, edges, {seed})` is a **seeded, deterministic** positioner returning normalized `{id:{x,y}}` for `NetworkCustomChart` (same seed ⇒ same layout; re-seed for a "re-run the layout" interaction); `forceLayoutAsync(nodes, edges, {execution?, workerThreshold?, signal?})` is its Promise sibling that runs large layouts in a short-lived module Web Worker (identical deterministic output; graceful sync fallback), and `useForceLayout(nodes, edges, options)` → `{positions, status, error}` is the React wrapper — SSR and first hydration stay synchronous for markup parity, client graph changes go async while previous positions stay visible, and settled positions are memoized by node/edge array identity + options so remounting the same module-constant graph is "ready" immediately (no loading flash). `arcLayout`/`adjacencyMatrix`/`circularLayout` (+ `orderByGroupDegree`, `arcPath`) are the classic physics-free network forms. `allocateCells` is the largest-remainder grid allocator behind `waffleLayout` (turn `{key, weight}[]` + a cell count into integer cells with no rounding drift; `minPerCategory` keeps small categories visible) — reusable for any feature-mix / proportional waffle. **`unitize(value, {unit, maxUnits?, minFraction?})` / `unitizeRange(value, rangeValue, opts)`** is the counting sibling: the pictogram/tally allocator (value → repeated unit signs with a fractional final sign — ISOTYPE: symbols repeat, they never grow). Returns `{units: [{index, fraction, start, end, value}], total, shown, overflow}` — `maxUnits` caps with an `overflow` flag, `minFraction` drops trailing slivers while `total` vs `shown` keeps the ledger honest; `unitizeRange` extends the tally to a projected/scenario endpoint (`rangeUnits` drawn hatched), sharing a mid-sign boundary exactly via `startFraction`. Feeds `glyph`-node `fraction`s directly (unit charts, sign stacks, arrow bundles; `allocateCells` divides fixed cells, `unitize` counts). **Tokenized reasoning helpers**: `generateTokens(input, tokenEncoding)` wraps `unitize` plus `actual`, `fixed-denominator`, `quantile`, `posterior-sample`/`sample`, and seeded `random-sample` strategies with explicit `tokenType` (`dot`/`icon`/`glyph`), `tokenSemantics` (`observed-unit`, `unitized-measure`, `risk-case`, `possible-outcome`, etc.), and `countStrategy`; `{ value, rangeValue }` yields `rangeTokens` for scenario/projection tallies. `layoutTokenGrid` places the resulting tokens for icon/glyph arrays; `normalizeTokenEncoding` keeps legacy `token`/`unit` aliases working while canonical configs use `icon`/`unitValue`; `diagnoseTokenEncoding`, `suggestTokenEncoding`, and `tokenTaskIntentToCapabilityIntents` expose IDID-style warnings, task-aware defaults, and a bridge to `suggestCharts` intents. Built-in token glyph names include `person`, `server`, `chip`, `bolt`, and `bus`. `intervalLanesLayout` (ordinal) packs concurrent `{start,end,lane}` records into stacked Gantt sub-tracks per lane with period bands + lane labels + a time axis — packing runs in rendered-pixel space and honors `minBarWidth` (2) so zero/short-duration events stay visible without overlapping same-track neighbors; `axisFixedForceLayout` (network) pins one axis from a field and settles the other (rect-aware collision). BYO heavy deps (`d3-flextree`, `dagre`) in user code. `packedClusterMatrix` (network) bins records into a column×row matrix of **densely-packed beeswarm clusters** (deterministic self-contained packing, geometry cached) and emits **multi-channel glyphs** — hue (`colorAccessor`/`colorMap`), size (`sizeAccessor`, area), shade (`shadeAccessor`, CIELAB lightness), plus EITHER shape-encoding (`symbolAccessor`/`symbolMap` — the base mark becomes that shape) OR the **composite-glyph** model (`iconAccessor`/`iconMap` — base is a filled circle, only mapped values get a stroked inner icon). `rowMode:"banded"` (default) gives aligned global orbit-bands (row labels align, one enclosure spans the columns per band, columns vary in height) vs `"stacked"` (per-column cell heights ∝ count). `callouts:[{field,value,label}]` draws leader lines to named marks. `cellSizing:"proportional"` makes area ∝ count. **Recipe chrome kit** (exported from `semiotic/recipes`, for any custom-layout's `overlays`): `roundedEnclosure`/`boundsOf` (group/band borders), `bandLabel` (overflow-aware axis/band labels), `markCallout` (leader-line callout to a mark), `readField` (`node.data`-wrapper reader), `groupBy`, `dimFor` (the highlight/dim opacity rule — `{predicate?, highlight?, baseOpacity?, dimOpacity?, brighten?}`; `matchesHighlight` is its `{field,value}[]` matcher), `signatureKey`/`LayoutCache` (content-signature geometry cache so re-styling never re-runs an expensive layout — key by *content*, never by `ctx.nodes` identity), `legendGroupsFrom` (`{colorMap|keys, symbolMap?, sizeStops?}` → `LegendGroup[]` for `frameProps.legend`), `shade`/`makeShade`, `symbolPathString`/`symbolRadius`/`symbolExtent`/`SYMBOL_SEQUENCE`, and the small numeric/color one-liners every layout re-declares — `clamp`, `mean`, `withAlpha`, `nonNegativeFinite` (hex→`rgba()` so a hover-dim can ride a recipe's `resolveColor` callback). (`bandLabel`/`dimFor` are adopted by marimekko/bullet/parallelCoordinates/packedClusterMatrix.) `lineageDagLayout` renders a pre-positioned **layered lineage/DAG** (reads logical layer/row coords, no re-layout) with composite node glyphs (one hit-rect per node + icon/label/store-chip chrome in `overlays`), level-of-detail collapse (full→compact→icon→dot), distinct dashed back-edges, and host-driven reach-dimming (`layoutConfig.reachableIds`) + selection (`layoutConfig.selectedId` / shared `ctx.selection`).
`tokenLayer({input, encoding, options})` is the high-level tokenized-rendering helper for custom layouts: it runs `generateTokens`, applies row/column/grid/waffle/dotplot/bar-segment/quantile-strip placement (or `positionToken` for scale/map-driven placement), and returns ordinary dot/symbol/glyph scene nodes with `pointId`/transition identity. Use it for ISOTYPE, icon arrays, risk grids, quantile dotplots, strips, and hybrid token overlays; pass `includeRange` to render `rangeTokens`; drop down to `generateTokens` when a bespoke layout only needs records.
**Chrome (labels/axes/legends): the recipe owns it.** Recipes emit own chrome via `overlays` return field (ReactNode painted on top). Built-in axes via `showAxes` on the HOC work for layouts respecting the standard scale. Recipe convention: `showXxx` boolean toggles, `xxxFormat` callbacks. Shipped recipes' toggles: marimekko `showCategoryLabels`, bullet `showLabels`+`showTicks`, parallelCoordinates `showAxes`, flextree/dagre `showLabels`, waffle/calendar none.
**Interaction (hover/brush/selection): the parent owns it.** Recipes are pure — they take predicate props (e.g. `parallelCoordinatesLayout`'s `highlightFn?`) and the parent manages state via `onObservation` (`{type: "hover" | "hover-end" | ...}`), feeding a derived predicate back into `layoutConfig`. Matching rows render at full opacity; non-matching dim; highlighted z-order on top.
**Notes:**
- Coords are plot-relative (frame translates by `margin`). Read `ctx.dimensions.plot`. Radial ordinal: `plot.x = -width/2`, `plot.y = -height/2` (center-translated).
- Layouts needing axis domains: pass `xExtent`/`yExtent` (XY) or `oExtent`/`rExtent` (ordinal) — those flow through scale construction *before* the layout runs.
- Streaming layouts: ingest via ref (`push`/`pushMany`); layout re-runs on each ingest. Overlays update on data-change paths, NOT per-frame.
- **On `NetworkCustomChart`, a `layoutConfig` change re-runs the layout (`buildScene`) WITHOUT re-ingesting the node/edge topology** — so drive interaction state, styling, or animation progress through `layoutConfig` for cheap per-frame updates; swapping `nodes`/`edges` (or the chart `width`/`height`) is the heavier path that re-ingests. Custom overlays are read straight from the store at render time (the frame's repaint re-reads them), so a recipe returning fresh JSX every layout call needs no per-frame `setState`.
- Custom layouts own their colors — always prefer `ctx.resolveColor(key)` over hardcoded literals. `CategoryColorProvider` integration is XY-only; for cross-chart sync on network/ordinal customLayouts, pass matching `colorScheme` to each.
- All four custom HOCs accept `selection` / `linkedHover` / `chartId` like the built-in HOCs: hover/click emit into the shared selection store, and the resolved predicate arrives as `ctx.selection` so the layout can dim/highlight by a cross-chart selection. Host-owned per-render dimming (e.g. a graph-reachability set) is orthogonal — pass it through `layoutConfig` and read `ctx.config`.
- **Selection restyle without a relayout** (the cheap hover path): by default a `ctx.selection` change re-runs the layout (rebuilds sceneNodes + repaints + rebuilds the quadtree). To restyle on hover/selection *without* re-positioning, opt in two ways: (1) return a **`restyle(node, selection)`** (network also `restyleEdge`) from the layout result — its presence makes a selection change re-apply styles to the existing scene **off each mark's base style** and just repaint (no relayout, no quadtree rebuild); compute geometry once in the layout body, express dimming in `restyle`. (2) For the React **`overlays`**, call **`useCustomLayoutSelection()`** (from `semiotic`/`semiotic/recipes` or the family entry) → `{ isActive, predicate }`; the frame swaps only the context value on selection change, so subscribing overlay components re-render while the canvas/quadtree stay untouched. Express selection/highlight through the selection store (not `layoutConfig`) to ride this path.
- **Annotations**: all four custom HOCs accept `annotations`. XY/ordinal/network custom marks can anchor by `pointId`; Geo annotations use geographic coordinates, while sprite- or tile-specific callouts can be emitted directly in the layout overlay.
- Tooltips: emit datum keys matching user-visible accessor names. Avoid underscored synthetic keys (default tooltip filters those out).
## Coordinated Views
**LinkedCharts** — `selections`. **CategoryColorProvider** — `colors`|`categories` + `colorScheme`.
Chart props: `selection`, `linkedHover`, `linkedBrush`. Hooks: `useSelection`, `useLinkedHover`, `useBrushSelection`. Works for `NetworkCustomChart` too — the resolved selection predicate is threaded into the custom layout as `ctx.selection`.
**`useSelectionActions(name)`** — write-only access (`selectPoints`/`clear`) that does NOT subscribe to selection state, so a *container* can push a selection (e.g. from a hover handler) without re-rendering; only the leaf consumers reading the selection re-render. The provider-at-top / consumers-at-leaves pattern for interaction-heavy coordinated views.
**Shared categories inside LinkedCharts → wrap in `CategoryColorProvider`.** Gives identical per-category colors AND makes LinkedCharts render one unified legend (suppressing individual chart legends). Without it, mismatched colors and duplicate legends.
**Linked crosshair**: `linkedHover={{ name: "sync", mode: "x-position", xField: "time" }}`. Click locks crosshair (dashed white); click/Escape unlocks.
**Linked series highlight** (series↔bar cross-highlight): `linkedHover={{ name: "sync", mode: "series" }}` auto-resolves the chart's series-identity field (colorBy/lineBy/areaBy/stackBy/groupBy) and keys the linked selection off it — no hand-wired `fields`. Add `seriesField: "region"` to override (align charts whose series live under different prop names). Modes are exclusive: `mode` is `"field"` (default) | `"x-position"` (crosshair) | `"series"`.
**CircularBrush** — accessible range brush over a **cyclical** domain (day-of-year, hour, compass): `value` ({start,end}) / `onChange` (value | updater) / `period` (365) / `radius` / `step`+`largeStep` / `formatValue` / `arcFill`/`stroke`. Wrap-around ranges, pointer-capture drag, and full keyboard control (each handle + the range is a `role="slider"`, ←/→ nudges, Shift = `largeStep`). The radial counterpart to the linear `RealtimeHistogram` brush. **Control-surface contract**: it takes value/domain/geometry/`onChange` and never reaches into a chart — layer it over a chart sharing its coordinate space and feed `onChange` into your state (or the selection store). Built on the cyclical + radial kit.
Also: **ScatterplotMatrix**, **ChartContainer** (`title`, `subtitle`, `actions`, `notifications` — `ChartNotification[]` chart-level notices with no mark to anchor to, e.g. audit/data-pitfall findings or user-authored notes; `{ id?, level? ("info"|"success"|"warning"|"error"|"neutral" → semantic role colors), title?, message, source?, dismissible? }`, collapsed into a severity-colored toolbar bell + count badge (bell adopts the most severe visible level's icon/color) that opens a popover of dismissible cards — an overlay, so notices never reflow the plot; sr-only aria-live region announces count + severity, + `onNotificationDismiss`), **ChartGrid** (`columns`, `gap`), **ContextLayout**.
## Server-Side Rendering (`semiotic/server`)
HOC charts render SVG automatically in server environments. For standalone generation:
```ts
import { renderChart, renderChartWithEvidence, renderToImage, renderToAnimatedGif, renderDashboard } from "semiotic/server"
const svg = renderChart("BarChart", { data, categoryAccessor, valueAccessor, theme: "tufte", showLegend, showGrid, annotations })
const { svg: svg2, evidence } = renderChartWithEvidence("BarChart", { data, categoryAccessor, valueAccessor }) // evidence: { markCount, markCountByType, empty, xDomain?, yDomain?, categories?, nodeCount?, edgeCount?, annotationCount, ariaLabel, warnings } — ground truth from the rendered scene; check `evidence.empty`/`markCount` instead of parsing SVG. MCP renderChart returns the same block.
const png = await renderToImage("LineChart", { ... }, { format: "png", scale: 2 }) // requires sharp
const gif = await renderToAnimatedGif("line", data, { xAccessor, yAccessor, theme: "dark" }, { fps: 12, transitionFrames: 4, decay: { type: "linear" } }) // requires sharp + gifenc
const dashboard = renderDashboard([{ component: "BarChart", props }, { component: "PieChart", colSpan: 2, props }], { title, theme, layout: { columns: 2 } })
```
All accept `theme` (preset name or object); theme categorical colors flow to data marks. `generateFrameSVGs()` returns frame SVGs without sharp/gifenc.
AnimatedGifOptions: `fps`, `stepSize`, `windowSize`, `frameCount`, `xExtent`/`yExtent` (lock axes), `transitionFrames`, `easing`, `decay`, `loop`, `scale`.
Server SVGs include `role="img"`, `<title>`, `<desc>`, grid, legend, annotations. SVG groups have stable `id` attrs for Figma layer naming: `data-area`, `axes`, `grid`, `annotations`, `legend`, `chart-title`.
`renderChart` props match the same accessors documented per-chart above. Sparkline has no axes/grid/legend/title by default (margin 2px). ForceDirectedGraph: materialize `nodes` before passing (don't infer from edge endpoints).
All components also accept: `width`, `height`, `theme`, `title`, `description`, `showLegend`, `showGrid`, `background`, `annotations`, `margin`, `colorScheme`, `colorBy`, `legendPosition`. Pass frame-level props via `frameProps`.
## Annotations
All HOCs accept `annotations`. Coordinates use data field names.
**Positioning**: `widget`, `label`, `callout`, `callout-circle`, `callout-rect`, `text`, `bracket`
**Reference lines**: `y-threshold` (`value`, `label`, `color`, `labelPosition`), `x-threshold`, `band` (`y0`, `y1` — a `null`/omitted bound extends to the axis min/max on that side, e.g. `y1: null` shades "at least `y0`"), `x-band` (`x0`, `x1`, `fill`, `fillOpacity` — full-height vertical region for eras/phases; a `null`/omitted bound likewise extends to the domain edge; only skipped when the axis has no scale at all)
**Ordinal**: `category-highlight`
**Enclosures**: `enclose`, `rect-enclose`, `highlight`
**Label backgrounds**: every region-bounding annotation (`y-threshold`, `x-threshold`, `band`, `x-band`, `enclose`, `rect-enclose`, `category-highlight`) accepts `labelBackground` for a legibility backdrop behind the label text — `"halo"`/`true` (stroke halo in the plot bg; default for threshold/band labels), `"box"` (semitransparent rounded panel), `"none"`/`false` (plain), or a config `{ type: "halo"|"box", fill?, opacity? (0.85), padding? ({x,y}), radius? (3), stroke?, haloWidth? (3) }`. One shared renderer (`AnnotationLabel`) drives client + SSR, so it works on every frame and in server SVG. `band`/`x-band` `fill` also accepts a `HatchFill` for hatched regions.
**Statistical**: `trend`, `envelope`, `anomaly-band`, `forecast`
**Streaming anchors**: `"fixed" | "latest" | "sticky" | "semantic"` — also exposed as `lifecycle.anchor` on the `semiotic/ai` annotation lifecycle. `"semantic"` is typed but currently falls back to fixed positioning; stableId-based re-resolution remains open.
**Hierarchy**: any annotation accepts `emphasis: "primary" | "secondary"` across XY, ordinal, network, geo, and static SVG rendering. `secondary` dims (opacity 0.6) and yields z-order; `primary` paints at full weight and on top. Type-agnostic, no-op when unset; class hooks `annotation-emphasis--{primary|secondary}` for further styling.
**Connectors**: `connector: { end?: "arrow"; type?: "line" | "curve"; curve? }`. `type: "curve"` draws a swoopy quadratic-bezier connector (bend = `curve` × connector length, default 0.25; negate to bow the other way); the arrowhead aligns to the curve's tangent. Default is a straight line.
**Auto-placement** (opt-in): `autoPlaceAnnotations` (boolean | config) on any HOC runs the `annotationLayout` recipe — collision-avoiding offsets for notes without manual `dx`/`dy`, curved connector routing when placement must go far. Config: `defaultOffset`, `notePadding`, `markPadding`, `edgePadding`, `preserveManualOffsets` (true), `routeLongConnectors` (true), `connectorThreshold`.
**Density** (opt-in, within `autoPlaceAnnotations`): `density` (true | `{ maxAnnotations?, areaPerAnnotation? (20000), minVisible? (1) }`) sheds lowest-priority note annotations when the plot is over-crowded — priority = `emphasis` (`primary` never shed) → `provenance.confidence` → `lifecycle.freshness` (`expired` first); reference lines/bands/overlays never count. `progressiveDisclosure: true` keeps shed notes tagged `_annotationDeferred` (`.annotation-deferred`, hidden until chart `:hover`/`:focus-within`) instead of dropping them; the persistent set is always shown. Pure forms in `semiotic/recipes`: `annotationDensity({ annotations, width, height, ... }) → { visible, deferred, budget }` and `annotationBudget(w, h)`. `diagnoseConfig` flags `ANNOTATION_DENSITY` when notes exceed the budget.
**Association/redundant cues** (opt-in, within `autoPlaceAnnotations`): `redundantCues: true` gives a colored `text` note offset from its anchor (the one note type that draws no connector) a faint leader line back to the anchor — a spatial, CVD-safe cue instead of color-alone matching. `auditAccessibility` flags color-only association as `perceivable.annotation-association` (warn) and treats `redundantCues` as satisfying it; `diagnoseConfig` flags `ANNOTATION_FAR_NO_CONNECTOR` / `ANNOTATION_LONG_CONNECTOR` for connector-necessity.
**Responsive/cohesion** (opt-in, within `autoPlaceAnnotations`): `responsive: true` (or `{ minWidth }`, default 480) sheds `secondary`-emphasis notes once the plot narrows past the breakpoint (keeps `primary`/unmarked); composes with `density`, and with `progressiveDisclosure` defers instead of drops. `cohesion: "blended" | "layer"` (also a per-annotation field; per-annotation wins) — `blended` adopts mark colors/typography (default look), `layer` renders a distinct editorial layer (`--semiotic-annotation-color`, italic) via the `annotation-cohesion--*` class.
**Audience/defensive** (M6): per-annotation `defensive: true` is never shed by density/responsive (joins the floor) so it survives into every export; with `provenance`, the layout pass bakes `source`+`confidence` visibly into the label (`"… (AI · 70%)"`). `autoPlaceAnnotations: { density: true, audience }` accepts an `AudienceProfile` (anything with a `familiarity` map) and scales the density budget by aggregate familiarity — low-familiarity keeps more notes (×1.5), expert fewer (×0.6).
**Editorial visibility**: `filterAnnotationsByStatus(annotations, { showRetractedAnnotations?, showSupersededAnnotations? })` returns the current note set without applying styles. `applyAnnotationStatus` uses the same visibility rule; `describeChart` and `buildNavigationTree` skip retracted and superseded notes by default.
## Theming
CSS custom properties: `--semiotic-{bg, text, text-secondary, border, grid, primary, secondary, surface, success, danger, warning, error, info, focus, font-family, annotation-color, legend-font-size, title-font-size, tick-font-family, tick-font-size (12px), axis-label-font-size (12px), tooltip-{bg, text, radius, font-size, shadow}}`.
```jsx
<ThemeProvider theme="tufte"> {/* named preset */}
<ThemeProvider theme={{ mode: "dark", colors: { categorical: [...] } }}> {/* merge onto dark base */}
```
**Color priority** (with `colorBy`): CategoryColorProvider/LinkedCharts map > `colorScheme` > ThemeProvider `colors.categorical` > `"category10"`. `colorScheme` accepts a named scheme (`"tableau10"`), an array, or a `{category: color}` **object map** for exact per-category colors (no array ordering to keep in sync).
Presets: `light`, `dark`, `high-contrast`, `pastels`(-dark), `bi-tool`(-dark), `italian`(-dark), `tufte`(-dark), `journalist`(-dark), `playful`(-dark), `carbon`(-dark).
Serialization: `themeToCSS(theme, selector)`, `themeToTokens(theme)`, `resolveThemePreset(name)`.
**Semantic status roles** (every preset): `colors.success/danger/warning/error/info` + `secondary`/`surface`. Each emits as `--semiotic-{role}`. Use for status-driven charts: `<Waterfall positiveColor="var(--semiotic-success)" negativeColor="var(--semiotic-danger)" />`, `<Swimlane color="var(--semiotic-warning)" />`, status annotations.
**Scoped CSS cascade override** (per-subtree, no ThemeProvider needed): wrap a subtree in `<div style={{ "--semiotic-danger": "#4b0082" }}>` — canvas scene builders read CSS vars via `getComputedStyle` on the canvas DOM ancestor, so cascade rules apply even though rendering is canvas. CSS vars for single-role overrides; nested `ThemeProvider` for array/scale overrides (categorical palette, sequential/diverging scheme).
## AI Design Guidance
- **ISOTYPE/icon arrays**: Repeated pictograms, semantic icons, and glyph tokens should be legible in the final rendered layout. Treat 16px as the minimum intended rendered icon dimension for ISOTYPE/icon-array designs. This is design guidance, not a Semiotic-enforced minimum: low-level helpers may default smaller for dense/sparkline contexts, so set `tokenSize`, glyph `size`, SVG slots, or wrapping explicitly. If available width would shrink icons below that visual floor, wrap tokens into multiple rows/columns, reduce visible token count, change the unit value, or choose a non-icon encoding.
## AI Features
Surface APIs: `onObservation`/`useChartObserver`, `toConfig`/`fromConfig`/`toURL`/`fromURL`/`copyConfig`/`configToJSX`, `validateProps`, `diagnoseConfig` (includes `tokenEncoding` warnings when present), `suggestTokenEncoding`/`diagnoseTokenEncoding`, `auditAccessibility`/`accessibilityCaveats` + `describeChart` + `buildNavigationTree`/`AccessibleNavTree`/`useNavigationSync` + `buildReaderGrounding` (a11y audit + descriptions + structured navigation + bidirectional sync + agent-reader grounding — see Accessibility), `exportChart(div, { format })`, `npx semiotic-ai --doctor`/`--audit-a11y`.
### Conversational Interrogation (`semiotic/ai`)
Headless "chat with the chart" hook. Library ships no UI — BYO chat surface.
- **`useChartInterrogation({ data, onQuery, componentName?, props?, initialAnnotations? })`** → `{ ask(query), history, summary, annotations, loading, error, reset }`
- **`onQuery: (query, context) => Promise<{ answer, annotations? }>`** — call your LLM. `context`: `{ data, summary, componentName?, props? }`.
- **`summary`**: stat summary (`rowCount`, per-field `{min, max, mean, median}` for numerics, top-k for categoricals, ISO range for dates). Available before any `ask()`.
- **`annotations`**: merged `initialAnnotations` + latest AI response. Wire to chart's `annotations` prop.
- **`summarizeData(data, options?)`**: standalone for server prompting or batch.
- **MCP tool**: `interrogateChart(component, props, query)` returns same summary + AI-facing instructions.
### Chart Capability Layer (`semiotic/ai`)
Heuristic chart-suggestion engine — no LLM required. Charts ship capability descriptors next to TSX files; engine ranks against a profiled dataset by intent.
- **`profileData(data, { rawInput?, seriesField? })`** → `ChartDataProfile`: per-role candidate fields (x/y/series/category/size/time), distinct counts, monotonicity, structure detection.
- **`suggestCharts(data, { intent?, allow?, deny?, maxResults?, includeVariants?, minScore?, audience? })`** → ranked `Suggestion[]` with `{ component, family, importPath, variant?, score, intentScores, rubric, reasons, caveats, props }`. `props` is spreadable directly. **Receivability**: set `audience.receptionModality` (`visual` default | `screen-reader` | `sonified` | `agent`); a non-visual channel audits each candidate and down-ranks charts the audience can't receive there (8-slice pie for a screen reader), adding the audit's findings to `caveats[]` (familiarity and receivability are separate axes). `accessibilityCaveats(auditResult)` distils any audit into the same caveat strings.
- **`scoreChart(component, data, { intent?, variantKey? })`** → evaluate a specific chart for a dataset.
- **`useChartSuggestions(data, options)`** → memoized React hook returning `{ suggestions, profile }`.
- **`registerChartCapability(capability)`** / **`unregisterChartCapability(name)`** — runtime registration for custom charts.
- **Intent taxonomy** (13 built-in): `trend`, `compare-series`, `compare-categories`, `rank`, `part-to-whole`, `distribution`, `correlation`, `flow`, `hierarchy`, `geo`, `outlier-detection`, `composition-over-time`, `change-detection`. Extend via `registerIntent`.
- **Capability authoring**: `Foo.capability.ts` next to `Foo.tsx`, append to registry in `src/components/ai/chartCapabilities.ts`. Declares `family`, `rubric` (familiarity/accuracy/precision 1-5), `fits(profile)` gate, `intentScores`, optional `variants` with `intentDeltas`, `buildProps(profile, variant)`.
- **Variants** encode that settings change what a chart is good for (e.g. StackedAreaChart's `streamgraph` variant boosts trend, penalizes part-to-whole).
- **Interrogation tie-in**: pass `includeSuggestions: true` to `useChartInterrogation` and the ranked list lands in `context.suggestions` for the LLM.
- **MCP tool**: `suggestCharts(data, intent?)`.
### Conversation-arc telemetry (`semiotic/ai`)
Opt-in event store recording the AI session arc: `suggestion-shown → suggestion-chosen → audience-set → chart-rendered → chart-edited → chart-replaced → chart-exported | chart-abandoned`, plus `interrogation-asked`/`interrogation-answered`, reader-navigation events, and `annotation-status-changed`. Module-scoped, no provider needed. Default surface is no-op — call `enableConversationArc()` to start.
- **`enableConversationArc({ capacity?, sessionId? })`** / **`disableConversationArc()`**. Bounded ring buffer (default 1000 events).
- **`getConversationArcStore()`** → `{ enabled, sessionId, capacity, record, flush, getEvents, subscribe, clear, reset }`. `getEvents()` returns referentially stable snapshot.
- **`useConversationArc({ enableOnMount?, disableOnUnmount?, capacity?, sessionId? })`** → `{ history, summary, enabled, sessionId, record, clear }`. Uses `useSyncExternalStore`.
- **`summarizeArc(events)`** → pure reducer. Server/replay safe.
- **Persistence / replay**: `registerConversationArcSink(sink)` attaches an opt-in durable sink. Built-ins: `createLocalStorageConversationArcSink({ key?, storage?, maxEvents? })`, `createIndexedDBConversationArcSink({ dbName?, storeName?, indexedDB?, maxEvents? })`, and `createWebhookConversationArcSink({ url, method?, headers?, fetch?, mapEvent? })`. Sinks receive accepted events only; disabled telemetry is still zero-overhead. `loadConversationArc(events, { enabled?, capacity?, sessionId?, append? })` / `replayConversationArc(...)` hydrate the visible store snapshot without re-emitting events to listeners or sinks; default `enabled: false` makes replay safe for analytics.
- **`recordAudienceChange(audience, previous?, { arcId?, meta? }?)`** — sugar for `audience-set`. Call from audience-picker `onChange`.
- **Events**: `ConversationArcEvent` discriminated union. Each variant carries its own payload.
- **Auto-instrumented**: `useChartSuggestions` emits `suggestion-shown` (dedup by component-list + intent); `useChartInterrogation` emits `interrogation-asked` + `interrogation-answered` (with `latencyMs`); `AccessibleNavTree` emits the reception pair `nav-node-focused`/`nav-branch-expanded` on reader traversal (keyboard/click), correlated by its `chartId` prop. Zero-overhead when disabled.
### Annotation provenance + lifecycle (`semiotic/ai`, types re-exported from `semiotic`)
Two optional blocks attach to any annotation — existing arrays keep working unchanged.
- **`provenance`**: `{ author?, authorKind?, source?, basis?, confidence?, createdAt?, dataVersion?, stableId? }` (union of the shipped fields + IDID §8 `ChartAnnotationProvenance`). `authorKind` = actor (`"human"|"agent"|"watcher"|"system"|(string & {})`); `basis` = evidence type (`"human-note"|"statistical-test"|"rule"|"llm-inference"|"external-source"|"computed"|(string & {})`), distinct from the actor; `source` open union (`"user"|"ai"|"agent"|"import"|"computed"|"system"|(string & {})`); `dataVersion` = data snapshot the note was made against.
- **`lifecycle`**: `{ freshness?, status?, supersedes?, ttlHint?, anchor? }`. Two orthogonal axes — **temporal** `freshness` (`"fresh"|"aging"|"stale"|"expired"`, derived from `createdAt`+`ttlHint`) and **editorial** `status` (`"proposed"|"accepted"|"disputed"|"retracted"`); a note can be fresh-but-disputed. `supersedes` = `stableId` of the note this replaces. `anchor`: `"fixed"|"latest"|"sticky"|"semantic"`. `ttlHint`: ISO 8601 duration (`"P30D"`) or ms.
- **`withProvenance(annotation, { provenance?, lifecycle? })`** → pure, SSR-safe.
- **`Annotated<T>`** type: `T & { provenance?, lifecycle? }`.
- **`computeAnnotationFreshness(annotations, { now?, dataExtent?, thresholds? })`** → populates `lifecycle.freshness`. `now` defaults to `dataExtent` max, then `Date.now()`. Default thresholds 1×/1.5×/3× TTL.
- **`annotationFreshnessFor(annotation, nowMs, thresholds?)`** → classifies a single annotation. Explicit `lifecycle.freshness` wins.
- **`applyAnnotationLifecycle(annotations, { now?, dataExtent?, opacity?, strokeDasharray?, labelSuffix?, showExpiredAnnotations?, thresholds? })`** → freshness + default visuals (aging dims opacity 0.55, stale dims + dashes `"4 4"`, expired filtered; set `showExpiredAnnotations: true` to keep). Per-band overrides; pass `null` to disable a band default. Annotation-level `opacity`/`strokeDasharray` win.
- **`applyAnnotationStatus(annotations, { opacity?, strokeDasharray?, labelSuffix?, showRetractedAnnotations?, showSupersededAnnotations? })`** (M7) → editorial-status treatment, orthogonal to freshness: `disputed`→`(?)` + dim, `proposed`→provisional dim+dash, `retracted`→filtered (like expired), `accepted`→full. Opacity **multiplies** into existing, so it composes with `applyAnnotationLifecycle` (run freshness first). Also resolves `supersedes` — a note superseded by a present, non-retracted note is hidden. Use **`filterAnnotationsByStatus`** when a non-visual or custom surface needs the same visibility contract without styling. Emit transitions via **`recordAnnotationStatusChange(toStatus, { annotationId?, fromStatus?, chartId? })`** → conversation-arc `annotation-status-changed` event.
- **Semantic anchors**: `anchor: "semantic"` / `lifecycle.anchor: "semantic"` re-resolves through `provenance.stableId` after data refresh, using point scene nodes or matching data rows and falling back to the recorded coordinate when the target is gone.
### Temporal lifecycle (shared `semiotic/realtime` + `semiotic/ai`)
Three systems answer "how does this look as it ages?" on three different time axes — not interchangeable.
| Policy | Lives in | Time axis | Output | Scope |
|---|---|---|---|---|
| `DecayConfig` | `semiotic/realtime` | buffer position | continuous opacity ramp | per-datum |
| `StalenessConfig` | `semiotic/realtime` | wall-clock idle | binary live/stale (+ optional badge) | chart-wide |
| Annotation freshness | `semiotic/ai` | `createdAt` + `ttlHint` | 4 named bands (opacity + dashing + expired filter) | per-annotation |
Shared primitive: **`bandFromAge(ageMs, ttlMs, thresholds?)`** → `"fresh"|"aging"|"stale"|"expired"`. Exported from both. `DEFAULT_LIFECYCLE_THRESHOLDS = { fresh: 1.0, aging: 1.5, stale: 3.0 }`. Shared **anchor mode** (`AnnotationAnchor` from `semiotic/realtime`, re-exported from `semiotic/ai` as `lifecycle.anchor`).
**Streaming chart-time aging**: pass chart's `dataExtent` to `applyAnnotationLifecycle` — latest data point becomes "now". Pair with `withCurrentProvenance(annotation, { author?, source? })` / `currentTimestamp()` to auto-stamp `createdAt`. Full survey: `/intelligence/temporal-lifecycle`.
### Variant discovery (`semiotic/ai`)
Interface for proposing/scoring variants beyond `capability.variants`. The built-in proposer emits registered variants, conservative heuristic transforms, and same-intent cross-family alternatives; external recommenders can register model/agent proposers.
- **`VariantProposal`**: `{ id, baseComponent, label?, intentDeltas?, rubricDeltas?, buildProps?, rationale?, source: "manual"|"heuristic"|"model", variantKey?, tags? }`.
- **`VariantScore`**: `{ proposalId, fit (0–5), novelty (0–1), risk (0–1), reasons }`. Mixes with `suggestCharts` composite scores.
- **`proposeVariant(component, capability, context)`** → `VariantProposal[]`. Context accepts `{ profile, audience?, intent?, existingVariants? }`.
- **`evaluateVariantProposal(proposal, profile, audience?, { intent?, baselineComponent? }?)`** → `VariantScore`.
- **MCP tool**: `proposeChartVariants(component, props?, data?, intent?, audience?)` ranks proposals and returns ready-to-use props.
- **`registerVariantDiscovery(fn)`** → registers proposer. `proposeVariant` dispatches through all and dedupes by `proposal.id`. Returns unregister. Inspect: `getRegisteredVariantDiscovery()` / `clearVariantDiscovery()`.
## AI Behavior Contracts
<!-- semiotic-behavior-contracts:start -->
These rules are generated from `ai/behaviorContracts.cjs` and are consumed by `semiotic-ai --doctor`, MCP resources, and docs checks.
- **Accessible chart text uses direct chart props** (`accessibility.description-props`): High-level charts expose title for the visible name, description for a concise accessible description, summary for a screen-reader-only takeaway and interaction guidance, and accessibleTable for the data-table fallback.
Agent action: Put title, description, summary, and accessibleTable directly on the chart component when they appear in its schema. For generated L1–L3 description or a navigable chart tree, use ChartContainer with chartConfig plus describe and/or navigable; do not invent frameProps fields.
- **Data required by usage mode** (`props.data-required-by-usage-mode`): Static usage (`renderChart`, MCP previews, SSR snapshots, and copy/paste examples with immediate data) requires data in props. React push mode selects live ingestion by omitting data and mutating through a ref.
Agent action: Pass usageMode="push" to `semiotic-ai --doctor` when validating ref-based JSX with no data prop. Keep usageMode="static" or omit it for renderChart/MCP/static configs where data must be present.
- **Categorical color precedence** (`color.category-precedence`): When colorBy is set, CategoryColorProvider/LinkedCharts category maps win for mapped categories. Unmapped categories fall back to explicit colorScheme, then ThemeProvider colors.categorical, then the built-in categorical fallback.
Agent action: Use colorBy for categorical encodings. Use CategoryColorProvider or LinkedCharts for cross-chart consistency, colorScheme for per-chart fallback palettes, and avoid frameProps style functions unless intentionally bypassing HOC color resolution.
- **Required prop combinations** (`props.required-combinations`): Some chart families need semantic props beyond data. These combinations are enforced by validation/schema for static configs and remain required in push mode unless explicitly noted.
Agent action: Before returning code, check the selected component against the required combinations list. For push mode, omit data but keep semantic props such as areaBy, sizeBy, stackBy, and groupBy.
Required combinations: StackedAreaChart: static data + areaBy; push areaBy. Stacked areas need a flat data array plus areaBy to identify the stacked series. BubbleChart: static data + sizeBy; push sizeBy. Bubbles need sizeBy in addition to x/y accessors so radius encodes data rather than a constant point size. StackedBarChart: static data + stackBy; push stackBy. Stacked bars need stackBy to split each category into stack segments. GroupedBarChart: static data + groupBy; push groupBy. Grouped bars need groupBy to split each category into side-by-side bars. SwimlaneChart: static data + subcategoryAccessor; push subcategoryAccessor. Swimlanes need subcategoryAccessor; colorBy defaults to the same field when not provided. GaugeChart: static value; push not supported. GaugeChart is value-only. thresholds, min, max, sweep, and arcWidth are optional. ForceDirectedGraph: static nodes + edges; push nodes + edges. ForceDirectedGraph schema/rendering requires nodes and edges. If an agent infers nodes from edge endpoints, it must materialize a nodes array before returning code.
- **Push mode omits data** (`streaming.push-mode-data`): HOC push mode is selected by omitting the data prop entirely. Passing data={[]} is static empty data and can clear/reinitialize the frame on render.
Agent action: For live charts, create a ref, omit data, then call ref.current.push() or pushMany(). For static renderChart/MCP snapshots, provide data because renderChart cannot push later.
- **Ref mutations need stable IDs** (`streaming.ref-mutations-require-id-accessors`): push() and pushMany() can append without IDs, but remove(id) and update(id, updater) require a stable ID accessor: pointIdAccessor for XY/realtime charts, dataIdAccessor for ordinal charts, and nodeIDAccessor/edgeIdAccessor for network operations.
Agent action: When generating code that calls remove() or update(), include the matching ID accessor and make sure pushed rows carry that ID field.
- **renderChart uses static props only** (`rendering.renderchart-static-props`): MCP renderChart and semiotic/server renderChart render a single static SVG/PNG snapshot. Browser-only realtime components and future ref pushes are not renderable through that path.
Agent action: Use renderChart only with renderable HOC components and complete static data. For live behavior, return React code with a ref and do not promise MCP-rendered output.
<!-- semiotic-behavior-contracts:end -->
## Accessibility
`role="group"` (outer) + `role="img"` (inner canvas). Keyboard: arrows navigate points, Enter cycles neighbors, Home/End/PageUp/PageDown. Shape-adaptive focus ring (`--semiotic-focus`). `accessibleTable` (default true) for sr-only data summary (live aria-live region sits OUTSIDE `role="img"` so AT announces hovered/focused data). Auto-detects `prefers-reduced-motion`, `forced-colors`. Hooks: `useReducedMotion()`, `useHighContrast()`.
**Chartability audit**: `auditAccessibility(component, props, { inChartContainer?, describe?, navigable? })` (from `semiotic/ai` or `semiotic/utils`) grades a config against Chartability (POUR-CAF) — credits built-ins, flags author-actionable gaps, marks un-checkable items `manual` (not a false pass). `formatAccessibilityAudit(result)` renders the report. Surfaced as `npx semiotic-ai --audit-a11y` (non-zero exit on critical fail → CI gate) + the `auditAccessibility` MCP tool. Returns `{ ok, summary, findings[] }`; each finding has `{ id, principle, heuristic, critical, status: "pass"|"fail"|"warn"|"manual"|"not-applicable", message, fix }`. NOT a pass/fail cert — pair with manual NVDA/JAWS/VoiceOver testing.
**Chart descriptions**: `describeChart(component, props, { levels?, locale?, capability?, audience? })` (from `semiotic/ai` or `semiotic/utils`) generates a layered natural-language description (Lundgard L1 encoding / L2 statistics / L3 trend) → `{ text, levels: {l1?,l2?,l3?,l4?}, annotations? }`. Richest for XY/bar/part-to-whole/distribution; degrades to L1 for network/hierarchy/geo/value. **Annotations**: when `props.annotations` is present, the result carries an `annotations` sentence ("The author has marked 2 features…") and it *leads* `text` ahead of L1–L3 — an author-placed note is intent in its purest form. Provenance-aware: an `authorKind`/`source` of `agent`/`ai`/`watcher` qualifies it ("an AI-suggested callout"). Absent when no annotations, so un-annotated callers are unchanged. **L4 (intent)**: pass a `capability` (a chart's descriptor or a resolved `{ family, intentScores }`) and `describeChart` emits the illocutionary *communicative-act* sentence ("This is an alerting chart; the peak at March is the point to investigate") — opt-in, default output stays L1–L3. Helpers: `resolveCommunicativeAct(component, capability)`, `communicativeActForIntent(intent)`, type `CommunicativeAct`. **Full-accessibility chrome (title, caption, description, navigation, data download) is the opt-in `ChartContainer` layer — not baked into the bare chart.** `<ChartContainer describe chartConfig={{component, props}}>` renders an sr-only L1–L3 description (`describe={{ visible:true }}` to show it, `{ levels }` for verbosity). The audit's `assistive.features-described` passes when `describe` is on.
**Agent-reader grounding**: `buildReaderGrounding(component, props, { capability?, audience?, includeStructure? })` (from `semiotic/ai` or `semiotic/utils`) → `{ description (L1–L3), intent (act + L4 sentence), structure (nav tree), text }` — the single payload an LLM reads to interpret a chart faithfully (the reader-side complement to a capability descriptor). MCP: `groundChart` tool.
**Structured navigation** (Olli/Data-Navigator model): `buildNavigationTree(component, props, { maxLeaves?, locale? })` (from `semiotic/ai` or `semiotic/utils`) → a `NavTreeNode` tree (chart → axis/series → datum), labels composed via describeChart. When `props.annotations` is present it appends an **Annotations** branch (`role: "annotation"`) so a reader encounters author notes during traversal — provenance + M7 editorial `status` surfaced inline, retracted and superseded notes skipped, added on every family (incl. root-only network/geo/hierarchy). `describeChart` also leads its text with an annotation sentence when annotations are present. `AccessibleNavTree` (from `semiotic` or `semiotic/ai`) renders it as a WAI-ARIA `tree` widget (arrows/Enter/Home/End, roving tabindex, `onActiveChange`, controlled `activeId` auto-expands to the node, `chartId` for telemetry), sr-only by default. With the conversation-arc store enabled it emits `nav-node-focused`/`nav-branch-expanded` reception events on genuine traversal (not on canvas-driven `activeId` changes). `<ChartContainer navigable chartConfig={...}>` mounts it (`navigable={{ visible?, maxLeaves? }}`). Audit's `compromising.navigable-structure` passes when `navigable` is on (incl. hierarchy charts). Audit options: `auditAccessibility(component, props, { inChartContainer?, describe?, navigable? })`.
**Bidirectional sync**: `useNavigationSync({ tree, chartId?, matchFields?, selectionName?, annotations? })` (from `semiotic` or `semiotic/ai`) → `{ activeId, onActiveChange, selection, annotatedIds, focusAnnotation }`. Tree→canvas highlights the matching mark (field-value selection); canvas→tree maps the hovered/clicked datum back to its leaf. Rides the module-global selection + observation stores — **no provider needed**: give the chart `chartId` + `selection={sync.selection}`, give `AccessibleNavTree` `activeId`/`onActiveChange`. **Annotation anchors**: pass the chart's `annotations` and an anchored annotation (carrying the datum's `matchFields`) resolves to a nav leaf — `annotatedIds` are the leaf ids with a note; `focusAnnotation(annotation | index)` jumps the tree + canvas to the anchored point so a non-visual reader can reach an AI's anchored note.
## Usage Notes
- **Push API**: Omit `data`. `data={[]}` clears on every render.
- **Tooltip datum shape**: HOC tooltips get raw data. Frame `tooltipContent` gets wrapped — use `d.data`.
- **Tooltip format cascade**: `valueFormat`/`xFormat`/`yFormat` flow to default tooltip (axis + tooltip read identically). Custom `tooltip` fully overrides — re-pass via `Tooltip({format})` / `MultiLineTooltip({fields:[{format}]})`. Bespoke-tooltip charts (Histogram, FunnelChart, LikertChart, GaugeChart) don't participate; customize via `tooltip`.
- **`tooltip="multi"`**: shows all series at hovered X for LineChart, AreaChart, StackedAreaChart. Custom fn receives `datum.allSeries`.
- **Legend**: "bottom" expands margin ~80px. MultiAxisLineChart: use `legendPosition="bottom"`.
- **Horizontal bars**: need wider left margin (`margin={{ left: 120 }}`).
- **Log scale**: domain min clamped to 1e-6. **`xScaleType: "time"`**: creates `scaleTime`; required for landmark ticks with timestamps.
- **`barPadding`**: pixel value (40/60 default). Reduce for small charts.
- **`sort`** (BarChart/StackedBarChart/GroupedBarChart/DotPlot): `false` preserves insertion order; `"auto"` = insertion while streaming, value-desc on static (DotPlot default). StackedBar/GroupedBar default to `false`; the underlying frame value-sorts when `oSort` is undefined, so always pass `sort` explicitly if order matters.
- **`fillArea`**: `fillArea={["seriesA"]}` fills named series only — names must match `lineBy`/`colorBy` keys.
- **`hoverHighlight`**: requires `colorBy` as a string field.
- **`frameProps` style functions**: bypass HOC color resolution — use `colorBy` instead.
- **Geo imports**: always `semiotic/geo`, never `semiotic`.
- **Axis config**: `frameProps.axes: [{ orient, includeMax, autoRotate, gridStyle, landmarkTicks, tickAnchor }]`. `tickAnchor: "edges"` flips first tick's `text-anchor` to `start`, last to `end` (and `dominant-baseline` on vertical axes) so edge labels don't overflow. Pairs with `axisExtent: "exact"`.
- **Per-axis CSS**: every axis renders as `<g class="semiotic-axis semiotic-axis-{bottom|left|right|top}" data-orient="…">`. Style via `[data-orient="left"] text { font-size: 14px }` — CSS-var defaults are set inline via `var(--semiotic-tick-font-size, …)`, so cascade overrides win cleanly. Tick text: `class="semiotic-axis-tick"`; labels: `class="semiotic-axis-label"`; titles: `class="semiotic-chart-title"`.
- **`scalePadding`**: pixel inset on scale ranges (via `frameProps={{ scalePadding: 12 }}`).
- **`categoryFormat`/`xFormat`/`yFormat`**: can return ReactNode (renders in `<foreignObject>`). Tick deduplication: adjacent identical labels auto-removed.
- **Composing overlays**: XY/Ordinal paint `--semiotic-bg` across the canvas; stack with `frameProps={{ background: "transparent" }}` on the overlay. Network/Geo don't paint bg by default.
- **`foregroundGraphics`/`backgroundGraphics` resolved scales**: the function form receives `{ size, margin, scales }` — `scales` is the frame's **resolved** scales (`{x, y}` for XY, `{o, r, projection}` for ordinal; `null` before first layout). Anchor a bespoke SVG overlay to `scales.x(...)`/`scales.y(...)` so it can't drift from the axes the chart drew (the HOC analogue of a custom layout's `ctx.scales`). Fall back to your own mapping while `scales` is null.
- **Theming a custom chart**: prefer `--semiotic-*` tokens for chrome (`stroke="var(--semiotic-text-secondary)"`) and `ctx.resolveColor(key)` / semantic role vars (`var(--semiotic-danger)`) for data, so the chart tracks `ThemeProvider`/dark mode — *or* deliberately paint fixed editorial colors on a `background: "transparent"` frame for art direction. Both are first-class; the kit's chrome helpers default to `--semiotic-*` tokens either way.
## Performance
Prefer string accessors (`xAccessor="value"`) — always referentially stable. Memoize function accessors with `useCallback`.