semiotic 3.5.4 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CLAUDE.md +196 -175
  2. package/README.md +52 -15
  3. package/ai/cli.js +41 -0
  4. package/ai/componentMetadata.cjs +11 -2
  5. package/ai/dist/mcp-server.js +454 -4
  6. package/ai/examples.md +98 -0
  7. package/ai/schema.json +614 -9
  8. package/ai/system-prompt.md +5 -2
  9. package/dist/components/AccessibleNavTree.d.ts +25 -0
  10. package/dist/components/Annotation.d.ts +40 -14
  11. package/dist/components/ChartContainer.d.ts +32 -2
  12. package/dist/components/ai/annotationProvenance.d.ts +349 -0
  13. package/dist/components/ai/audienceProfile.d.ts +147 -0
  14. package/dist/components/ai/audiences.d.ts +31 -0
  15. package/dist/components/ai/chartCapabilities.d.ts +55 -0
  16. package/dist/components/ai/chartCapabilityTypes.d.ts +254 -0
  17. package/dist/components/ai/chartRoles.d.ts +27 -0
  18. package/dist/components/ai/conversationArc.d.ts +379 -0
  19. package/dist/components/ai/dataScaleProfile.d.ts +320 -0
  20. package/dist/components/ai/describeChart.d.ts +114 -0
  21. package/dist/components/ai/diffProfile.d.ts +51 -0
  22. package/dist/components/ai/inferIntent.d.ts +24 -0
  23. package/dist/components/ai/intents.d.ts +34 -0
  24. package/dist/components/ai/navigationTree.d.ts +45 -0
  25. package/dist/components/ai/profileData.d.ts +16 -0
  26. package/dist/components/ai/qualityFixtures.d.ts +2 -0
  27. package/dist/components/ai/qualityScorecard.d.ts +82 -0
  28. package/dist/components/ai/readerGrounding.d.ts +70 -0
  29. package/dist/components/ai/repairChartConfig.d.ts +73 -0
  30. package/dist/components/ai/streamingTypes.d.ts +64 -0
  31. package/dist/components/ai/suggestCharts.d.ts +109 -0
  32. package/dist/components/ai/suggestDashboard.d.ts +92 -0
  33. package/dist/components/ai/suggestStreamCharts.d.ts +34 -0
  34. package/dist/components/ai/suggestStretchCharts.d.ts +60 -0
  35. package/dist/components/ai/useChartSuggestions.d.ts +22 -0
  36. package/dist/components/ai/useConversationArc.d.ts +89 -0
  37. package/dist/components/ai/useNavigationSync.d.ts +61 -0
  38. package/dist/components/ai/variantDiscovery.d.ts +168 -0
  39. package/dist/components/charts/geo/ChoroplethMap.capability.d.ts +2 -0
  40. package/dist/components/charts/geo/DistanceCartogram.capability.d.ts +2 -0
  41. package/dist/components/charts/geo/FlowMap.capability.d.ts +2 -0
  42. package/dist/components/charts/geo/ProportionalSymbolMap.capability.d.ts +2 -0
  43. package/dist/components/charts/index.d.ts +1 -1
  44. package/dist/components/charts/network/ChordDiagram.capability.d.ts +2 -0
  45. package/dist/components/charts/network/CirclePack.capability.d.ts +2 -0
  46. package/dist/components/charts/network/ForceDirectedGraph.capability.d.ts +2 -0
  47. package/dist/components/charts/network/OrbitDiagram.capability.d.ts +2 -0
  48. package/dist/components/charts/network/ProcessSankey.capability.d.ts +2 -0
  49. package/dist/components/charts/network/SankeyDiagram.capability.d.ts +2 -0
  50. package/dist/components/charts/network/TreeDiagram.capability.d.ts +2 -0
  51. package/dist/components/charts/network/Treemap.capability.d.ts +2 -0
  52. package/dist/components/charts/ordinal/BarChart.capability.d.ts +2 -0
  53. package/dist/components/charts/ordinal/BoxPlot.capability.d.ts +2 -0
  54. package/dist/components/charts/ordinal/DonutChart.capability.d.ts +2 -0
  55. package/dist/components/charts/ordinal/DotPlot.capability.d.ts +2 -0
  56. package/dist/components/charts/ordinal/FunnelChart.capability.d.ts +2 -0
  57. package/dist/components/charts/ordinal/GaugeChart.capability.d.ts +2 -0
  58. package/dist/components/charts/ordinal/GroupedBarChart.capability.d.ts +2 -0
  59. package/dist/components/charts/ordinal/Histogram.capability.d.ts +2 -0
  60. package/dist/components/charts/ordinal/Histogram.d.ts +4 -2
  61. package/dist/components/charts/ordinal/LikertChart.capability.d.ts +2 -0
  62. package/dist/components/charts/ordinal/LikertChart.d.ts +1 -1
  63. package/dist/components/charts/ordinal/LikertChart.defaults.d.ts +1 -0
  64. package/dist/components/charts/ordinal/PieChart.capability.d.ts +2 -0
  65. package/dist/components/charts/ordinal/RidgelinePlot.capability.d.ts +2 -0
  66. package/dist/components/charts/ordinal/StackedBarChart.capability.d.ts +2 -0
  67. package/dist/components/charts/ordinal/SwarmPlot.capability.d.ts +2 -0
  68. package/dist/components/charts/ordinal/SwimlaneChart.capability.d.ts +2 -0
  69. package/dist/components/charts/ordinal/ViolinPlot.capability.d.ts +2 -0
  70. package/dist/components/charts/realtime/RealtimeHeatmap.capability.d.ts +2 -0
  71. package/dist/components/charts/realtime/RealtimeHeatmap.d.ts +3 -0
  72. package/dist/components/charts/realtime/RealtimeHistogram.capability.d.ts +2 -0
  73. package/dist/components/charts/realtime/RealtimeHistogram.d.ts +3 -0
  74. package/dist/components/charts/realtime/RealtimeLineChart.capability.d.ts +2 -0
  75. package/dist/components/charts/realtime/RealtimeLineChart.d.ts +3 -0
  76. package/dist/components/charts/realtime/RealtimeSwarmChart.capability.d.ts +2 -0
  77. package/dist/components/charts/realtime/RealtimeSwarmChart.d.ts +3 -0
  78. package/dist/components/charts/realtime/RealtimeWaterfallChart.capability.d.ts +2 -0
  79. package/dist/components/charts/realtime/RealtimeWaterfallChart.d.ts +3 -0
  80. package/dist/components/charts/realtime/TemporalHistogram.capability.d.ts +7 -0
  81. package/dist/components/charts/shared/annotationHierarchy.d.ts +42 -0
  82. package/dist/components/charts/shared/annotationResolvers.d.ts +3 -2
  83. package/dist/components/charts/shared/annotationRules.d.ts +16 -0
  84. package/dist/components/charts/shared/annotationTypes.d.ts +14 -0
  85. package/dist/components/charts/shared/auditAccessibility.d.ts +90 -0
  86. package/dist/components/charts/shared/chartSpecs.d.ts +2 -37
  87. package/dist/components/charts/shared/diagnoseConfig.d.ts +4 -6
  88. package/dist/components/charts/shared/selectionUtils.d.ts +5 -2
  89. package/dist/components/charts/shared/streamPropsHelpers.d.ts +2 -0
  90. package/dist/components/charts/shared/types.d.ts +5 -1
  91. package/dist/components/charts/value/BigNumber.capability.d.ts +13 -0
  92. package/dist/components/charts/value/BigNumber.d.ts +14 -0
  93. package/dist/components/charts/value/formatting.d.ts +40 -0
  94. package/dist/components/charts/value/thresholdSparkline.d.ts +40 -0
  95. package/dist/components/charts/value/types.d.ts +292 -0
  96. package/dist/components/charts/xy/AreaChart.capability.d.ts +10 -0
  97. package/dist/components/charts/xy/BubbleChart.capability.d.ts +2 -0
  98. package/dist/components/charts/xy/CandlestickChart.capability.d.ts +2 -0
  99. package/dist/components/charts/xy/ConnectedScatterplot.capability.d.ts +2 -0
  100. package/dist/components/charts/xy/DifferenceChart.capability.d.ts +8 -0
  101. package/dist/components/charts/xy/Heatmap.capability.d.ts +9 -0
  102. package/dist/components/charts/xy/LineChart.capability.d.ts +9 -0
  103. package/dist/components/charts/xy/MinimapChart.capability.d.ts +2 -0
  104. package/dist/components/charts/xy/MultiAxisLineChart.capability.d.ts +2 -0
  105. package/dist/components/charts/xy/QuadrantChart.capability.d.ts +2 -0
  106. package/dist/components/charts/xy/QuadrantChart.d.ts +5 -2
  107. package/dist/components/charts/xy/QuadrantChart.defaults.d.ts +2 -0
  108. package/dist/components/charts/xy/Scatterplot.capability.d.ts +2 -0
  109. package/dist/components/charts/xy/StackedAreaChart.capability.d.ts +2 -0
  110. package/dist/components/data/DataSummarizer.d.ts +45 -0
  111. package/dist/components/realtime/lifecycleBands.d.ts +44 -0
  112. package/dist/components/realtime/types.d.ts +23 -8
  113. package/dist/components/recipes/annotationDensity.d.ts +69 -0
  114. package/dist/components/recipes/annotationLayout.d.ts +93 -0
  115. package/dist/components/semiotic-ai.d.ts +58 -0
  116. package/dist/components/semiotic-realtime.d.ts +2 -0
  117. package/dist/components/semiotic-recipes.d.ts +4 -0
  118. package/dist/components/semiotic-utils.d.ts +8 -0
  119. package/dist/components/semiotic-value.d.ts +55 -0
  120. package/dist/components/semiotic-xy.d.ts +1 -1
  121. package/dist/components/semiotic.d.ts +8 -1
  122. package/dist/components/server/staticAnnotations.d.ts +2 -0
  123. package/dist/components/store/useChartFocus.d.ts +43 -0
  124. package/dist/components/store/useChartInterrogation.d.ts +141 -0
  125. package/dist/components/stream/AccessibleDataTable.d.ts +10 -1
  126. package/dist/components/stream/NetworkSVGOverlay.d.ts +11 -5
  127. package/dist/components/stream/OrdinalSVGOverlay.d.ts +2 -0
  128. package/dist/components/stream/SVGOverlay.d.ts +2 -0
  129. package/dist/components/stream/geoTypes.d.ts +3 -0
  130. package/dist/components/stream/networkTypes.d.ts +2 -0
  131. package/dist/components/stream/ordinalTypes.d.ts +2 -0
  132. package/dist/components/stream/types.d.ts +2 -0
  133. package/dist/geo.min.js +1 -1
  134. package/dist/geo.module.min.js +1 -1
  135. package/dist/network.min.js +1 -1
  136. package/dist/network.module.min.js +1 -1
  137. package/dist/ordinal.min.js +1 -1
  138. package/dist/ordinal.module.min.js +1 -1
  139. package/dist/realtime.min.js +1 -1
  140. package/dist/realtime.module.min.js +1 -1
  141. package/dist/semiotic-ai.d.ts +58 -0
  142. package/dist/semiotic-ai.min.js +1 -1
  143. package/dist/semiotic-ai.module.min.js +1 -1
  144. package/dist/semiotic-realtime.d.ts +2 -0
  145. package/dist/semiotic-recipes.d.ts +4 -0
  146. package/dist/semiotic-recipes.min.js +1 -1
  147. package/dist/semiotic-recipes.module.min.js +1 -1
  148. package/dist/semiotic-themes.min.js +1 -1
  149. package/dist/semiotic-themes.module.min.js +1 -1
  150. package/dist/semiotic-utils.d.ts +8 -0
  151. package/dist/semiotic-utils.min.js +1 -1
  152. package/dist/semiotic-utils.module.min.js +1 -1
  153. package/dist/semiotic-value.d.ts +55 -0
  154. package/dist/semiotic-value.min.js +2 -0
  155. package/dist/semiotic-value.module.min.js +2 -0
  156. package/dist/semiotic-xy.d.ts +1 -1
  157. package/dist/semiotic.d.ts +8 -1
  158. package/dist/semiotic.min.js +1 -1
  159. package/dist/semiotic.module.min.js +1 -1
  160. package/dist/server.min.js +1 -1
  161. package/dist/server.module.min.js +1 -1
  162. package/dist/xy.min.js +1 -1
  163. package/dist/xy.module.min.js +1 -1
  164. package/package.json +28 -5
package/CLAUDE.md CHANGED
@@ -4,56 +4,53 @@
4
4
  - Install: `npm install semiotic`
5
5
  <!-- semiotic-bundle-sizes:start -->
6
6
  <!-- Auto-generated by scripts/sync-bundle-sizes.mjs — do not edit by hand. -->
7
- - **Use sub-path imports** — `semiotic/xy` (85KB gz), `semiotic/ordinal` (69KB gz), `semiotic/network` (63KB gz), `semiotic/geo` (52KB gz), `semiotic/realtime` (90KB gz), `semiotic/server` (122KB gz), `semiotic/utils` (22KB gz), `semiotic/recipes` (5KB gz), `semiotic/themes` (4KB gz), `semiotic/data` (3KB gz), `semiotic/ai` (189KB gz). Full `semiotic` is 188KB gz.
7
+ - **Use sub-path imports** — `semiotic/xy` (90KB gz), `semiotic/ordinal` (74KB gz), `semiotic/network` (68KB gz), `semiotic/geo` (55KB gz), `semiotic/realtime` (95KB gz), `semiotic/server` (127KB gz), `semiotic/utils` (37KB gz), `semiotic/recipes` (9KB gz), `semiotic/themes` (4KB gz), `semiotic/data` (3KB gz), `semiotic/value` (6KB gz), `semiotic/ai` (246KB gz). Full `semiotic` is 203KB gz.
8
8
  <!-- semiotic-bundle-sizes:end -->
9
- - CLI: `npx semiotic-ai [--schema|--compact|--examples|--doctor]`
10
- - MCP: `npx semiotic-mcp`
9
+ - CLI: `npx semiotic-ai [--schema|--compact|--examples|--doctor|--audit-a11y]` · MCP: `npx semiotic-mcp`
11
10
 
12
11
  ## Architecture
13
- - **HOC Charts**: Simple props, sensible defaults. **Stream Frames**: Full control.
14
- - **Always use HOC charts** unless you need control they don't expose. Stream Frames pass `RealtimeNode`/`RealtimeEdge` wrappers in callbacks, not your data.
15
- - Every HOC accepts `frameProps` for pass-through. TypeScript `strict: true`. Every HOC has error boundary + dev-mode validation.
12
+ 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`.
16
13
 
17
14
  ## Common Props (all HOCs)
18
- `title`, `description` (aria-label), `summary` (sr-only), `width` (600), `height` (400), `responsiveWidth`, `responsiveHeight`, `margin`, `className`, `color` (uniform fill), `stroke` (uniform stroke color — CSS var OK), `strokeWidth` (uniform stroke width in px), `opacity` (uniform 0–1 opacity), `enableHover` (true), `tooltip` (boolean | "multi" | function | config object), `showLegend`, `showGrid` (false), `frameProps`, `onObservation`, `onClick`, `chartId`, `loading` (false), `loadingContent` (ReactNode — replaces the default skeleton when `loading` is true; pass `false` to suppress the loading UI entirely), `emptyContent`, `legendInteraction` ("none"|"highlight"|"isolate"), `legendPosition` ("right"|"left"|"top"|"bottom"), `emphasis` ("primary"|"secondary"), `annotations` (array), `accessibleTable` (true), `hoverHighlight` (boolean — dims non-hovered series, requires `colorBy`), `hoverRadius` (30), `animate` (boolean | { duration?, easing?, intro? } — animated intro on first render + smooth transitions on data change; intro defaults to true when animate is enabled), `axisExtent` ("nice" default | "exact" — pins first/last tick to actual data min/max with equidistant intermediates. Applies to XY x/y axes and ordinal value axis only; no-op on network/geo/hierarchy. Explicit `tickValues` still wins.)
15
+ `title`, `description` (aria-label), `summary` (sr-only), `width` (600), `height` (400), `responsiveWidth`, `responsiveHeight`, `margin`, `className`, `color` (uniform fill), `stroke`, `strokeWidth`, `opacity`, `enableHover` (true), `tooltip` (boolean | "multi" | function | config), `showLegend`, `showGrid` (false), `frameProps`, `onObservation`, `onClick`, `chartId`, `loading`, `loadingContent` (ReactNode; `false` suppresses), `emptyContent`, `legendInteraction` ("none"|"highlight"|"isolate"), `legendPosition` ("right"|"left"|"top"|"bottom"), `emphasis` ("primary"|"secondary"), `annotations`, `accessibleTable` (true), `hoverHighlight` (requires `colorBy`), `hoverRadius` (30), `animate` (boolean | {duration?, easing?, intro?}), `axisExtent` ("nice"|"exact" — pins first/last tick to data min/max; XY x/y + ordinal value axis only).
19
16
 
20
- **Primitive styling props** (`color`, `stroke`, `strokeWidth`, `opacity`) apply to any shape the chart draws (bars, circles, lines, wedges, rects). Precedence: top-level prop > `frameProps.*Style` function return > HOC base > theme fallback. Use CSS variables (`stroke="var(--semiotic-border)"`) for theme-aware, cascade-overridable styling. For per-datum customization, keep using the function-form `frameProps.pieceStyle` / `pointStyle` / `lineStyle` etc. — the top-level prop overlays on top of whatever the function returns.
17
+ **Primitive styling** (`color`/`stroke`/`strokeWidth`/`opacity`): apply to any shape the chart draws. Precedence: top-level prop > `frameProps.*Style` fn return > HOC base > theme. Use CSS vars (`stroke="var(--semiotic-border)"`) for cascade-overridable theming. Per-datum: use `frameProps.pieceStyle`/`pointStyle`/`lineStyle` fn form.
21
18
 
22
19
  `onClick` receives `(datum, { x, y })`. `onObservation` receives `{ type, datum?, x?, y?, timestamp, chartType, chartId }`.
23
20
 
24
21
  ## XY Charts (`semiotic/xy`)
25
22
 
26
- **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` (asymmetric min/max envelope: `{ y0Accessor, y1Accessor, style?, perSeries?, interactive? }` or array for fan charts; participates in yExtent; non-interactive by default; hovered datum is enriched with `band: {y0,y1}` and `bands: [...]`), `directLabel`, `gapStrategy`, `xScaleType`/`yScaleType` ("linear"|"log"|"time"), `tooltip="multi"` for hover-anywhere series comparison
27
- **AreaChart** — LineChart props + `areaBy`, `y0Accessor`, `gradientFill`, `areaOpacity` (0.7), `showLine` (true), `band` (same shape as LineChart — decorative envelope under the area), `tooltip="multi"` for hover-anywhere area comparison
28
- **DifferenceChart** — Two-series A/B comparison. Fills the area between with `seriesAColor` where A > B and `seriesBColor` where B > A; crossovers interpolated. Props: `data`, `xAccessor` ("x"), `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` (max raw rows in push buffer; FIFO eviction). Push API via `ref.current.push({x, a, b})`. Accessor outputs coerce through `toNumber` so `Date` + numeric strings are accepted.
29
- **StackedAreaChart** — flat array + `areaBy` (required), `colorBy`, `normalize`, `baseline` (`"zero"` default | `"wiggle"` streamgraph | `"silhouette"` centered), `stackOrder` (`"key"` alpha | `"insideOut"` largest-in-middle | `"asc"`/`"desc"`). Streamgraph: `baseline="wiggle"` + `stackOrder="insideOut"`. `baseline` ⊥ `normalize`. No `lineBy`/`lineDataAccessor`. `tooltip="multi"` lists every series at the hovered x (values interpolated between samples).
30
- **Scatterplot** — `data`, `xAccessor`, `yAccessor`, `colorBy`, `sizeBy`, `sizeRange`, `pointRadius` (5), `pointOpacity` (0.8), `marginalGraphics`, `regression` (boolean | "linear" | "polynomial" | "loess" | RegressionConfig — sugar for a trend-annotation overlay; sits underneath user annotations)
23
+ **LineChart** — `data`, `xAccessor` ("x"), `yAccessor` ("y"), `lineBy`, `lineDataAccessor`, `colorBy`, `colorScheme`, `curve`, `lineWidth` (2), `showPoints`, `pointRadius` (3), `fillArea` (boolean|string[]), `areaOpacity` (0.3), `lineGradient`, `anomaly`, `forecast`, `band` ({y0Accessor, y1Accessor, style?, perSeries?, interactive?} or array for fan charts; participates in yExtent; non-interactive by default), `directLabel`, `gapStrategy`, `xScaleType`/`yScaleType` ("linear"|"log"|"time"), `tooltip="multi"` for hover-anywhere
24
+ **AreaChart** — LineChart props + `areaBy`, `y0Accessor`, `gradientFill`, `areaOpacity` (0.7), `showLine` (true), `band`, `tooltip="multi"`
25
+ **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`.
26
+ **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.
27
+ **Scatterplot** — `xAccessor`, `yAccessor`, `colorBy`, `sizeBy`, `sizeRange`, `pointRadius` (5), `pointOpacity` (0.8), `marginalGraphics`, `regression` (boolean | "linear"|"polynomial"|"loess" | RegressionConfig — sugar for trend overlay)
31
28
  **BubbleChart** — Scatterplot + `sizeBy` (required), `sizeRange` ([5,40]), `regression`
32
29
  **ConnectedScatterplot** — + `orderAccessor`, `regression`
33
- **QuadrantChart** — Scatterplot + `quadrants` (required), `xCenter`, `yCenter`
34
- **MultiAxisLineChart** — Dual Y-axis. `series` (required: `[{ yAccessor, label?, color?, format?, extent? }]`). Falls back to multi-line if not 2 series.
35
- **Heatmap** — `data`, `xAccessor`, `yAccessor`, `valueAccessor`, `colorScheme`, `showValues`, `cellBorderColor`
36
- **ScatterplotMatrix** — `data`, `fields` (array of numeric field names for grid)
30
+ **QuadrantChart** — Scatterplot + `quadrants`, `xCenter`, `yCenter`
31
+ **MultiAxisLineChart** — Dual Y-axis. `series` (`[{yAccessor, label?, color?, format?, extent?}]`). Falls back to multi-line if 2 series.
32
+ **Heatmap** — `xAccessor`, `yAccessor`, `valueAccessor`, `colorScheme`, `showValues`, `cellBorderColor`
33
+ **ScatterplotMatrix** — `fields` (numeric field names)
37
34
  **MinimapChart** — Overview + detail with linked zoom. Wraps an XY chart.
38
- **CandlestickChart** — `data`, `xAccessor`, `highAccessor` (req), `lowAccessor` (req), `openAccessor` + `closeAccessor` (optional). With all four: OHLC bars. With only high/low: degrades to a range chart. `candlestickStyle` ({ upColor, downColor, wickColor, rangeColor, bodyWidth, wickWidth }). Honors `mode` (primary/context/sparkline).
35
+ **CandlestickChart** — `xAccessor`, `highAccessor` (req), `lowAccessor` (req), `openAccessor`+`closeAccessor` (optional OHLC; high/low only range). `candlestickStyle` ({upColor, downColor, wickColor, rangeColor, bodyWidth, wickWidth}). Honors `mode`.
39
36
 
40
37
  ## Ordinal Charts (`semiotic/ordinal`)
41
38
 
42
- **BarChart** — `data`, `categoryAccessor`, `valueAccessor`, `orientation`, `colorBy`, `sort`, `barPadding` (40), `roundedTop`, `gradientFill` (`true` | `{topOpacity, bottomOpacity}` | `{colorStops}` — same API as AreaChart; runs tip→base), `regression` (boolean | "linear" | "polynomial" | "loess" | RegressionConfig — sugar for a trend-annotation overlay; categories regressed as category-index, line projects through band scale)
43
- **StackedBarChart** — + `stackBy` (required), `normalize`, `sort` (default false — insertion order)
44
- **GroupedBarChart** — + `groupBy` (required), `barPadding` (60), `sort` (default false — insertion order)
39
+ **BarChart** — `categoryAccessor`, `valueAccessor`, `orientation`, `colorBy`, `sort`, `barPadding` (40), `roundedTop`, `gradientFill` (true | {topOpacity, bottomOpacity} | {colorStops}; tip→base), `regression`
40
+ **StackedBarChart** — + `stackBy` (required), `normalize`, `sort` (false default — insertion order)
41
+ **GroupedBarChart** — + `groupBy` (required), `barPadding` (60), `sort` (false default)
45
42
  **SwarmPlot** — `colorBy`, `sizeBy`, `pointRadius`, `pointOpacity`
46
43
  **BoxPlot** — + `showOutliers`, `outlierRadius`
47
44
  **Histogram** — + `bins` (25), `relative`. Always horizontal.
48
45
  **ViolinPlot** — + `bins`, `curve`, `showIQR`
49
46
  **RidgelinePlot** — + `bins`, `amplitude` (1.5)
50
- **DotPlot** — + `sort` ("auto" — insertion order when streaming, value-desc on static), `dotRadius`, `showGrid` default true, `regression`
47
+ **DotPlot** — + `sort` ("auto"), `dotRadius`, `showGrid` (true default), `regression`
51
48
  **PieChart** — `categoryAccessor`, `valueAccessor`, `colorBy`, `startAngle`
52
49
  **DonutChart** — PieChart + `innerRadius` (60), `centerContent`
53
- **FunnelChart** — `stepAccessor`, `valueAccessor`, `categoryAccessor` (optional), `connectorOpacity`, `orientation`
54
- **SwimlaneChart** — `categoryAccessor`, `subcategoryAccessor` (required), `valueAccessor`, `colorBy` (defaults to subcategoryAccessor), `orientation`, `roundedTop` (pixel radius applied to both outer ends of each lane left+right for horizontal, top+bottom for vertical. Middle segments stay square so adjacent pieces butt against each other; single-segment lanes round all four corners.)
55
- **LikertChart** — `categoryAccessor`, `valueAccessor`|`levelAccessor`+`countAccessor`, `levels` (required), `orientation`, `colorScheme`
56
- **GaugeChart** — `value` (required), `min`, `max`, `thresholds`, `arcWidth`, `cornerRadius` (pixel radius for rounded segment ends — same semantics as DonutChart), `sweep`, `fillZones`, `showNeedle`, `centerContent`
50
+ **FunnelChart** — `stepAccessor`, `valueAccessor`, `categoryAccessor?`, `connectorOpacity`, `orientation`
51
+ **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)
52
+ **LikertChart** — `categoryAccessor`, `valueAccessor`|`levelAccessor`+`countAccessor`, `levels?`, `orientation`, `colorScheme`
53
+ **GaugeChart** — `value` (req), `min`, `max`, `thresholds`, `arcWidth`, `cornerRadius` (rounded segment ends), `sweep`, `fillZones`, `showNeedle`, `centerContent`
57
54
 
58
55
  All ordinal: `colorBy`, `colorScheme`, `categoryFormat` (string|ReactNode), `showCategoryTicks` (true).
59
56
 
@@ -61,7 +58,7 @@ All ordinal: `colorBy`, `colorScheme`, `categoryFormat` (string|ReactNode), `sho
61
58
 
62
59
  **ForceDirectedGraph** — `nodes`, `edges`, `nodeIDAccessor`, `sourceAccessor`, `targetAccessor`, `colorBy`, `nodeSize`, `nodeSizeRange`, `edgeWidth`, `iterations` (300), `forceStrength` (0.1), `showLabels`, `nodeLabel`
63
60
  **SankeyDiagram** — `edges`, `nodes`, `valueAccessor`, `nodeIdAccessor`, `colorBy`, `edgeColorBy`, `orientation`, `nodeAlign`, `nodeWidth`, `nodePaddingRatio`, `showLabels`
64
- **ProcessSankey** — temporal sankey with a real time x-axis. `nodes`, `edges` (each with `startTime`/`endTime`), `domain` (required `[t0, t1]`), `axisTicks?`, `xExtentAccessor` (optional `[start, end]` lifetime per node — lane spans `min(xExtent[0], earliestEdge)` to `max(xExtent[1], latestEdge)`), `colorBy`/`colorScheme`/`showLegend`/`legendPosition`, `pairing` ("value"|"temporal"), `packing` ("off"|"reuse"), `laneOrder` ("crossing-min"|"inside-out"|"crossing-min+inside-out"|"insertion"), `lifetimeMode` ("full"|"half"), `ribbonLane` ("source"|"target"|"both"), `showLaneRails`, `showLabels` (default true), `showQualityReadout`, `showParticles` + `particleStyle` (same shape as SankeyDiagram, canvas + ParticlePool), `timeFormat`/`valueFormat`, push API via ref. Static-graph cycles are valid as long as edges move forward in time. Use for time-stamped flow events; use `SankeyDiagram` for static total-flow snapshots.
61
+ **ProcessSankey** — temporal sankey with real time x-axis. `nodes`, `edges` (each with `startTime`/`endTime`), `domain` (req `[t0, t1]`), `axisTicks?`, `xExtentAccessor` (optional `[start, end]` lifetime per node), `colorBy`/`colorScheme`/`showLegend`/`legendPosition`, `pairing` ("value"|"temporal"), `packing` ("off"|"reuse"), `laneOrder` ("crossing-min"|"inside-out"|"crossing-min+inside-out"|"insertion"), `lifetimeMode` ("full"|"half"), `ribbonLane` ("source"|"target"|"both"), `showLaneRails`, `showLabels` (true), `showQualityReadout`, `showParticles` + `particleStyle`, `timeFormat`/`valueFormat`, push API via ref. Static-graph cycles OK as long as edges move forward in time. Use ProcessSankey for time-stamped events; SankeyDiagram for static snapshots.
65
62
  **ChordDiagram** — `edges`, `nodes`, `valueAccessor`, `edgeColorBy`, `padAngle`, `showLabels`
66
63
  **TreeDiagram** — `data` (root), `layout`, `orientation`, `childrenAccessor`, `colorBy`, `colorByDepth`
67
64
  **Treemap** — `data` (root), `childrenAccessor`, `valueAccessor`, `colorBy`, `colorByDepth`, `showLabels`
@@ -70,21 +67,40 @@ All ordinal: `colorBy`, `colorScheme`, `categoryFormat` (string|ReactNode), `sho
70
67
 
71
68
  ## Geo Charts (`semiotic/geo`)
72
69
 
73
- Import from `semiotic/geo` NOT `semiotic` to avoid pulling d3-geo into non-geo bundles.
70
+ Import from `semiotic/geo` onlyavoids d3-geo in non-geo bundles.
74
71
 
75
72
  **ChoroplethMap** — `areas` (GeoJSON Feature[] or "world-110m"), `valueAccessor`, `colorScheme`, `projection` ("equalEarth"), `graticule`, `tooltip`, `showLegend`
76
- **ProportionalSymbolMap** — `points`, `xAccessor` ("lon"), `yAccessor` ("lat"), `sizeBy`, `sizeRange`, `colorBy`, `areas` (optional background)
73
+ **ProportionalSymbolMap** — `points`, `xAccessor` ("lon"), `yAccessor` ("lat"), `sizeBy`, `sizeRange`, `colorBy`, `areas?`
77
74
  **FlowMap** — `flows`, `nodes`, `valueAccessor`, `edgeColorBy`, `lineType`, `showParticles`
78
75
  **DistanceCartogram** — `points`, `center`, `costAccessor`, `strength`, `showRings`
79
76
 
80
- All geo: `fitPadding`, `zoomable`, `zoomExtent`, `onZoom`, `dragRotate`, `graticule`, `tileURL`, `tileAttribution`
81
- Helpers: `resolveReferenceGeography("world-110m"|"world-50m")`, `mergeData(features, data, { featureKey, dataKey })`
77
+ All geo: `fitPadding`, `zoomable`, `zoomExtent`, `onZoom`, `dragRotate`, `graticule`, `tileURL`, `tileAttribution`. Helpers: `resolveReferenceGeography("world-110m"|"world-50m")`, `mergeData(features, data, {featureKey, dataKey})`.
78
+
79
+ ## Value Charts (`semiotic/value`)
80
+
81
+ 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.
82
+
83
+ **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).
84
+
85
+ **Two chart slot positions, picked by chart aspect:**
86
+ - **`trendSlot`** — wide / rectangular charts beneath the value (LineChart, AreaChart, DifferenceChart in `mode="sparkline"`). Renders at full card width.
87
+ - **`chartSlot`** — square charts beside the value (DonutChart, PieChart, Scatterplot, Treemap, CirclePack). Splits the card horizontally: text-on-left, chart-on-right.
88
+ - Pair both: square chart anchors top-right, wide trend stretches across the bottom.
89
+ - 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.
90
+
91
+ Push API via `forwardRef`: `ref.current.push(value | {value, time?, comparison?})`, `pushMany`, `clear`, `getValue()`, `getData()`. Stable across renders (refs back the imperative handle).
92
+
93
+ 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).
94
+
95
+ 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.
96
+
97
+ Helpers exported: `buildFormatter`, `formatSignedDelta`, `formatDeltaPercent`, `formatDuration`, `resolveThreshold`, `colorForLevel`, `buildSparklinePath` (for custom-slot rendering).
82
98
 
83
99
  ## Realtime Charts (`semiotic/realtime`)
84
100
 
85
- Push API: `ref.current.push({ time, value })`. All pushed data **must** include a time field.
101
+ Push API: `ref.current.push({time, value})`. All pushed data must include a time field.
86
102
 
87
- **RealtimeLineChart**, **RealtimeHistogram** (+ `brush`, `onBrush`, `linkedBrush`, `direction`), **TemporalHistogram** (static-data sibling of RealtimeHistogram — same props minus `windowSize`/`windowMode`; takes a bounded `data` array), **RealtimeSwarmChart**, **RealtimeWaterfallChart**, **RealtimeHeatmap**, **Streaming Sankey** (StreamNetworkFrame + `showParticles`)
103
+ **RealtimeLineChart**, **RealtimeHistogram** (+ `brush`, `onBrush`, `linkedBrush`, `direction`), **TemporalHistogram** (static sibling — same props minus `windowSize`/`windowMode`), **RealtimeSwarmChart**, **RealtimeWaterfallChart**, **RealtimeHeatmap**, **Streaming Sankey** (StreamNetworkFrame + `showParticles`).
88
104
 
89
105
  Encoding: `decay`, `pulse`, `transition`, `staleness` — compose freely.
90
106
 
@@ -94,105 +110,50 @@ Most HOCs support push via `forwardRef`. **Omit** `data` — do NOT pass `data={
94
110
  const ref = useRef()
95
111
  ref.current.push({ id: "p1", x: 1, y: 2 })
96
112
  ref.current.pushMany([...points])
97
- ref.current.replace([...points]) // ordinal only — full dataset replacement, preserves category order + transitions (progressively chunks large datasets)
98
- ref.current.remove("p1") // by ID — requires pointIdAccessor
99
- ref.current.remove(["p1", "p2"]) // batch remove
100
- ref.current.update("p1", d => ({ ...d, y: 99 })) // in-place update — requires pointIdAccessor
113
+ ref.current.replace([...points]) // ordinal only — bounded-ingest, preserves category order + transitions
114
+ ref.current.remove("p1" | ["p1","p2"]) // requires ID accessor
115
+ ref.current.update("p1", d => ({ ...d, y: 99 })) // requires ID accessor
101
116
  ref.current.clear()
102
117
  ref.current.getData()
103
- ref.current.getScales() // returns {o, r, projection} (ordinal) / {x, y} (XY) — null if not yet computed
118
+ ref.current.getScales() // {o, r, projection} (ordinal) | {x, y} (XY) — null if unmounted
104
119
  <Scatterplot ref={ref} xAccessor="x" yAccessor="y" pointIdAccessor="id" />
105
120
  ```
106
- `remove()` and `update()` require an ID accessor: `pointIdAccessor` on XY/realtime charts, `dataIdAccessor` on ordinal charts. `replace()` is ordinal-only and routes through a bounded-ingest path that preserves category insertion-order memory and the transition position snapshot — what aggregator HOCs like LikertChart use under the hood to re-aggregate streaming input without shuffling categories or losing animations. Network HOC refs also use `remove(id)`/`update(id, updater)` (operates on nodes). For edge-level operations, use `StreamNetworkFrameHandle` directly: `removeNode(id)`, `removeEdge(sourceId, targetId)` or `removeEdge(edgeId)` (requires `edgeIdAccessor`), `updateNode(id, updater)`, `updateEdge(sourceId, targetId, updater)`.
121
+ 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)`.
107
122
  Not supported: Tree, Treemap, CirclePack, Orbit, ChoroplethMap, FlowMap, ScatterplotMatrix.
108
123
 
109
124
  ## Custom Charts (escape hatch)
110
125
 
111
- When the catalog doesn't fit, three HOCs let you supply a layout function that emits scene primitives directly. The frame still owns hit testing, transitions, decay, theme cascade, and SSR — your layout owns geometry only.
112
-
113
- - **`XYCustomChart`** (`semiotic/xy`) — XY layouts: waffle, calendar heatmap, custom point/line/area arrangements
114
- - **`OrdinalCustomChart`** (`semiotic/ordinal`) — category × value layouts: marimekko, parallel coordinates, bullet, fan chart, slope graph
115
- - **`NetworkCustomChart`** (`semiotic/network`) — graph layouts: flextree, dagre, custom force/radial
116
-
117
- All three accept `layout` and `layoutConfig` (your own typed config), but the layout context and return shape differ by chart family:
118
-
119
- - **`XYCustomChart` / `OrdinalCustomChart`** — `layout: (ctx) => { nodes, overlays? }`. Context exposes `data`, `scales`, `dimensions` (with plot rect — center-anchored for radial ordinal, top-left otherwise), `theme` (semantic + categorical), `resolveColor(key)`, and `config`. XY scales: `{ x, y }` (linear). Ordinal scales: `{ o, r, projection }` (band + linear).
120
- - **`NetworkCustomChart`** — `layout: (ctx) => { sceneNodes?, sceneEdges?, labels?, overlays? }`. Context exposes `nodes`, `edges`, `dimensions`, `theme`, `resolveColor(key)`, and `config` — graph data, no `data`/`scales`. Network layouts often run an external positioner (`d3-flextree`, `dagre`) on nodes/edges, then emit network scene primitives (`circle`, `rect`, `arc` for nodes; `line`, `bezier`, `curved` for edges).
121
-
122
- XY/ordinal frames render whatever you put in `nodes` (rect, point, area, line, wedge, connector, etc.). Network frames split node-shaped scenes from edge-shaped scenes — give them `sceneNodes` for the round/rect/arc visuals and `sceneEdges` for the connecting paths. All three frames handle painting, hit testing, accessibility, transitions, decay, and SSR for you.
123
-
124
- ```tsx
125
- import { XYCustomChart } from "semiotic/xy"
126
- import { OrdinalCustomChart } from "semiotic/ordinal"
127
- import { NetworkCustomChart } from "semiotic/network"
128
- import {
129
- waffleLayout, calendarLayout, // XY recipes
130
- marimekkoLayout, bulletLayout, parallelCoordinatesLayout, // ordinal
131
- flextreeLayout, dagreLayout, // network
132
- } from "semiotic/recipes"
133
-
134
- <XYCustomChart data={cells} layout={waffleLayout} layoutConfig={{ rows: 10, columns: 10, ... }} />
135
- <OrdinalCustomChart data={revenue} layout={marimekkoLayout} layoutConfig={{ ... }} />
136
- <NetworkCustomChart nodes={nodes} edges={edges} layout={flextreeLayout} layoutConfig={{ ... }} />
137
- ```
138
-
139
- **Recipes subpath** (`semiotic/recipes`) ships pure layout functions. They emit standard SceneNodes — no chart code. BYO heavy deps (`d3-flextree`, `dagre`) live in user code.
126
+ When the catalog doesn't fit, three HOCs take a layout function emitting scene primitives. Frame still owns hit testing, transitions, decay, theme, SSR.
140
127
 
141
- ### Chrome (labels, axes, legends): the recipe owns it
128
+ - **`XYCustomChart`** (`semiotic/xy`) — waffle, calendar heatmap, custom point/line/area
129
+ - **`OrdinalCustomChart`** (`semiotic/ordinal`) — marimekko, parallel coords, bullet, fan, slope
130
+ - **`NetworkCustomChart`** (`semiotic/network`) — flextree, dagre, custom force/radial
142
131
 
143
- Custom layouts often need chrome the standard chart axes can't render — variable-width bars, per-row independent value scales, parallel axes, asymmetric tree branches. The unified pattern: **the recipe emits its own labels/axes/ticks via the `overlays` return field** (a ReactNode painted on top of the canvas). Built-in axes (via `showAxes` on the HOC) work for layouts that respect the standard scale; everything else, the recipe handles itself.
132
+ Layout signature differs by family:
133
+ - **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`.
134
+ - **Network**: `layout: (ctx) => { sceneNodes?, sceneEdges?, labels?, overlays? }`. `ctx`: `nodes`, `edges`, `dimensions`, `theme`, `resolveColor(key)`, `config`. Run external positioners (`d3-flextree`, `dagre`) then emit network scene primitives (circle/rect/arc nodes; line/bezier/curved edges).
144
135
 
145
- Convention across shipped recipes every recipe takes a consistent set of label/axis toggles in `layoutConfig`:
136
+ `semiotic/recipes` ships pure layout functions (`waffleLayout`, `calendarLayout`, `marimekkoLayout`, `bulletLayout`, `parallelCoordinatesLayout`, `flextreeLayout`, `dagreLayout`). BYO heavy deps (`d3-flextree`, `dagre`) in user code.
146
137
 
147
- | Recipe | Toggle | Default | What it draws |
148
- |---|---|---|---|
149
- | `marimekkoLayout` | `showCategoryLabels` | `true` | Category names under each variable-width bar |
150
- | `bulletLayout` | `showLabels` | `true` | Metric name to the left of each row |
151
- | `bulletLayout` | `showTicks` | `true` | Per-row value-axis ticks (each row independently scaled) |
152
- | `parallelCoordinatesLayout` | `showAxes` | `true` | Vertical axis line + field name + 5 ticks per axis |
153
- | `flextreeLayout` / `dagreLayout` | `showLabels` | `true` | Node text rendered inside each rect |
154
- | `waffleLayout` / `calendarLayout` | — | — | No chrome needed (uniform grid + cell color tells the story) |
138
+ **Chrome (labels/axes/legends): the recipe owns it.** Recipes emit own chrome via `overlays` return field (ReactNode painted on top). Built-in axes via `showAxes` on the HOC work for layouts respecting the standard scale. Recipe convention: `showXxx` boolean toggles, `xxxFormat` callbacks. Shipped recipes' toggles: marimekko `showCategoryLabels`, bullet `showLabels`+`showTicks`, parallelCoordinates `showAxes`, flextree/dagre `showLabels`, waffle/calendar none.
155
139
 
156
- Writing your own recipe: prefer `showXxx` boolean toggles for chrome opt-out, `xxxFormat` callbacks for custom number/string formatting, and reserve plot-rect padding (`labelWidth`, `labelPadding`, `axisLabelPadding`) when chrome eats space.
157
-
158
- ### Interaction (hover, brush, selection): the parent component owns it
159
-
160
- Recipes are pure functions, so they can't carry interactive state (hover, brush ranges, selection). The pattern: recipes accept a **predicate prop** (e.g. `parallelCoordinatesLayout`'s `highlightFn?: (d) => boolean`) and the parent component manages state. Wire `onObservation` (`{ type: "hover" | "hover-end" | ... }`) on `OrdinalCustomChart` / `XYCustomChart` / `NetworkCustomChart` to update parent state, then feed a derived predicate back into `layoutConfig`. Matching rows render at full opacity; non-matching dim. Highlighted rows z-order on top so neighbors don't cover them.
161
-
162
- ```tsx
163
- const [hovered, setHovered] = useState(null)
164
- <OrdinalCustomChart
165
- data={rows}
166
- layout={parallelCoordinatesLayout}
167
- layoutConfig={{
168
- fields: ["mpg", "hp", "weight"],
169
- highlightFn: hovered ? (d) => d.name === hovered : undefined,
170
- }}
171
- onObservation={(obs) => {
172
- if (obs.type === "hover") setHovered(obs.datum?.data?.name ?? obs.datum?.name)
173
- else if (obs.type === "hover-end") setHovered(null)
174
- }}
175
- />
176
- ```
177
-
178
- ### Built-in chrome works when the layout uses standard scales
179
-
180
- Pass `showAxes` on `XYCustomChart` / `OrdinalCustomChart` to get the same x/y or o/r axes the built-in HOCs render — useful for custom layouts that overlay points/lines on regular scales. Recipe-managed chrome is for cases where standard axes don't fit (variable-width bars under a band scale, per-row independent scales, etc.).
140
+ **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.
181
141
 
182
142
  **Notes:**
183
- - Coords are plot-relative (the frame translates the canvas/SVG group by `margin`). Read `ctx.dimensions.plot` for the drawing rect. Radial ordinal projection is the one exception: `plot.x = -width/2`, `plot.y = -height/2` because the canvas ctx is center-translated.
184
- - Layouts that need axis domains: pass `xExtent`/`yExtent` (XY) or `oExtent`/`rExtent` (ordinal) — those flow through scale construction *before* the layout runs.
185
- - Streaming layouts: ingest data via the chart's ref (`push`/`pushMany`); the layout re-runs on each ingest. Custom overlays update on data-change paths, NOT on per-frame animation rebuilds (intentional — would force a React re-render per frame).
186
- - Custom layouts own their colors. Always prefer `ctx.resolveColor(key)` over hardcoded literals so `ThemeProvider` / `colorScheme` flow through. `CategoryColorProvider` integration is XY-only; for cross-chart category sync on network/ordinal customLayouts, pass a matching `colorScheme` to each chart.
187
- - Tooltips: emit datum keys that match the user-visible accessor names (e.g. when `categoryAccessor: "region"` and `valueAccessor: "revenue"` are passed, put `region` and `revenue` on each rect's `datum`). The default tooltip looks them up by accessor name and the user's custom tooltip will read whatever fields they expect. Avoid underscored synthetic keys — the default tooltip filters those out.
143
+ - Coords are plot-relative (frame translates by `margin`). Read `ctx.dimensions.plot`. Radial ordinal: `plot.x = -width/2`, `plot.y = -height/2` (center-translated).
144
+ - Layouts needing axis domains: pass `xExtent`/`yExtent` (XY) or `oExtent`/`rExtent` (ordinal) — those flow through scale construction *before* the layout runs.
145
+ - Streaming layouts: ingest via ref (`push`/`pushMany`); layout re-runs on each ingest. Overlays update on data-change paths, NOT per-frame.
146
+ - 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.
147
+ - Tooltips: emit datum keys matching user-visible accessor names. Avoid underscored synthetic keys (default tooltip filters those out).
188
148
 
189
149
  ## Coordinated Views
190
150
 
191
- **LinkedCharts** — `selections`, **CategoryColorProvider** — `colors`|`categories` + `colorScheme`
192
- Chart props: `selection`, `linkedHover`, `linkedBrush`. Hooks: `useSelection`, `useLinkedHover`, `useBrushSelection`
193
- **Shared categories inside LinkedCharts → wrap in `CategoryColorProvider`.** When two or more charts encode the same categorical field (e.g. both `colorBy="region"`), wrapping in `CategoryColorProvider` gives every chart identical colors per category AND makes `LinkedCharts` render one unified legend (and suppress individual chart legends). Without it, each chart renders its own legend independently — often with mismatched colors.
194
- **Linked crosshair**: `linkedHover={{ name: "sync", mode: "x-position", xField: "time" }}`. Click-to-lock: click locks crosshair (dashed white), click/Escape unlocks.
195
- **ScatterplotMatrix**, **ChartContainer** (`title`, `subtitle`, `actions`), **ChartGrid** (`columns`, `gap`), **ContextLayout**
151
+ **LinkedCharts** — `selections`. **CategoryColorProvider** — `colors`|`categories` + `colorScheme`.
152
+ Chart props: `selection`, `linkedHover`, `linkedBrush`. Hooks: `useSelection`, `useLinkedHover`, `useBrushSelection`.
153
+ **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.
154
+ **Linked crosshair**: `linkedHover={{ name: "sync", mode: "x-position", xField: "time" }}`. Click locks crosshair (dashed white); click/Escape unlocks.
155
+ **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"`.
156
+ Also: **ScatterplotMatrix**, **ChartContainer** (`title`, `subtitle`, `actions`), **ChartGrid** (`columns`, `gap`), **ContextLayout**.
196
157
 
197
158
  ## Server-Side Rendering (`semiotic/server`)
198
159
 
@@ -201,72 +162,126 @@ HOC charts render SVG automatically in server environments. For standalone gener
201
162
  ```ts
202
163
  import { renderChart, renderToImage, renderToAnimatedGif, renderDashboard } from "semiotic/server"
203
164
 
204
- const svg = renderChart("BarChart", { data, categoryAccessor: "region", valueAccessor: "revenue", theme: "tufte", showLegend: true, showGrid: true, annotations: [...] })
205
- const png = await renderToImage("LineChart", { data, ... }, { format: "png", scale: 2 }) // requires sharp
206
- const gif = await renderToAnimatedGif("line", data, { xAccessor: "x", yAccessor: "y", theme: "dark" }, { fps: 12, transitionFrames: 4, decay: { type: "linear" } }) // requires sharp + gifenc
207
- const dashboard = renderDashboard([{ component: "BarChart", props: {...} }, { component: "PieChart", colSpan: 2, props: {...} }], { title: "Q1", theme: "dark", layout: { columns: 2 } })
165
+ const svg = renderChart("BarChart", { data, categoryAccessor, valueAccessor, theme: "tufte", showLegend, showGrid, annotations })
166
+ const png = await renderToImage("LineChart", { ... }, { format: "png", scale: 2 }) // requires sharp
167
+ const gif = await renderToAnimatedGif("line", data, { xAccessor, yAccessor, theme: "dark" }, { fps: 12, transitionFrames: 4, decay: { type: "linear" } }) // requires sharp + gifenc
168
+ const dashboard = renderDashboard([{ component: "BarChart", props }, { component: "PieChart", colSpan: 2, props }], { title, theme, layout: { columns: 2 } })
208
169
  ```
209
170
 
210
- All render functions accept `theme` (preset name or object). Theme categorical colors flow to data marks automatically. `generateFrameSVGs()` returns frame SVGs without sharp/gifenc (sync, for client preview).
171
+ All accept `theme` (preset name or object); theme categorical colors flow to data marks. `generateFrameSVGs()` returns frame SVGs without sharp/gifenc.
211
172
  AnimatedGifOptions: `fps`, `stepSize`, `windowSize`, `frameCount`, `xExtent`/`yExtent` (lock axes), `transitionFrames`, `easing`, `decay`, `loop`, `scale`.
212
- Server SVGs include `role="img"`, `<title>`, `<desc>`, grid, legend, annotations (y-threshold, x-threshold, band, label, text, category-highlight). SVG groups have `id` attributes for Figma layer naming: `data-area`, `axes`, `grid`, `annotations`, `legend`, `chart-title`.
213
-
214
- **`renderChart` required props by component:**
215
- - **Sparkline** — `data`, `xAccessor`, `yAccessor`. No axes/grid/legend/title by default. Margin defaults to 2px.
216
- - **LineChart/AreaChart** `data`, `xAccessor`, `yAccessor`. Optional: `lineBy`/`areaBy`, `colorBy`, `colorScheme`.
217
- - **StackedAreaChart** — `data`, `xAccessor`, `yAccessor`, `areaBy` (required).
218
- - **Scatterplot/BubbleChart** — `data`, `xAccessor`, `yAccessor`. BubbleChart requires `sizeBy`.
219
- - **Heatmap** — `data`, `xAccessor`, `yAccessor`, `valueAccessor`.
220
- - **BarChart** — `data`, `categoryAccessor`, `valueAccessor`.
221
- - **StackedBarChart** — `data`, `categoryAccessor`, `valueAccessor`, `stackBy` (required).
222
- - **GroupedBarChart** — `data`, `categoryAccessor`, `valueAccessor`, `groupBy` (required).
223
- - **PieChart/DonutChart** — `data`, `categoryAccessor`, `valueAccessor`.
224
- - **FunnelChart** — `data`, `stepAccessor` ("step"), `valueAccessor` ("value"). Renders with trapezoid connectors, no axes.
225
- - **GaugeChart** — `value`. Optional: `thresholds` (array of `{value, color, label}`), `min`, `max`, `sweep`, `arcWidth`, `cornerRadius`.
226
- - **SwimlaneChart** — `data`, `categoryAccessor`, `subcategoryAccessor` (required), `valueAccessor`.
227
- - **ForceDirectedGraph** — `nodes`, `edges` (both required). If deriving nodes from edge endpoints, materialize `nodes` before returning JSX/renderChart props.
228
- - **SankeyDiagram** — `edges` (required), `valueAccessor`.
229
- - **ChoroplethMap** — `areas` (GeoJSON features, pre-resolved).
230
-
231
- All components accept: `width`, `height`, `theme`, `title`, `description`, `showLegend`, `showGrid`, `background`, `annotations`, `margin`, `colorScheme`, `colorBy`, `legendPosition`. Pass additional frame-level props via `frameProps`.
173
+ Server SVGs include `role="img"`, `<title>`, `<desc>`, grid, legend, annotations. SVG groups have stable `id` attrs for Figma layer naming: `data-area`, `axes`, `grid`, `annotations`, `legend`, `chart-title`.
174
+
175
+ `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).
176
+
177
+ All components also accept: `width`, `height`, `theme`, `title`, `description`, `showLegend`, `showGrid`, `background`, `annotations`, `margin`, `colorScheme`, `colorBy`, `legendPosition`. Pass frame-level props via `frameProps`.
232
178
 
233
179
  ## Annotations
234
180
 
235
- All HOCs accept `annotations` (array). Coordinates use data field names.
181
+ All HOCs accept `annotations`. Coordinates use data field names.
236
182
 
237
- **Positioning**: `widget`, `label`, `callout`, `text`, `bracket`
183
+ **Positioning**: `widget`, `label`, `callout`, `callout-circle`, `callout-rect`, `text`, `bracket`
238
184
  **Reference lines**: `y-threshold` (`value`, `label`, `color`, `labelPosition`), `x-threshold`, `band` (`y0`, `y1`)
239
185
  **Ordinal**: `category-highlight`
240
186
  **Enclosures**: `enclose`, `rect-enclose`, `highlight`
241
187
  **Statistical**: `trend`, `envelope`, `anomaly-band`, `forecast`
242
- **Streaming anchors**: `"fixed"` | `"latest"` | `"sticky"`
188
+ **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.
189
+ **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.
190
+ **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.
191
+ **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`.
192
+ **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.
193
+ **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.
194
+ **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.
195
+ **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).
196
+ **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.
243
197
 
244
198
  ## Theming
245
199
 
246
- CSS custom properties: `--semiotic-bg`, `--semiotic-text`, `--semiotic-text-secondary`, `--semiotic-border`, `--semiotic-grid`, `--semiotic-primary`, `--semiotic-secondary`, `--semiotic-surface`, `--semiotic-success`, `--semiotic-danger`, `--semiotic-warning`, `--semiotic-error`, `--semiotic-info`, `--semiotic-focus`, `--semiotic-font-family`, `--semiotic-annotation-color`, `--semiotic-legend-font-size`, `--semiotic-title-font-size`, `--semiotic-tick-font-family`, `--semiotic-tick-font-size` (10px default — drives axis tick text), `--semiotic-axis-label-font-size` (12px default — drives axis-label text + foreignObject ticks), `--semiotic-tooltip-bg`/`text`/`radius`/`font-size`/`shadow`.
200
+ CSS custom properties: `--semiotic-{bg, text, text-secondary, border, grid, primary, secondary, surface, success, danger, warning, error, info, focus, font-family, annotation-color, legend-font-size, title-font-size, tick-font-family, tick-font-size (12px), axis-label-font-size (12px), tooltip-{bg, text, radius, font-size, shadow}}`.
247
201
 
248
202
  ```jsx
249
- <ThemeProvider theme="tufte"> {/* Named preset */}
250
- <ThemeProvider theme={{ mode: "dark", colors: { categorical: [...] } }}> {/* Merge onto dark base */}
203
+ <ThemeProvider theme="tufte"> {/* named preset */}
204
+ <ThemeProvider theme={{ mode: "dark", colors: { categorical: [...] } }}> {/* merge onto dark base */}
251
205
  ```
252
206
 
253
- **Color priority** (with `colorBy`): CategoryColorProvider/LinkedCharts category map > explicit `colorScheme` fallback > ThemeProvider `colors.categorical` > `"category10"`.
207
+ **Color priority** (with `colorBy`): CategoryColorProvider/LinkedCharts map > `colorScheme` > ThemeProvider `colors.categorical` > `"category10"`.
254
208
  Presets: `light`, `dark`, `high-contrast`, `pastels`(-dark), `bi-tool`(-dark), `italian`(-dark), `tufte`(-dark), `journalist`(-dark), `playful`(-dark), `carbon`(-dark).
255
209
  Serialization: `themeToCSS(theme, selector)`, `themeToTokens(theme)`, `resolveThemePreset(name)`.
256
210
 
257
- **Semantic status roles** (on every preset): `colors.success`, `colors.danger`, `colors.warning`, `colors.error`, `colors.info`, plus `colors.secondary` and `colors.surface`. Each emits as a `--semiotic-{role}` CSS custom property. Use for status-driven charts: `<Waterfall positiveColor="var(--semiotic-success)" negativeColor="var(--semiotic-danger)" />`, `<Swimlane color="var(--semiotic-warning)" />`, bar stroke delineation `<RealtimeHistogram stroke="var(--semiotic-border)" />`, status annotations.
211
+ **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.
258
212
 
259
- **Scoped CSS cascade override** (per-subtree, no ThemeProvider needed):
260
- ```jsx
261
- <div style={{ "--semiotic-danger": "#4b0082" }}>
262
- {/* every chart below inherits this danger color via canvas CSS-var lookup */}
263
- </div>
264
- ```
265
- Canvas scene builders read CSS variables via `getComputedStyle` on the canvas DOM ancestor, so standard CSS cascade rules apply even though rendering is canvas-based. Use CSS vars for **single-role** overrides; use a nested `ThemeProvider` for **array/scale** overrides (categorical palette, sequential/diverging scheme name).
213
+ **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).
266
214
 
267
215
  ## AI Features
268
- `onObservation`/`useChartObserver`, `toConfig`/`fromConfig`/`toURL`/`fromURL`/`copyConfig`/`configToJSX`, `validateProps(component, props)`, `diagnoseConfig(component, props)`, `exportChart(div, { format })`, `npx semiotic-ai --doctor`
269
216
 
217
+ Surface APIs: `onObservation`/`useChartObserver`, `toConfig`/`fromConfig`/`toURL`/`fromURL`/`copyConfig`/`configToJSX`, `validateProps`, `diagnoseConfig`, `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`.
218
+
219
+ ### Conversational Interrogation (`semiotic/ai`)
220
+ Headless "chat with the chart" hook. Library ships no UI — BYO chat surface.
221
+ - **`useChartInterrogation({ data, onQuery, componentName?, props?, initialAnnotations? })`** → `{ ask(query), history, summary, annotations, loading, error, reset }`
222
+ - **`onQuery: (query, context) => Promise<{ answer, annotations? }>`** — call your LLM. `context`: `{ data, summary, componentName?, props? }`.
223
+ - **`summary`**: stat summary (`rowCount`, per-field `{min, max, mean, median}` for numerics, top-k for categoricals, ISO range for dates). Available before any `ask()`.
224
+ - **`annotations`**: merged `initialAnnotations` + latest AI response. Wire to chart's `annotations` prop.
225
+ - **`summarizeData(data, options?)`**: standalone for server prompting or batch.
226
+ - **MCP tool**: `interrogateChart(component, props, query)` returns same summary + AI-facing instructions.
227
+
228
+ ### Chart Capability Layer (`semiotic/ai`)
229
+ Heuristic chart-suggestion engine — no LLM required. Charts ship capability descriptors next to TSX files; engine ranks against a profiled dataset by intent.
230
+ - **`profileData(data, { rawInput?, seriesField? })`** → `ChartDataProfile`: per-role candidate fields (x/y/series/category/size/time), distinct counts, monotonicity, structure detection.
231
+ - **`suggestCharts(data, { intent?, allow?, deny?, maxResults?, includeVariants?, minScore?, audience? })`** → ranked `Suggestion[]` with `{ component, family, importPath, variant?, score, intentScores, rubric, reasons, caveats, props }`. `props` is spreadable directly. **Receivability**: set `audience.receptionModality` (`visual` default | `screen-reader` | `sonified` | `agent`); a non-visual channel audits each candidate and down-ranks charts the audience can't receive there (8-slice pie for a screen reader), adding the audit's findings to `caveats[]` (familiarity and receivability are separate axes). `accessibilityCaveats(auditResult)` distils any audit into the same caveat strings.
232
+ - **`scoreChart(component, data, { intent?, variantKey? })`** → evaluate a specific chart for a dataset.
233
+ - **`useChartSuggestions(data, options)`** → memoized React hook returning `{ suggestions, profile }`.
234
+ - **`registerChartCapability(capability)`** / **`unregisterChartCapability(name)`** — runtime registration for custom charts.
235
+ - **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`.
236
+ - **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)`.
237
+ - **Variants** encode that settings change what a chart is good for (e.g. StackedAreaChart's `streamgraph` variant boosts trend, penalizes part-to-whole).
238
+ - **Interrogation tie-in**: pass `includeSuggestions: true` to `useChartInterrogation` and the ranked list lands in `context.suggestions` for the LLM.
239
+ - **MCP tool**: `suggestCharts(data, intent?)`.
240
+
241
+ ### Conversation-arc telemetry (`semiotic/ai`)
242
+ 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.
243
+ - **`enableConversationArc({ capacity?, sessionId? })`** / **`disableConversationArc()`**. Bounded ring buffer (default 1000 events).
244
+ - **`getConversationArcStore()`** → `{ enabled, sessionId, capacity, record, flush, getEvents, subscribe, clear, reset }`. `getEvents()` returns referentially stable snapshot.
245
+ - **`useConversationArc({ enableOnMount?, disableOnUnmount?, capacity?, sessionId? })`** → `{ history, summary, enabled, sessionId, record, clear }`. Uses `useSyncExternalStore`.
246
+ - **`summarizeArc(events)`** → pure reducer. Server/replay safe.
247
+ - **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.
248
+ - **`recordAudienceChange(audience, previous?, { arcId?, meta? }?)`** — sugar for `audience-set`. Call from audience-picker `onChange`.
249
+ - **Events**: `ConversationArcEvent` discriminated union. Each variant carries its own payload.
250
+ - **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.
251
+
252
+ ### Annotation provenance + lifecycle (`semiotic/ai`, types re-exported from `semiotic`)
253
+ Two optional blocks attach to any annotation — existing arrays keep working unchanged.
254
+ - **`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.
255
+ - **`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.
256
+ - **`withProvenance(annotation, { provenance?, lifecycle? })`** → pure, SSR-safe.
257
+ - **`Annotated<T>`** type: `T & { provenance?, lifecycle? }`.
258
+ - **`computeAnnotationFreshness(annotations, { now?, dataExtent?, thresholds? })`** → populates `lifecycle.freshness`. `now` defaults to `dataExtent` max, then `Date.now()`. Default thresholds 1×/1.5×/3× TTL.
259
+ - **`annotationFreshnessFor(annotation, nowMs, thresholds?)`** → classifies a single annotation. Explicit `lifecycle.freshness` wins.
260
+ - **`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.
261
+ - **`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.
262
+ - **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.
263
+
264
+ ### Temporal lifecycle (shared `semiotic/realtime` + `semiotic/ai`)
265
+ Three systems answer "how does this look as it ages?" on three different time axes — not interchangeable.
266
+
267
+ | Policy | Lives in | Time axis | Output | Scope |
268
+ |---|---|---|---|---|
269
+ | `DecayConfig` | `semiotic/realtime` | buffer position | continuous opacity ramp | per-datum |
270
+ | `StalenessConfig` | `semiotic/realtime` | wall-clock idle | binary live/stale (+ optional badge) | chart-wide |
271
+ | Annotation freshness | `semiotic/ai` | `createdAt` + `ttlHint` | 4 named bands (opacity + dashing + expired filter) | per-annotation |
272
+
273
+ 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`).
274
+
275
+ **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`.
276
+
277
+ ### Variant discovery (`semiotic/ai`)
278
+ 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.
279
+ - **`VariantProposal`**: `{ id, baseComponent, label?, intentDeltas?, rubricDeltas?, buildProps?, rationale?, source: "manual"|"heuristic"|"model", variantKey?, tags? }`.
280
+ - **`VariantScore`**: `{ proposalId, fit (0–5), novelty (0–1), risk (0–1), reasons }`. Mixes with `suggestCharts` composite scores.
281
+ - **`proposeVariant(component, capability, context)`** → `VariantProposal[]`. Context accepts `{ profile, audience?, intent?, existingVariants? }`.
282
+ - **`evaluateVariantProposal(proposal, profile, audience?, { intent?, baselineComponent? }?)`** → `VariantScore`.
283
+ - **MCP tool**: `proposeChartVariants(component, props?, data?, intent?, audience?)` ranks proposals and returns ready-to-use props.
284
+ - **`registerVariantDiscovery(fn)`** → registers proposer. `proposeVariant` dispatches through all and dedupes by `proposal.id`. Returns unregister. Inspect: `getRegisteredVariantDiscovery()` / `clearVariantDiscovery()`.
270
285
 
271
286
  ## AI Behavior Contracts
272
287
 
@@ -291,32 +306,38 @@ These rules are generated from `ai/behaviorContracts.cjs` and are consumed by `s
291
306
  <!-- semiotic-behavior-contracts:end -->
292
307
 
293
308
  ## Accessibility
309
+ `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()`.
310
+
311
+ **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.
312
+
313
+ **Chart descriptions**: `describeChart(component, props, { levels?, locale?, capability?, audience? })` (from `semiotic/ai` or `semiotic/utils`) generates a layered natural-language description (Lundgard L1 encoding / L2 statistics / L3 trend) → `{ text, levels: {l1?,l2?,l3?,l4?}, annotations? }`. Richest for XY/bar/part-to-whole/distribution; degrades to L1 for network/hierarchy/geo/value. **Annotations**: when `props.annotations` is present, the result carries an `annotations` sentence ("The author has marked 2 features…") and it *leads* `text` ahead of L1–L3 — an author-placed note is intent in its purest form. Provenance-aware: an `authorKind`/`source` of `agent`/`ai`/`watcher` qualifies it ("an AI-suggested callout"). Absent when no annotations, so un-annotated callers are unchanged. **L4 (intent)**: pass a `capability` (a chart's descriptor or a resolved `{ family, intentScores }`) and `describeChart` emits the illocutionary *communicative-act* sentence ("This is an alerting chart; the peak at March is the point to investigate") — opt-in, default output stays L1–L3. Helpers: `resolveCommunicativeAct(component, capability)`, `communicativeActForIntent(intent)`, type `CommunicativeAct`. **Full-accessibility chrome (title, caption, description, navigation, data download) is the opt-in `ChartContainer` layer — not baked into the bare chart.** `<ChartContainer describe chartConfig={{component, props}}>` renders an sr-only L1–L3 description (`describe={{ visible:true }}` to show it, `{ levels }` for verbosity). The audit's `assistive.features-described` passes when `describe` is on.
294
314
 
295
- `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. Auto-detects `prefers-reduced-motion` and `forced-colors`. Hooks: `useReducedMotion()`, `useHighContrast()`.
315
+ **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.
316
+
317
+ **Structured navigation** (Olli/Data-Navigator model): `buildNavigationTree(component, props, { maxLeaves?, locale? })` (from `semiotic/ai` or `semiotic/utils`) → a `NavTreeNode` tree (chart → axis/series → datum), labels composed via describeChart. When `props.annotations` is present it appends an **Annotations** branch (`role: "annotation"`) so a reader encounters author notes during traversal — provenance + M7 editorial `status` surfaced inline, retracted and superseded notes skipped, added on every family (incl. root-only network/geo/hierarchy). `describeChart` also leads its text with an annotation sentence when annotations are present. `AccessibleNavTree` (from `semiotic` or `semiotic/ai`) renders it as a WAI-ARIA `tree` widget (arrows/Enter/Home/End, roving tabindex, `onActiveChange`, controlled `activeId` auto-expands to the node, `chartId` for telemetry), sr-only by default. With the conversation-arc store enabled it emits `nav-node-focused`/`nav-branch-expanded` reception events on genuine traversal (not on canvas-driven `activeId` changes). `<ChartContainer navigable chartConfig={...}>` mounts it (`navigable={{ visible?, maxLeaves? }}`). Audit's `compromising.navigable-structure` passes when `navigable` is on (incl. hierarchy charts). Audit options: `auditAccessibility(component, props, { inChartContainer?, describe?, navigable? })`.
318
+
319
+ **Bidirectional sync**: `useNavigationSync({ tree, chartId?, matchFields?, selectionName?, annotations? })` (from `semiotic` or `semiotic/ai`) → `{ activeId, onActiveChange, selection, annotatedIds, focusAnnotation }`. Tree→canvas highlights the matching mark (field-value selection); canvas→tree maps the hovered/clicked datum back to its leaf. Rides the module-global selection + observation stores — **no provider needed**: give the chart `chartId` + `selection={sync.selection}`, give `AccessibleNavTree` `activeId`/`onActiveChange`. **Annotation anchors**: pass the chart's `annotations` and an anchored annotation (carrying the datum's `matchFields`) resolves to a nav leaf — `annotatedIds` are the leaf ids with a note; `focusAnnotation(annotation | index)` jumps the tree + canvas to the anchored point so a non-visual reader can reach an AI's anchored note.
296
320
 
297
321
  ## Usage Notes
298
322
 
323
+ - **Push API**: Omit `data`. `data={[]}` clears on every render.
299
324
  - **Tooltip datum shape**: HOC tooltips get raw data. Frame `tooltipContent` gets wrapped — use `d.data`.
325
+ - **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`.
326
+ - **`tooltip="multi"`**: shows all series at hovered X for LineChart, AreaChart, StackedAreaChart. Custom fn receives `datum.allSeries`.
300
327
  - **Legend**: "bottom" expands margin ~80px. MultiAxisLineChart: use `legendPosition="bottom"`.
301
- - **Log scale**: Domain min clamped to 1e-6.
302
- - **barPadding**: Pixel value (40/60 default). Reduce for small charts.
303
- - **sort** (BarChart/StackedBarChart/GroupedBarChart/DotPlot): `false` preserves insertion order; `"auto"` = insertion-order while streaming, value-desc on static (DotPlot default — opt-in on others, avoids category shuffling under the push API). StackedBar/GroupedBar default to `false`; the underlying frame value-sorts when `oSort` is undefined, so always pass `sort` explicitly if order matters.
304
- - **Tooltip format cascade**: `valueFormat`/`xFormat`/`yFormat` flow to the default tooltip automatically, so axis and tooltip read identically. A custom `tooltip` prop fully overrides — re-pass via `Tooltip({format})` / `MultiLineTooltip({fields:[{format}]})`. Bespoke-tooltip charts (Histogram, FunnelChart, LikertChart, GaugeChart) don't participate; customize via `tooltip`.
305
- - **Horizontal bars**: Need wider left margin: `margin={{ left: 120 }}`.
306
- - **Push API**: Omit `data` entirely. `data={[]}` clears on every render.
307
- - **frameProps style functions**: Bypass HOC color resolution — use `colorBy` prop instead.
308
- - **Geo imports**: Always `semiotic/geo`, never `semiotic`, to avoid d3-geo in non-geo bundles.
309
- - **fillArea**: `fillArea={["seriesA"]}` fills named series only. Names must match `lineBy`/`colorBy` keys.
310
- - **hoverHighlight**: Requires `colorBy` as a string field.
311
- - **tooltip="multi"**: Shows all series at hovered X for LineChart, AreaChart, and StackedAreaChart. Custom fn receives `datum.allSeries`.
312
- - **Axis config**: `frameProps.axes: [{ orient, includeMax, autoRotate, gridStyle, landmarkTicks, tickAnchor }]`. `tickAnchor: "edges"` flips the first tick's `text-anchor` to `start` and the last to `end` on horizontal axes (and `dominant-baseline` to `hanging`/`auto` on vertical axes) so edge labels don't overflow the plot. Pairs naturally with `axisExtent: "exact"`.
313
- - **Targeting individual axes from CSS**: every axis renders as its own `<g class="semiotic-axis semiotic-axis-{bottom|left|right|top}" data-orient="…">`. Style with `[data-orient="left"] text { font-size: 14px }` or via the class names no `!important` needed because the CSS-var defaults are set inline via `var(--semiotic-tick-font-size, …)`, so cascade overrides win cleanly. Tick text carries `class="semiotic-axis-tick"`; labels carry `class="semiotic-axis-label"`; titles carry `class="semiotic-chart-title"`.
314
- - **xScaleType: "time"**: Creates `scaleTime`. Required for landmark ticks with timestamps.
315
- - **scalePadding**: Pixel inset on scale ranges. Pass via `frameProps={{ scalePadding: 12 }}`.
316
- - **categoryFormat/xFormat/yFormat**: Can return ReactNode (renders in `<foreignObject>`).
317
- - **Tick deduplication**: Adjacent identical labels auto-removed.
318
- - **Composing overlays**: XY/Ordinal charts paint `--semiotic-bg` across the canvas; stack with `frameProps={{ background: "transparent" }}` on the overlay. Network/Geo don't paint bg by default.
328
+ - **Horizontal bars**: need wider left margin (`margin={{ left: 120 }}`).
329
+ - **Log scale**: domain min clamped to 1e-6. **`xScaleType: "time"`**: creates `scaleTime`; required for landmark ticks with timestamps.
330
+ - **`barPadding`**: pixel value (40/60 default). Reduce for small charts.
331
+ - **`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.
332
+ - **`fillArea`**: `fillArea={["seriesA"]}` fills named series only names must match `lineBy`/`colorBy` keys.
333
+ - **`hoverHighlight`**: requires `colorBy` as a string field.
334
+ - **`frameProps` style functions**: bypass HOC color resolution — use `colorBy` instead.
335
+ - **Geo imports**: always `semiotic/geo`, never `semiotic`.
336
+ - **Axis config**: `frameProps.axes: [{ orient, includeMax, autoRotate, gridStyle, landmarkTicks, tickAnchor }]`. `tickAnchor: "edges"` flips first tick's `text-anchor` to `start`, last to `end` (and `dominant-baseline` on vertical axes) so edge labels don't overflow. Pairs with `axisExtent: "exact"`.
337
+ - **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"`.
338
+ - **`scalePadding`**: pixel inset on scale ranges (via `frameProps={{ scalePadding: 12 }}`).
339
+ - **`categoryFormat`/`xFormat`/`yFormat`**: can return ReactNode (renders in `<foreignObject>`). Tick deduplication: adjacent identical labels auto-removed.
340
+ - **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.
319
341
 
320
342
  ## Performance
321
-
322
343
  Prefer string accessors (`xAccessor="value"`) — always referentially stable. Memoize function accessors with `useCallback`.