# Semiotic — Complete AI Reference This is an on-demand encyclopedia, not a system prompt. Start with `ai/system-prompt.md` and the exact component schema, search this file for the relevant chart or behavior, and load only that section. Validate generated configurations with `prepareChart`, `npx semiotic-ai --doctor`, or the MCP diagnostic tools; when possible, confirm a non-empty render with evidence. ## Quick Start - Install: `npm install semiotic` - **Use sub-path imports** — `semiotic/access` (36KB gz), `semiotic/evidence` (50KB gz), `semiotic/artifact` (121KB gz), `semiotic/artifact/react` (4KB gz), `semiotic/line` (132KB gz), `semiotic/xy` (163KB gz), `semiotic/ordinal` (131KB gz), `semiotic/network` (151KB gz), `semiotic/geo` (108KB gz), `semiotic/realtime` (160KB gz), `semiotic/realtime/core` (159KB gz), `semiotic/realtime/react` (1KB gz), `semiotic/server` (230KB gz), `semiotic/server/node` (230KB gz), `semiotic/server/edge` (232KB gz), `semiotic/utils` (98KB gz), `semiotic/utils/core` (91KB gz), `semiotic/utils/react` (7KB gz), `semiotic/recipes` (108KB gz), `semiotic/recipes/core` (101KB gz), `semiotic/recipes/react` (8KB gz), `semiotic/themes` (12KB gz), `semiotic/themes/core` (12KB gz), `semiotic/themes/react` (7KB gz), `semiotic/data` (3KB gz), `semiotic/value` (6KB gz), `semiotic/physics` (166KB gz), `semiotic/physics/matter` (1KB gz), `semiotic/physics/rapier` (1KB gz), `semiotic/ai` (594KB gz), `semiotic/ai/core` (138KB gz), `semiotic/controls` (10KB gz), `semiotic/rough` (3KB gz), `semiotic/text` (1KB gz). Full `semiotic` is 374KB gz. - CLI: `npx semiotic-ai [--schema|--compact|--examples|--doctor|--audit-a11y|--evaluate]` · 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 (chart HOCs only) `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; use `{ portalTarget: "element-id" }` to portal its interactive UI outside a consumer-owned `role="img"`), `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; override per XY axis with `frameProps.axes[i].extent`), `maxDevicePixelRatio` (canvas backing-store cap; defaults 3 desktop / 2 coarse-pointer or narrow viewports; very large canvases are further capped to an 8-megapixel, 16K-side backing-store budget; canvases repaint on browser zoom / display-density changes). This list is not global. Plain value components do not inherit chart-HOC props and must be validated against their own schema. In particular, `BigNumber` uses `label`, `description`, and `summary`; it does not accept `title` or `accessibleTable`. **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`/`nodeStyle`/`edgeStyle` fn form. These retained mark styles also accept `cursor` (for example `nodeStyle={() => ({ cursor: "pointer" })}`), with canvas/SVG/static parity. Cursor is presentation only; pair it with the chart/frame click or observation API so pointer and keyboard behavior remain explicit. `onClick` receives `(datum, { x, y })`. `onObservation` receives `{ type, datum?, x?, y?, timestamp, chartType, chartId }`. `ObservationReadout` renders an inline live-region description from those events. Pass `chartId` inside `LinkedCharts` to subscribe to the observation store, or pass the latest event through `observation`; its render function receives the unwrapped user datum. `observedDatum(event)` exposes the same normalization without rendering UI. ## 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` ("linear"|"log"|"time"), `yScaleType` ("linear"|"log"|"symlog"), `styleRules` (per-series), `brush` (x-axis overlay; also enabled by `linkedBrush`), `tooltip="multi"` for hover-anywhere **AreaChart** — LineChart props + `areaBy`, `y0Accessor`, `gradientFill`, `semanticGradient` (`{stops: [{offset: 0–1, color?, opacity?}]}`; offset follows the resolved y-domain), `semanticLine` (true with semanticGradient; false keeps the normal stroke/lineGradient), `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`. **BumpChart** — Ranking-over-x. Each x-column ranks series by `yAccessor`; rank is vertical position. `data`, `xAccessor` ("x"), `yAccessor` ("y"), `lineBy` ("series"), `rankDirection` ("descending"|"ascending"), `ribbon` (false — encode magnitude as true perpendicular-offset ribbon width instead of fixed-width lines; line↔ribbon share centerline geometry so `animate` tweens width only), `curve` ("smooth"|"linear"), `ribbonSizeRange` ([4,28]), `samplesPerSegment` (12), `lineWidth` (3), `highlightTop` (color only the N best series by mean rank; the rest share `neutralColor`), `neutralColor`, `styleRules` (per-series; resolve against each series' first observation), `showPoints`, `showLabels` (true|"start"|"end"|"both"|"auto"; endpoint collisions are shed deterministically), `labelPriorityAccessor` / `maxLabels` (prioritize and cap labels under `showLabels="auto"`), `hoverHighlight`, `annotations` (x may use original x values incl. `Date`). Wraps XYCustomChart. **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), `brush` (xy overlay; also enabled by `linkedBrush`) **BubbleChart** — Scatterplot + `sizeBy` (required), `sizeRange` ([5,40]), `regression`, `brush` (xy overlay; also enabled by `linkedBrush`) **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`, `customColorScale`, `showValues`, `cellBorderColor`, `styleRules` (displayed cells; aggregate cells expose `count`, `sum`, `agg`, bin centers, and the displayed aggregate as `ctx.value`) **ScatterplotMatrix** — `fields` (numeric field names). `renderChart()` emits one standalone composite SVG containing every off-diagonal scatter scene, diagonal distribution, field label, and categorical legend; interactive brushing/hover remains a browser affordance. **MinimapChart** — Overview + detail with linked zoom. Wraps an XY chart. `renderChart()` preserves both scenes and paints a supplied `brushExtent` into the overview, so the standalone snapshot retains the selected-detail/context relationship. **CandlestickChart** — `xAccessor`, `highAccessor` (req), `lowAccessor` (req), `openAccessor`+`closeAccessor` (optional → OHLC; high/low only → range). `candlestickStyle` ({upColor, downColor, wickColor, rangeColor, bodyWidth, wickWidth}). Honors `mode`. **WaterfallChart** — cumulative signed steps as floating bars. `xAccessor` ("x"), `yAccessor` ("y" — each row is a **delta**, not a running total), `positiveColor`, `negativeColor`, `connectorStroke`, `connectorWidth`, `gap`. String/categorical x values are plotted by index. Live window: `RealtimeWaterfallChart`. ## Ordinal Charts (`semiotic/ordinal`) **BarChart** — `categoryAccessor`, `valueAccessor`, `orientation`, `colorBy`, `sort`, `barPadding` (40), `roundedTop`, `gradientFill` (`{stops: [{offset: 0–1, color?, opacity?}]}`; tip→base), `styleRules`, `regression`, `brush` (value-axis overlay; `onBrush` / `linkedBrush`) **StackedBarChart** — + `stackBy` (required), `normalize`, `sort` (false default — insertion order), `styleRules`, `brush` (value-axis overlay; `onBrush` / `linkedBrush`) **GroupedBarChart** — + `groupBy` (required), `barPadding` (60), `sort` (false default), `styleRules`, `brush` (value-axis overlay; `onBrush` / `linkedBrush`) **SwarmPlot** — `colorBy`, `sizeBy`, `symbolBy` (categorical field → glyph shape, like Scatterplot), `symbolMap`, `pointRadius`, `pointOpacity` **BoxPlot** — + `showOutliers`, `outlierRadius` **Histogram** — + `bins` (25), `relative`, `brush` (value-axis overlay; `onBrush` / `linkedBrush`). Always horizontal. **ViolinPlot** — + `bins`, `curve`, `showIQR` **RidgelinePlot** — + `bins`, `amplitude` (1.5) **DotPlot** — + `sort` ("auto"), `dotRadius`, `showGrid` (true default), `regression` **PieChart** — `categoryAccessor`, `valueAccessor`, `colorBy`, `startAngle`, `valueFormat` (default tooltip) **DonutChart** — PieChart + `innerRadius` (60), `centerContent` **FunnelChart** — `stepAccessor`, `valueAccessor`, `categoryAccessor?`, `connectorOpacity`, `orientation`, `valueFormat` (tooltip + vertical value-axis ticks) **RadarChart** — multivariate comparison on a shared radial axis. Long-form rows: one observation per series × attribute. `categoryAccessor` ("attribute"), `valueAccessor` ("value"), `seriesAccessor` (defaults to `colorBy`), `pointRadius` (4), `valueExtent` (`[0, data-max]` default), `valueFormat` (radial ticks + default tooltip). Axes must be comparable magnitudes. **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` To compare changing data on a consistent scale, set `valueExtent: [min, max]` on BarChart, StackedBarChart, GroupedBarChart, DotPlot, SwarmPlot, BoxPlot, ViolinPlot, Histogram, RidgelinePlot, RadarChart, SwimlaneChart or LikertChart. These bounds work in React and static `renderChart`/MCP configurations. Numeric `[min]` leaves the upper bound data-derived; React also accepts `undefined` for an automatic bound. For example, `valueExtent: [0, 40]` keeps a value of 12 the same size when another value changes from 18 to 36. A fully specified extent may exclude values outside its bounds, so choose a domain that covers the comparison. All ordinal: `colorBy`, `colorScheme`, `categoryFormat` (string|ReactNode), `showCategoryTicks` (true), `styleRules`. Raw-mark charts use `valueAccessor`; box/violin/ridgeline use the rendered category median and expose `n`/`min`/`q1`/`median`/`q3`/`max`/`mean`; Histogram uses displayed bin count and exposes `bin`/`count`/`range`/`category`; Pie/Donut use absolute wedge magnitude; Likert uses displayed signed percentage; Gauge rules see synthetic zone segments (`category`, proportional `value`, `_zone`, `_isFill`). **`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: **all 16 ordinal HOCs**; **XY** LineChart/AreaChart/StackedAreaChart/BumpChart/MultiAxisLineChart/MinimapChart (per-series), Scatterplot/BubbleChart/QuadrantChart/ConnectedScatterplot/ScatterplotMatrix (per-point), Heatmap (displayed cells), WaterfallChart (signed bars), CandlestickChart (bodies; y=close or high), and DifferenceChart (fills/lines); **network** ForceDirectedGraph/SankeyDiagram/ProcessSankey/ChordDiagram plus Tree/Treemap/CirclePack/Orbit (authored nodes); **realtime** Line/Histogram/TemporalHistogram/Swarm/Waterfall/Heatmap; **geo** ChoroplethMap (features), ProportionalSymbolMap/DistanceCartogram (symbols), and FlowMap (edges); **physics** GaltonBoard/UnitPile/CollisionSwarm/EventDrop/PacketFlow/ProcessFlow (particles or packets). `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 are host-resolved: ordinal `{ value, category }`; XY `{ value(=y), x, y }`; node/feature/particle families `{ value, category }`; aggregate Heatmap/Histogram forms use the displayed aggregate and retain fields such as `count`, `sum`, `agg`, and `range`. A rule's style may include `cursor` plus normal fill/stroke/opacity fields; `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 `` in SSR. Precedence: top-level primitives > per-mark style fn (`pieceStyle`/`pointStyle`/`nodeStyle`) > `styleRules` > base `colorBy`/`color`/theme. Works through `renderChart`/MCP for every SSR-capable wired form (use declarative thresholds, not predicates, across JSON boundaries). Caveats: line/area rules resolve per-series, not per-vertex; MultiAxisLineChart thresholds read the original series y, not the unitized display scale; ScatterplotMatrix rules apply to scatter cells, not diagonal histograms. 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`, `nodeStroke`/`nodeStrokeWidth` (node-only outline), `edgeWidth`, `edgeColor`/`edgeOpacity` (edge-only stroke), `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). **Node vs edge stroking**: the generic `stroke`/`strokeWidth`/`opacity` style *all* marks uniformly; to stroke nodes and edges separately use node-only `nodeStroke`/`nodeStrokeWidth` (e.g. `nodeStroke="none"` drops the node ring) and edge-only `edgeColor`/`edgeWidth`/`edgeOpacity`. Precedence per property: specific > generic > built-in default. **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`; zero-duration OK), `domain` (req `[t0, t1]`), `axisTicks?`, `xExtentAccessor` (optional `[start, end]` lifetime per node), `nodeLabel` (visible lane-label accessor), `colorBy`/`colorScheme`/`showLegend`/`legendPosition`, `styleRules` (band fills; `fill` may be a color or **HatchFill** — canvas + SSR), `selection`/`linkedHover`, `selectionDatum` ("raw" default | "scene" — what selection predicates see), `pairing` ("value"|"temporal", default temporal), `packing` ("off"|"reuse"), `laneOrder` ("crossing-min"|"inside-out"|"crossing-min+inside-out"|"insertion"), `maxValueScale` (pixels-per-unit cap), `lanePlacement` ("stack"|"hug" — hug/binding maxValueScale also post-scale geometry-refines order), `lifetimeMode` ("full"|"half", default half), `ribbonLane` ("source"|"target"|"both"), `ribbonMinRun` (0 exact | pixel minimum | "auto" — **source-only feeder** runway + source-band handoff; not a general min-edge length), `showLaneRails`, `showLabels` (true|false|"auto" density budget; auto sheds keep deferred text for selection reveal), `labelPriorityAccessor` / `maxLabels` (auto priority + hard cap), `showQualityReadout` (crossings/pixel + non-fatal validation warnings), `layoutExecution` ("auto"|"worker"|"sync" — auto offloads packing/ordering to a module worker by cost; SSR always sync), `layoutWorkerThreshold`, `layoutLoadingContent`, `onLayoutStateChange` (`"pending"|"ready"|"error"`), `showParticles` + `particleStyle`, `timeFormat`/`valueFormat`, push API via ref (`getScales` → time + centerlines; `getCustomLayout` → `{ layout, bands, ribbons, warnings, … }` with `layout.layoutQuality`). Pure quality helpers from `semiotic/network`: `diagnoseProcessSankeyLayout` / `diagnoseProcessSankeyProps` / `explainProcessSankeyLayout` (also fed into `diagnoseConfig("ProcessSankey", props)`). **Validation policy**: static/MCP hard-fail duplicate ids; React push warns on duplicates and strips non-finite systemIn/Out times. 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), `simulationMode` ("sample"|"mechanical"; legacy `mode` aliases accepted), `pegRows`, `mechanicalCount`, `branchProbability`, `ballRadius`, `colorBy`, `styleRules` (per-particle; `ctx.category` = colorBy group), `seed`, `size`/`width`/`height`, `paused`, `frameProps`. Values enter assigned bins before physical replay. Mechanical mode generates seeded Bernoulli samples; the apparatus does not simulate peg collisions. **EventDropChart** — `data`, `timeAccessor` ("time"), `arrivalAccessor` ("arrivalTime"), `windows` ({size}), `watermark` ({delay}|{value}|fn), `watermarkAtArrivalAccessor`, `timeScale`, `ballRadius`, `colorBy`, `seed`, `size`, `paused`, `frameProps`. Delay/function policies classify events in arrival order against a monotonic watermark; ties retain source order. A window is closed when its end is at or below the watermark. Accepted historical events stay accepted as later events advance it. An explicit `watermark.value` tests one fixed policy; supply `watermarkAtArrivalAccessor` to preserve recorded admission thresholds independently of current closure. `timeScale` changes pacing, not decisions. Push/update/remove and replacement data rebuild the source projection and scene together; `getData()` returns source records. Current lids are solid for every body: accepted history starts beneath them, while late arrivals roll into the far-left bin. For admission → closure → late-arrival teaching, supply successive data/watermark states and pause or coordinate source time while bodies travel; changing `timeScale` alone does not animate historical lid changes. Projection labels are source snapshot totals (accepted per window, late in the far-left bin). `readEventDropOccupancy(metadata, bodies)` reads body centers from the matching builder geometry and returns `{accepted: number[], late, inFlight, total}`; these sum to materialized body count, excluding queued arrivals. Metadata also exposes `windowWalls` rectangles derived from collision geometry for custom drawing. **UnitPileChart** (was `PhysicsPileChart`; alias kept) — `data`, `categoryAccessor` ("category"), `valueAccessor` (omit to count rows), `simulationMode` ("sample"|"mechanical"; legacy `mode` aliases accepted), `mechanicalCount`, `mechanicalCategories`, `unitValue` (1), `ballRadius`, `colorBy`, `seed`, `showProjection`, `size`, `paused`, `frameProps`. A full circle represents `unitValue`; each source record's remainder uses proportional circle area. Labels retain exact source totals; pile height is approximate. Mechanical mode generates a seeded unit pile and defaults `valueAccessor` to "value". **CollisionSwarmChart** — `data`, `xAccessor` ("x"), optional `groupAccessor`, `radiusAccessor`, `pointRadius`, `xExtent`, `collisionIterations`, `settle`, `showProjection`, `colorBy`, `seed`, `size`, `paused`, `frameProps`. X stays exact throughout motion; springs and collisions arrange vertical spacing within group lanes. If packing cannot fit at the chosen radius and height, points retain their values and radii and the projection discloses overlap. Reduce radius or increase height to make room. `settle` starts bodies at their packed targets; it does not run the settle loop. **PacketFlowChart** (was `PhysicalFlowChart`; alias kept) — `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. **Stage geography kit** (`semiotic/physics` + `semiotic/recipes`) — `physicsStageGeography({size, flow ("down"|"right"), destinations (count | [{id,label}]), padding, chargeExtent, destinationExtent, projectionExtent, channelRatio})` → `{charge, apparatus, destinations: [{id,label,order,centerX,centerY,…}], projection}`. The shared **charge → apparatus → destinations** vocabulary every physics chart re-invented locally (Gauntlet `startX`/`socketX`/`graveyardX`, Crucible `chamber`/`mouth`/`outlets`, Galton bins and Pile tubes writing the *same* lane formula). Companions: `physicsStageColliders` (floor + one divider per interior boundary, so bodies land in the bin the data says), `physicsChargePoint` (spread a burst across the entry zone instead of co-locating it), `physicsDestination(geo, id)`, `describePhysicsStageGeography` (one-sentence reading protocol for a11y/agent grounding). Authoring vocabulary for a **new** chart or `PhysicsCustomChart` layout — shipped charts keep their own layouts; a test pins the builder to their lane math. **Terminal state (event-tape charts)** — a tape-driven physics chart's end state must be computable from authored inputs with no simulation, because that pure result is what reduced motion, SSR, snapshot export, and `describeChart` receive. `CrucibleChart`: `compileCruciblePlan(...)` → `plan.initialState`/`plan.terminalState`/`plan.terminalSpawns`. `ChainReactionChart`: `initialRuntime(machine, mode, currentTime, true)`. `GauntletChart`: **`resolveGauntletTerminalStates({projects, events, layout, positiveProperties, negativeProperties, viability?, outcome?})`** (also `resolveGauntletTerminalState` for one project) — folds the authored tape, mirroring the live tick's effect order. Caveat: with Gauntlet's `crashDetection` armed (default), physics can still override the outcome to `bad_design_crash`, which no pure fold can predict; the pure result is "what the plan earns on paper". Pass `crashDetection={false}` for a fully authored reading. **Design test for a new physics metaphor:** can you state its ledger in one sentence (else it's an animation), does its terminal state exist without simulating (else it's a movie), would a naive reader guess the reading protocol from the apparatus (else the name is wrong)? **Physics family contracts** (every physics HOC) — `seed` (deterministic; an explicit `frameProps.config.kernel.seed` still wins), `rerunMS` (settle → delay → deterministic replay; `null`/omitted = single run), `onSimulationStateChange(state, previousState)` (public running/settled readiness without reaching into `frameProps.config`), `showProjection`, `paused`, and `selection` + `linkedHover`. `selection` accepts **either** a `{ name }` `SelectionConfig` — joining the shared selection store so physics bodies cross-highlight inside `LinkedCharts` like every other family — **or** a resolved `{ isActive, predicate }` `PhysicsBodySelection` body predicate as the escape hatch. The store's datum predicate is lifted over `body.datum`, so chrome bodies (walls, pegs, tubes) never match a data selection. Under `prefers-reduced-motion`, the frame runs a bounded pass using `config.settleStepLimit`. Paced arrivals retain their times; arrivals beyond that budget can remain queued. Forces, controllers, and `onTick` execute at each fixed step (with `steps=0` at admission, `steps=1` thereafter), including imperative frame settles. Callback-driven execution uses sync mode. Budget exhaustion and sleeping bodies do not prove semantic completion: inspect the queue and process ledger, or declare an explicit horizon. Plain `PhysicsPipelineStore.settle()` and raw static rendering run the kernel; `tick`/`settleWithObservations` accept an optional `{onStep, continueWhile}` execution hook for headless authored behavior. **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). **CrucibleChart** — bounded peer components undergo authored `phases` and `events`, form declared `products`, and settle into reason-labelled `outlets` with source lineage. The ledger/projection is authoritative; motion never infers classification, timing, membership, loss, or routing. `buildCrucibleProductEvents({productId, form, contributions?, complete})` is pure authoring sugar for `combine → contribute* → complete-product`; every source/relation id, event position, reason, and outlet remains caller-supplied. The ref handle's `replay()` atomically restarts the deterministic tape even mid-run (`reset()` restores-and-pauses; `rerunMS` repeats after settlement). `playbackRate` changes presentation only. No push/live-event API. **ChainReactionChart** — dependency chain reaction. `data`, `taskIDAccessor`, `labelAccessor`, `laneAccessor`, `dependencyAccessor` (all required — edges are never inferred), plus optional `startAccessor`/`endAccessor`/`progressAccessor`/`statusAccessor`/`completionTimeAccessor`/`blockerAccessor`/`milestoneAccessor`. Tasks place by workstream lane × dependency depth; a completed task releases one delivery ball per outgoing edge and a downstream task arms only once every prerequisite ball arrives, so a blocked task's *reach* is the reading. `mode` ("snapshot" derives the settled state at `currentTime` with no simulation | "replay" animates deliveries over that same derived state | "mechanical"), `insight` ("blocker-amplification" reports unfinished downstream tasks + affected lanes), `controls` (play/pause/step/reset/settle), `selectedTaskIDs`/`onSelectionChange`, `reducedMotion: "settle"`, `seed`. Ref handle: `play`/`pause`/`step`/`reset`/`settle`, `previewResolve(taskID)`/`clearPreview` (ask "what would resolving this blocker unlock?" without editing data), `completeTask`/`blockTask`/`unblockTask`, `getAmplification`, `getMachineState`. Task completion is always an explicit data event — the simulation delivers prerequisites, it never decides done. `renderChart()` emits the authored task/dependency projection at `currentTime`, including blocker reach and selection; it deliberately omits arbitrary in-flight delivery-ball positions because those are replay state, not the chart's ledger. **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** (+ sequential `colorScheme`; use `colorScheme="custom"` with `customColorScale(value)` for a custom ramp), **Streaming Sankey** (StreamNetworkFrame + `showParticles`). All six named realtime chart forms expose `styleRules`; aggregate histogram/heatmap rules resolve against displayed bins/cells rather than arbitrary source rows. Temporal histograms support native `responsiveWidth` / `responsiveHeight` (definite parent height required), `showTimeAxis` / `showValueAxis`, and shared XY `axes` configs with `visible: false`. Hidden axes reserve no default margin; explicit margins win. Other XY HOCs use `frameProps.axes`. For mirrored histograms, use shared ascending time/value extents and side margins, hide the upper time axis, set the lower top margin to zero, and use `direction="down"` below. Match plot heights, allowing extra outer height for the lower time axis. Realtime downward orientation also follows pushed data. Histogram `linkedHover={{ name: "detail", mode: "field", fields: ["time", "category"] }}` emits bin fields or source-row values for absent bin fields. A LineChart with `selection={{ name: "detail" }}` dims nonmatching series, and `showPoints` dims nonmatching observations. Line field hover publishes the hovered row's fields; x-position crosshairs require explicit `mode: "x-position"`. Bin `onHover` receives `{ data: { binStart, binEnd, total, category? }, ... }` or null; `onObservation` emits standard hover/hover-end events. See the realtime histogram docs' linked/mirrored example. All five realtime wrappers accept a top-level `cursor` as a presentation-only default for retained marks; `RealtimeSwarmChart.pointStyle` can override it per datum. Pair actionable cursors with explicit click or observation behavior. `RealtimeLineChart` supports `eventTime={{ lateness, latePolicy? }}` for bounded out-of-order input. Type its ref as `RealtimeLineChartHandle` and call `ref.current.flush()` when the source ends so events still inside the grace window are released in event-time order. A flush commits an ordering boundary: later newer events buffer normally, while events older than the flushed frontier follow `latePolicy`. Changing the event-time config or `timeAccessor` live drains the old tail before the new interpretation begins. With `aggregate`, changing only `stat`/`band`/`sigma` reuses accumulated windows. Changing `window`/`size`/`hop`/`gap`/`retain`, `timeAccessor`, or `valueAccessor` starts a structurally new accumulator: controlled `data` is replayed under the new definition, while push-only history resets because aggregation intentionally retains window statistics rather than every raw event. Encoding: `decay`, `pulse`, `transition`, `staleness` — compose freely. ### Push API on HOC charts Most HOCs support push via `forwardRef`. **Omit** `data` — do NOT pass `data={[]}`. TypeScript: `RealtimeFrameHandle` preserves the authored row through `push`, `pushMany`, `remove`, `update`, and `getData`; omitting parameters retains the loose 3.x `Datum` contract. RealtimeLineChart aggregate mode accepts source `TDatum` but materializes `AggregatedRealtimeDatum`, exposed through `RealtimeLineChartHandle`. ```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 ``` 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 ### Built-in portable recipes `ParallelCoordinatesRecipe` and `CalendarHeatmapRecipe` are the first custom forms promoted into the serialized catalog. They are discoverable through `suggestCharts`, `getSchema`, the CLI/MCP schema index, and the server renderer. Their component names are serialization and rendering identifiers; React uses the generic host rather than one-off HOCs: ```jsx import { ChartRecipe } from "semiotic/ai" ``` The serialized equivalent is `{ component: "ParallelCoordinatesRecipe", props: { data, layoutConfig, title, description, summary, accessibleTable: true } }`; substitute `CalendarHeatmapRecipe` with `layoutConfig: { dateAccessor, valueAccessor, year? }` for the calendar form. Both names work with `renderChart` and `renderChartWithEvidence`. Import the raw `parallelCoordinatesLayout` or `calendarLayout` from `semiotic/recipes` only for React-only callbacks or bespoke frame control. `semiotic/ai/core` exposes their manifests and discovery metadata without importing the chart renderer or layouts. 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) => { backgrounds?, 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). `backgrounds` is plot-space SVG painted below the canvas/static scene (for fitted regions and enclosures); `overlays` paints above it. Both are included in SSR/static SVG and do not add hit targets. 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. `` (from `semiotic/recipes`) renders the same definition as React SVG for overlays/legends/decoration; `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: `backgrounds` → canvas → `overlays` → `htmlMarks`). Reach for it over an SVG `` when a mark is **text-heavy/rich and dims or animates on hover** — a real `
` 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, cursor?}`. The optional CSS `cursor` reaches the retained canvas and SVG/static output; it is presentation only, so pair `cursor:"pointer"` with the frame's click/observation API when the mark activates something. 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 decoration 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`. - **Decoration**: `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 `` 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). **Word Trails** (`wordTrailsLayout`) is the quantitatively anchored word-cloud recipe: column = category, segment = ordered vertical position, weight = font size, with overlap-free stable placement. `wordColor`/`wordOpacity` receive `WordTrailsWordInfo`: canonical `word`/`column`/`weight`/`segment` plus the exact source `datum`, `dataIndex`, `columnIndex`, and `resolvedColumnColor`, so derived encodings do not need brittle compound-key lookup maps. Spread `wordTrailsProgressiveReveal({currentSegment, segmentDomain, oldestOpacity?, currentOpacity?, futureOpacity?, combineWeightOpacity?})` into `layoutConfig` for future hiding + linearly faded history without reflow; zero-opacity rows reserve geometry but emit no glyph or hit target. Analysis-derived color/distinctiveness remains a source-data field, not something the layout infers. `semiotic/recipes` ships pure layout functions (`waffleLayout`, `calendarLayout`, `marimekkoLayout`, `bulletLayout`, `parallelCoordinatesLayout`, `intervalLanesLayout`, `flextreeLayout`, `dagreLayout`, `lineageDagLayout`, `netEnsembleLayout`, `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`), `analyzeNetEnsemble` (the headless net census — see `netEnsembleLayout` below), 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 decoration 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 decoration 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`). `netEnsembleLayout` (network) is the complement — for **ensembles of disconnected/trivially-connected DAGs** (a "bag of little graphs" force layout scatters and dagre/flextree can't place). It splits into weakly-connected components, tests each for **directedness** (borrowing the mathematical *net*/directed-set idea: for a weakly-connected DAG, a single sink ⟹ everything converges to one limit; ≥2 sinks ⟹ it branches), fingerprints components with Weisfeiler–Leman refinement so **order-isomorphic motifs** group together, lays each out converging toward its sink(s) at the bottom, and arranges the ensemble as small multiples in motif bands (collapsing to one census glyph per component when cells get tiny). Config: `colorMode` (`"directedness"|"motif"|"category"`), `groupByMotif`, `sort`, `fingerprintRounds`, `minCellForFull`. `analyzeNetEnsemble(nodes, edges)` is the pure headless census (per-component `directed`/sink/source counts + motif classes) with no rendering. Demo + lay explanation at `/recipes/net-ensemble`. For fitted lineage regions, set `lineageDagLayout`'s `hullGroupAccessor` (for example, `"subtopologyId"`). It emits one sorted, deterministic convex hull per present group through `NetworkLayoutResult.backgrounds`, behind edges/nodes in live and static SVG. Optional controls: `hullColors`, `hullPadding` (16), `hullRadius` (12), `hullFillOpacity` (0.08), `hullStrokeOpacity` (0.4), and `hullLabel`. Omitting `hullGroupAccessor` emits no background layer. `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. **Motif Braid preparation** (`semiotic/recipes/core` or `semiotic/recipes`): `await prepareNetworkAtlasAsync(spec: NetworkAtlasSpec, source: NetworkAtlasSource)` returns `{ ok: true, atlas, issues }` or `{ ok: false, issues }`. Both preparation functions load their analysis code on demand. After checking `ok`, call `await prepareMotifBraid(atlas)` to obtain a `MotifBraidProjection`. Use `NetworkCustomChart` with `nodes={braid.sceneSeeds.nodes}`, `edges={braid.sceneSeeds.edges}`, `layout={motifBraidLayout}`, and `layoutConfig={{ braid }}`; the same props support `renderChart` from `semiotic/server`. The layout labels every prefix step with a rounded square at its depth, shares reference prefix ordering across selected comparison partitions, and keeps separate parallel strands until their prefixes diverge. Supply optional `stepEntityCounts` on each source occurrence: one finite, nonnegative traffic count per `nodePath` entry. Width tapers between adjacent counts using one scale across visible partitions (maximum 10 px, minimum 1 px for positive traffic; zero traffic has zero width). Omit the array to use `entityCount` at every step. Cohort weights and profile counts still use `entityCount`; step traffic does not change motif prevalence. Repeated vertices remain separate visits and short journeys end at their actual depth. Episodes end at the second visit to a repeated state; profile cells count each entity once and use the declared assigned denominator. Capsule expansion is not supported. `MotifBraidChart` itself is recipe-local. For Dependency X-Ray preparation, optionally set `forest.requiredPaths: { roots: ["world"], relationScopeId: "directed-admitted" }`. The existing public `prepareNetworkAtlasAsync` facades then return `atlas.requiredPaths`: original-graph immediate dominators, reachable and unreachable node IDs, and an exact/incomplete status. Null parents refer to the analytical synthetic root, not source edges. The scope currently admits only the full directed graph. `rooted-traversal:id-asc` and `rooted-traversal:id-desc` select an acyclic display backbone while retaining every other original edge as residual. Neither dominance nor a structural bypass proves usable capacity or AND-prerequisite completion. The X-Ray chart, projection, layout and queries remain source recipes; `/examples/dependency-xray` demonstrates the synthetic supplier study. Do not invent a packaged `DependencyForestChart` import or serialized component name. **Decoration (labels/axes/legends): the recipe owns it.** Recipes emit own decoration 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, semanticStatus, semanticDiagnostics, xDomain?, yDomain?, categories?, nodeCount?, edgeCount?, annotationCount, ariaLabel, warnings } — ground truth from the rendered scene; check paint status (`empty`/`markCount`) separately from capability-owned semantic viability (`meaningful` | `degraded` | `degenerate` | `not-assessed`). 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"`, ``, `<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. **Optional measured note wrapping (React only)**: install `@chenglou/pretext@0.0.9` and import `usePretextAnnotations` from `semiotic/text`. Pass `usePretextAnnotations(notes, { fontFamily: "Arial, sans-serif", fontSize: 14, lineHeight: 20 })` to the chart's `annotations`. Supports `label`, `callout`, `callout-circle`, `callout-rect`, and `bracket` notes; uses each note's `wrap` and shares measured lines with `autoPlaceAnnotations`. Options also include `enabled`, `fontWeight`, and `titleFontWeight`. Use named fonts. The hook waits for fonts after mounting and refreshes on font loading; SSR/hydration and unsupported browsers retain default wrapping. Axes, legends, plain `text` annotations, and HTML tooltips are unaffected. This hook is not a JSON/MCP configuration option; never serialize its internal callback. See `/annotations/text-layout` for the interactive comparison. **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) **Serializable chart-adjacent text**: `frame-text` anchors text to the resolved plot rectangle rather than a data scale, so it renders identically in live charts and `renderChart` SVG. Use `position` (`"top-left"|"top-center"|"top-right"|"middle-left"|"center"|"middle-right"|"bottom-left"|"bottom-center"|"bottom-right"`) plus pixel `dx`/`dy`; `label` and `text` are aliases. Example endpoint labels below a compact bar: `annotations={[{ type: "frame-text", text: "0", position: "bottom-left", dy: 16 }, { type: "frame-text", text: "100", position: "bottom-right", dy: 16 }]}`. Reserve enough margin for outward offsets. Prefer this over post-processing SVG strings or estimating the data-area transform. **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, legend-font-family, legend-font-weight, title-font-size, title-font-family, title-font-weight, 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)`, `designTokensToTheme(tokens)`, `resolveThemePreset(name)`. Native `semiotic.*` tokens round-trip title/legend size, family, and weight along with the base typography and color roles. **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), `evaluateChart`/`formatEvaluateChart` (one ranked data → deception → accessibility evaluation), `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`/`--evaluate`. ### 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?, identifiers?, fieldRoles? })`** → `ChartDataProfile`: per-role candidate fields (x/y/series/category/size/time), distinct counts, monotonicity, structure detection, normalized `fieldRoles`, and `identifiers`. Identifier/ignored fields are excluded from every encoding candidate. Semantic hints are `measure`, `dimension`, and `temporal`; exact hints are `x`, `y`, `size`, `category`, `series`, and `time`. - **`rederiveProfile(profile, { primary? })`** → a new coherent `ChartDataProfile` after candidate edits or explicit primary-role changes. It revalidates assignments and recomputes category/series/x counts, repeated/monotonic x, provenance, and stackability together; assigning an identifier or non-candidate throws. - **`deriveProfileFields(data, candidates, fieldRoles, { primary? })`** → the lower-level derived-field projection used by `rederiveProfile`; use it when a profiler adapter owns candidates separately from the complete profile object. - **`suggestCharts(data, { intent?, allow?, deny?, maxResults?, includeVariants?, minScore?, audience?, identifiers?, fieldRoles? })`** → ranked `Suggestion[]` with `{ component, family, importPath, variant?, score, intentScores, rubric, reasons, caveats, props, propContract }`. `props` is spreadable directly. `propContract` declares `componentKind`, whether chart-HOC defaults apply, the heading prop, and the valid mode vocabulary; the same per-component contracts live in `ai/surface-manifest.json#components.suggestionPropContracts`. Capability `fieldPolicy` declarations are enforced after `buildProps`, so custom accessor vocabularies and direct values cannot reintroduce an identifier as a measure. **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?, audience?, identifiers?, fieldRoles? })`** → evaluate a specific chart for a dataset. It applies the same audience familiarity, target, and non-visual receivability policy as `suggestCharts`, so a direct fit score and a ranked recommendation cannot disagree about the intended audience. - **`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`. A registered descriptor may declare `composes` plus optional `weights`; requested custom scores are blended from existing capability scores without changing no-intent/default rankings. `inferIntent(query, { mode: "schema", fields?, minimumConfidence? })` opts into whole-token field signals and typed data-shape signals; natural-language prose remains the default mode. - **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)`, and (for novel quantitative prop names or direct computed values) `fieldPolicy.measureAccessorProps` / `fieldPolicy.measureFields`. `semanticViability(props, evidence)` optionally reports stable warning/error diagnostics after paint; errors make render evidence `degenerate` even though `status` remains `ok`. - **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. If a consumer-owned role=img wraps the chart, use accessibleTable: { portalTarget: "element-id" } and render that target outside the image. For generated L1–L3 description or a navigable chart tree, use ChartContainer with chartConfig plus describe and/or navigable; do not invent frameProps fields. - **Cursor styling does not create behavior** (`interaction.cursor-is-presentation-only`): Cursor values in realtime props, retained mark styles, styleRules, and custom hit targets change pointer presentation only. They do not install click handlers, keyboard activation, observations, or accessibility semantics. Agent action: Use an actionable cursor only when the application separately supplies documented click or observation behavior and an accessible activation path. Treat cursor in serialized/static output as visual metadata, never as proof that a mark is interactive. - **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. Its thresholds use { value, color, label? }; BigNumber thresholds use the distinct { at, level, color?, label? } vocabulary. 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. - **Serialized proposals keep the initial snapshot** (`streaming.serialized-proposal-snapshot`): A JSON component/props proposal for MCP, SSR, or evaluation is a static snapshot even when the request mentions future pushes. Keep the supplied initial rows in the component's real data prop and do not invent pushRows, pushRequirement, ref, or method props. Agent action: Return renderable initial props in JSON. If the requested deliverable is React push code, separately omit the controlled data prop in JSX, attach a ref, and call the documented imperative method from an effect or event handler. Example: `JSON snapshot: { "component": "LineChart", "props": { "data": [{"week":1,"users":120}], "xAccessor":"week", "yAccessor":"users" } }. React push code instead omits data and calls ref.current?.push(row).` - **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. - **Axis formatters are React callbacks** (`serialization.formatters-are-react-callbacks`): xFormat, yFormat, categoryFormat, and valueFormat are callback props, not d3 format strings or axis-title strings. They are intentionally absent from JSON/MCP schemas and string values fail validation. Agent action: In serialized props, omit formatter callbacks and use xLabel, yLabel, categoryLabel, or valueLabel for axis titles. In React JSX, pass a function such as xFormat={value => formatAxis(value)}. Example: `JSON: { "xLabel": "Payload (KB)", "yLabel": "Response time (ms)" }. React-only: xFormat={value => formatNumber(value, { maximumFractionDigits: 0 })}.` - **Value components do not inherit chart-HOC props** (`value.bignumber-wire-contract`): BigNumber is a value component, not a chart HOC, so it does not inherit the common chart-HOC prop list. It uses label as its visible heading and supports description and summary; title and accessibleTable are invalid. Its percent format expects a ratio such as 0.97 and renders it as 97%. Agent action: Validate BigNumber against its own prop schema and remove inherited chart-HOC props such as title and accessibleTable before returning a proposal. Use label, description, and summary for text. For a 97-of-100 KPI, either pass value={0.97} with format="percent" or pass value={97} with format="number" and suffix="%"; do not combine value={97} with format="percent". Example: `{ "component": "BigNumber", "props": { "value": 97, "label": "SLA attainment", "format": "number", "suffix": "%", "target": { "value": 99, "label": "target", "format": "number" } } }` - **Proportional symbol maps use geographic props** (`geo.proportional-symbol-wire-shape`): ProportionalSymbolMap reads point rows from points, longitude from xAccessor (default lon), latitude from yAccessor (default lat), and radius from sizeBy. sizeRange is the two-number pixel-radius range. Agent action: Use points, xAccessor, yAccessor, and sizeBy. Do not rename points to data or sizeBy to valueAccessor merely because the map renders circles. Example: `{ "points": [{"city":"A","longitude":-122.4,"latitude":37.8,"incidents":18}], "xAccessor":"longitude", "yAccessor":"latitude", "sizeBy":"incidents", "sizeRange":[5,40] }` - **Physics charts separate chart mode from simulation input** (`physics.sample-and-mechanical-inputs`): Sample simulations use data plus the chart's accessors. Seeded no-data demonstrations use simulationMode="mechanical" (legacy mode="mechanical" remains accepted); mode otherwise carries chart display modes such as primary or sparkline. Agent action: For observed Galton values pass data + valueAccessor. For unit piles pass data + categoryAccessor + valueAccessor. Use seed for reproducibility, bins for Galton columns, and unitValue for the amount represented by one full UnitPile circle; remainders use proportional area. Example: `{ "component": "GaltonBoardChart", "props": { "data": [{"id":"a","value":1}], "valueAccessor":"value", "bins":4, "seed":42 } }` - **Physics push methods ingest source records** (`physics.push-uses-source-records`): Physics HOC refs push source records through the chart's accessors. pushRows and dataIdAccessor are not component props; stable source id fields are retained on spawned bodies without an invented accessor. Agent action: For React live code, call ref.current?.push(row) or pushMany(rows). For serialized snapshots, append the rows to data. Keep category/value/time accessors, but omit pushRows and dataIdAccessor. Example: `ref.current?.push({ id: "c", team: "Blue", value: 2 }); <UnitPileChart ref={ref} categoryAccessor="team" valueAccessor="value" unitValue={1} />` - **Distribution physics charts update bodies and projections together** (`physics.live-source-reconciliation`): Bodies, categories, domains, and totals update together without changing React keys. getData() returns source rows. New data replaces live rows; rerunMS restores the seed. Agent action: Omit data for push mode; data=[] stays empty and rejects pushes. Use stable IDs and remove for source edits; popBodies only removes marks. Set xExtent/valueExtent/timeExtent for stable domains. Example: `ref.current?.update("a", row => ({ ...row, value: 102 })); ref.current?.remove("b");` - **EventDrop distinguishes admission history from current closure** (`physics.event-admission-history`): Arrival order determines historical lateness with a delay/function watermark. Equal arrival times keep source order. A window is closed when its end is at or below the watermark; previously accepted events stay accepted as later events advance it. An explicit watermark.value instead tests one fixed policy. Agent action: Use watermark={{delay: n}} for arrival-ordered admission, or watermarkAtArrivalAccessor for recorded thresholds independent of current closure. Keep all times in the same units. The current board has solid lids: accepted history starts below them, late arrivals roll left. For a chronological demonstration, supply successive data/watermark states and coordinate source time with travel. timeScale does not replay lid changes. Source projection labels describe snapshot totals; readEventDropOccupancy(metadata, bodies) counts current containers and in-flight bodies independently of source flags. Example: `{ "data": [{"time":1,"arrivalTime":2,"admission":-3}], "watermarkAtArrivalAccessor":"admission", "watermark":{"value":100} }` <!-- 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. **Unified chart evaluation**: `evaluateChart(component, props, data?, { dataAudit?, inChartContainer?, describe?, navigable?, render?, notificationMax? })` (from `semiotic/ai`, `semiotic/ai/core`, or `semiotic/utils`) composes `auditData` → configuration/representation diagnoses → `auditAccessibility` into `{ validation, data, deception, accessibility, evidence?, ok, summary, findings, notifications }`. `findings` are ranked by severity (`error`, `warning`, `manual`) and stage; `formatEvaluateChart(result)` produces a compact report. Inject a `RenderFn` when server render evidence is available to turn empty scenes and renderer warnings into findings. Surfaced as the `evaluateChart` MCP tool and `npx semiotic-ai --evaluate`; it is static analysis, so manual assistive-technology testing remains required. **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 decoration (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. Complex families retain their reading structure: network nodes are grouped with link/flow branches; hierarchy parents announce direct children, total descendants, leaf counts, and leaf-value rollups (including zero/negative values); ChoroplethMap announces numeric coverage/range/average/total and then groups ranked regions into equal-width highest/middle/lowest thirds of the observed numeric range plus a no-value branch; other geo forms separate locations from routes. Empty thirds are omitted and all-equal metrics receive one explicit branch. Caps produce branch-local omission nodes rather than silently truncating. 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. `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 any semantic node carrying that authored datum, including hierarchy parents. GeoJSON `properties` are normalized to the direct-field shape emitted by StreamGeoFrame, so region sync works without nested-path configuration. 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 datum node — `annotatedIds` are the node 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"`** / **`tooltip={{ mode: "multi", content? }}`**: multi-series hover-anywhere for LineChart, AreaChart, StackedAreaChart, DifferenceChart (wires `tooltipMode:"multi"`). Built-in multi renderer when `content` is omitted. Custom multi: `tooltip={{ mode: "multi", content: (d) => … }}` — the function receives the unwrapped raw datum with hover-root `allSeries` / `xValue` re-attached (`{group, value, valuePx, color, datum}` entries). Legacy escape hatch: `tooltip={fn}` + `frameProps={{ tooltipMode: "multi" }}`. - **Legend**: "bottom" expands margin ~80px. Numeric margins are minima: chart-owned legends grow their side when measurement needs more room, while preserving a larger caller baseline. MultiAxisLineChart: use `legendPosition="bottom"`. A **bottom** legend is placed *outside* the bottom axis chrome (tick labels, axis title) so it doesn't overdraw it; the band is auto-measured. Top axes are opt-in, so a **top** legend reserves nothing by default — set `frameProps.legendLayout.axisGutter` for one. `axisGutter` also overrides the measured bottom band (`0` anchors to the plot edge); `sideGutter` is the left/right counterpart for plot-adjacent chrome, and `edgeGutter` defaults to 3px to keep a side legend's focus ring inside the SVG (`0` restores flush placement). - **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, extent }]`. `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 `extent: "nice"|"exact"` overrides chart-level `axisExtent` (common: exact x / nice y). Exact on a **value (y)** axis also skips y `extentPadding`; exact on x only affects tick placement. - **Adaptive time ticks**: `xFormat={adaptiveTimeTicks()}` or `adaptiveTimeTicks("minutes")`. Default formats in **UTC** for deterministic SSR. Timezone: `{ timeZone: "local" }` for the viewer, `{ timeZone: "America/Los_Angeles" }` (any IANA id) for a pinned region, or legacy `{ utc: false }` for local. `timeZone` wins over `utc` when both are set. - **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 decoration (`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 decoration helpers default to `--semiotic-*` tokens either way. ## Performance Prefer string accessors (`xAccessor="value"`) — always referentially stable. Memoize function accessors with `useCallback`. Threshold annotations (`x-threshold` and `y-threshold`) accept `endCap: "circle"` or `{ radius?, fill? }` at the top (x) or left (y) plot edge in both browser and static SVG. GaugeChart defaults to a native SVG center readout; custom HTML centerContent still uses foreignObject for SVG exports.