semiotic 3.1.2 → 3.2.1

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 (133) hide show
  1. package/CLAUDE.md +173 -213
  2. package/LICENSE +197 -10
  3. package/README.md +12 -11
  4. package/ai/dist/componentRegistry.js +6 -0
  5. package/ai/dist/mcp-server.js +115 -5
  6. package/ai/examples.md +184 -0
  7. package/ai/schema.json +4140 -888
  8. package/ai/system-prompt.md +36 -1
  9. package/dist/components/ChartContainer.d.ts +2 -0
  10. package/dist/components/DataSummaryContext.d.ts +12 -0
  11. package/dist/components/ThemeProvider.d.ts +5 -3
  12. package/dist/components/Tooltip/FlippingTooltip.d.ts +34 -0
  13. package/dist/components/charts/geo/ChoroplethMap.d.ts +1 -1
  14. package/dist/components/charts/index.d.ts +12 -1
  15. package/dist/components/charts/ordinal/BarChart.d.ts +4 -1
  16. package/dist/components/charts/ordinal/BoxPlot.d.ts +4 -1
  17. package/dist/components/charts/ordinal/DonutChart.d.ts +1 -1
  18. package/dist/components/charts/ordinal/DotPlot.d.ts +4 -1
  19. package/dist/components/charts/ordinal/FunnelChart.d.ts +57 -0
  20. package/dist/components/charts/ordinal/GroupedBarChart.d.ts +4 -1
  21. package/dist/components/charts/ordinal/Histogram.d.ts +17 -2
  22. package/dist/components/charts/ordinal/LikertChart.d.ts +94 -0
  23. package/dist/components/charts/ordinal/PieChart.d.ts +1 -1
  24. package/dist/components/charts/ordinal/RidgelinePlot.d.ts +12 -7
  25. package/dist/components/charts/ordinal/StackedBarChart.d.ts +4 -1
  26. package/dist/components/charts/ordinal/SwarmPlot.d.ts +15 -1
  27. package/dist/components/charts/ordinal/SwimlaneChart.d.ts +65 -0
  28. package/dist/components/charts/ordinal/ViolinPlot.d.ts +17 -2
  29. package/dist/components/charts/realtime/RealtimeHistogram.d.ts +20 -0
  30. package/dist/components/charts/shared/annotationResolvers.d.ts +28 -0
  31. package/dist/components/charts/shared/colorManipulation.d.ts +15 -0
  32. package/dist/components/charts/shared/formatUtils.d.ts +28 -0
  33. package/dist/components/charts/shared/hatchPattern.d.ts +35 -0
  34. package/dist/components/charts/shared/hooks.d.ts +39 -3
  35. package/dist/components/charts/shared/legendUtils.d.ts +2 -1
  36. package/dist/components/charts/shared/selectionUtils.d.ts +16 -1
  37. package/dist/components/charts/shared/statisticalOverlays.d.ts +49 -5
  38. package/dist/components/charts/shared/statsTooltip.d.ts +11 -0
  39. package/dist/components/charts/shared/types.d.ts +26 -2
  40. package/dist/components/charts/shared/useChartSetup.d.ts +12 -2
  41. package/dist/components/charts/shared/useLikertAggregation.d.ts +51 -0
  42. package/dist/components/charts/shared/useOrdinalBrush.d.ts +28 -0
  43. package/dist/components/charts/shared/useOrdinalStreaming.d.ts +54 -0
  44. package/dist/components/charts/shared/useStreamingLegend.d.ts +2 -2
  45. package/dist/components/charts/shared/validateProps.d.ts +2 -2
  46. package/dist/components/charts/shared/validationMap.d.ts +12 -0
  47. package/dist/components/charts/xy/AreaChart.d.ts +11 -0
  48. package/dist/components/charts/xy/Heatmap.d.ts +1 -1
  49. package/dist/components/charts/xy/MinimapChart.d.ts +1 -1
  50. package/dist/components/charts/xy/MultiAxisLineChart.d.ts +71 -0
  51. package/dist/components/charts/xy/StackedAreaChart.d.ts +11 -0
  52. package/dist/components/realtime/types.d.ts +6 -0
  53. package/dist/components/semiotic-ai.d.ts +4 -0
  54. package/dist/components/semiotic-ordinal.d.ts +5 -0
  55. package/dist/components/semiotic-themes.d.ts +80 -0
  56. package/dist/components/semiotic-utils.d.ts +30 -0
  57. package/dist/components/semiotic-xy.d.ts +1 -0
  58. package/dist/components/semiotic.d.ts +11 -5
  59. package/dist/components/store/LinkedCrosshairStore.d.ts +11 -0
  60. package/dist/components/store/ThemeStore.d.ts +22 -2
  61. package/dist/components/store/useSelection.d.ts +1 -0
  62. package/dist/components/stream/AccessibleDataTable.d.ts +28 -6
  63. package/dist/components/stream/FocusRing.d.ts +33 -0
  64. package/dist/components/stream/OrdinalBrushOverlay.d.ts +43 -0
  65. package/dist/components/stream/OrdinalPipelineStore.d.ts +16 -0
  66. package/dist/components/stream/OrdinalSVGOverlay.d.ts +2 -1
  67. package/dist/components/stream/PipelineStore.d.ts +7 -47
  68. package/dist/components/stream/SVGOverlay.d.ts +9 -3
  69. package/dist/components/stream/SceneGraph.d.ts +6 -1
  70. package/dist/components/stream/XYBrushOverlay.d.ts +47 -0
  71. package/dist/components/stream/accessorUtils.d.ts +14 -0
  72. package/dist/components/stream/geoTypes.d.ts +5 -1
  73. package/dist/components/stream/keyboardNav.d.ts +85 -9
  74. package/dist/components/stream/layouts/hierarchySceneBuilders.d.ts +35 -0
  75. package/dist/components/stream/layouts/hierarchyUtils.d.ts +25 -0
  76. package/dist/components/stream/networkTypes.d.ts +7 -1
  77. package/dist/components/stream/ordinalSceneBuilders/barFunnelScene.d.ts +27 -0
  78. package/dist/components/stream/ordinalSceneBuilders/funnelScene.d.ts +26 -0
  79. package/dist/components/stream/ordinalSceneBuilders/swimlaneScene.d.ts +12 -0
  80. package/dist/components/stream/ordinalTypes.d.ts +30 -4
  81. package/dist/components/stream/pipelineDecay.d.ts +20 -0
  82. package/dist/components/stream/pipelinePulse.d.ts +24 -0
  83. package/dist/components/stream/pipelineTransitions.d.ts +59 -0
  84. package/dist/components/stream/renderers/barFunnelCanvasRenderer.d.ts +12 -0
  85. package/dist/components/stream/renderers/pointCanvasRenderer.d.ts +2 -1
  86. package/dist/components/stream/renderers/trapezoidCanvasRenderer.d.ts +15 -0
  87. package/dist/components/stream/sceneUtils.d.ts +10 -0
  88. package/dist/components/stream/types.d.ts +29 -4
  89. package/dist/components/stream/useMediaPreferences.d.ts +11 -0
  90. package/dist/components/stream/xySceneBuilders/areaScene.d.ts +13 -0
  91. package/dist/components/stream/xySceneBuilders/barScene.d.ts +18 -0
  92. package/dist/components/stream/xySceneBuilders/boundsScene.d.ts +8 -0
  93. package/dist/components/stream/xySceneBuilders/candlestickScene.d.ts +10 -0
  94. package/dist/components/stream/xySceneBuilders/emitPointNodes.d.ts +13 -0
  95. package/dist/components/stream/xySceneBuilders/heatmapScene.d.ts +3 -0
  96. package/dist/components/stream/xySceneBuilders/lineScene.d.ts +12 -0
  97. package/dist/components/stream/xySceneBuilders/pointScene.d.ts +12 -0
  98. package/dist/components/stream/xySceneBuilders/swarmScene.d.ts +10 -0
  99. package/dist/components/stream/xySceneBuilders/types.d.ts +93 -0
  100. package/dist/components/stream/xySceneBuilders/waterfallScene.d.ts +12 -0
  101. package/dist/geo.min.js +1 -1
  102. package/dist/geo.module.min.js +1 -1
  103. package/dist/network.min.js +1 -1
  104. package/dist/network.module.min.js +1 -1
  105. package/dist/ordinal.min.js +1 -1
  106. package/dist/ordinal.module.min.js +1 -1
  107. package/dist/realtime.min.js +1 -1
  108. package/dist/realtime.module.min.js +1 -1
  109. package/dist/semiotic-ai-statisticalOverlays-C2PPlmXv.js +1 -0
  110. package/dist/semiotic-ai.d.ts +4 -0
  111. package/dist/semiotic-ai.min.js +1 -1
  112. package/dist/semiotic-ai.module.min.js +1 -1
  113. package/dist/semiotic-ordinal.d.ts +5 -0
  114. package/dist/semiotic-statisticalOverlays-DGX_WWc5.js +1 -0
  115. package/dist/semiotic-themes.d.ts +80 -0
  116. package/dist/semiotic-themes.min.js +1 -0
  117. package/dist/semiotic-themes.module.min.js +1 -0
  118. package/dist/semiotic-utils.d.ts +30 -0
  119. package/dist/semiotic-utils.min.js +1 -0
  120. package/dist/semiotic-utils.module.min.js +1 -0
  121. package/dist/semiotic-xy.d.ts +1 -0
  122. package/dist/semiotic.d.ts +11 -5
  123. package/dist/semiotic.min.js +1 -1
  124. package/dist/semiotic.module.min.js +1 -1
  125. package/dist/server.min.js +1 -1
  126. package/dist/server.module.min.js +1 -1
  127. package/dist/xy-statisticalOverlays-C2PPlmXv.js +1 -0
  128. package/dist/xy.min.js +1 -1
  129. package/dist/xy.module.min.js +1 -1
  130. package/package.json +38 -12
  131. package/dist/semiotic-ai-statisticalOverlays-C1f7TYyD.js +0 -1
  132. package/dist/semiotic-statisticalOverlays-C1f7TYyD.js +0 -1
  133. package/dist/xy-statisticalOverlays-C1f7TYyD.js +0 -1
package/CLAUDE.md CHANGED
@@ -2,49 +2,34 @@
2
2
 
3
3
  ## Quick Start
4
4
  - Install: `npm install semiotic`
5
- - Import: `semiotic`, `semiotic/xy`, `semiotic/ordinal`, `semiotic/network`, `semiotic/geo`, `semiotic/realtime`, `semiotic/ai`, `semiotic/data`, `semiotic/server`
5
+ - Import: `semiotic`, `semiotic/xy`, `semiotic/ordinal`, `semiotic/network`, `semiotic/geo`, `semiotic/realtime`, `semiotic/ai`, `semiotic/data`, `semiotic/server`, `semiotic/themes`, `semiotic/utils`
6
6
  - CLI: `npx semiotic-ai [--schema|--compact|--examples|--doctor]`
7
7
  - MCP: `npx semiotic-mcp`
8
- - Every HOC has a built-in error boundary (never blanks the page) and dev-mode validation warnings
8
+ - Every HOC has a built-in error boundary and dev-mode validation warnings
9
9
 
10
10
  ## Architecture
11
11
  - **HOC Charts**: Simple props, sensible defaults. **Stream Frames**: Full control.
12
- - **Always use HOC charts** (`ForceDirectedGraph`, `SankeyDiagram`, `LineChart`, `RealtimeLineChart`, `ChoroplethMap`, etc.) unless you need sophisticated control they don't expose. Stream Frames (`StreamNetworkFrame`, `StreamXYFrame`, `StreamOrdinalFrame`, `StreamGeoFrame`) are low-level escape hatches — they accept raw `RealtimeNode`/`RealtimeEdge` wrappers in callbacks, not your data objects directly.
12
+ - **Always use HOC charts** unless you need control they don't expose. Stream Frames (`StreamNetworkFrame`, `StreamXYFrame`, `StreamOrdinalFrame`, `StreamGeoFrame`) are low-level — they pass `RealtimeNode`/`RealtimeEdge` wrappers in callbacks, not your data.
13
13
  - Every HOC accepts `frameProps` to pass through. TypeScript `strict: true`.
14
14
 
15
15
  ## Common Props (all HOCs)
16
- `title`, `width` (600), `height` (400), `responsiveWidth`, `responsiveHeight`, `margin`, `className`, `enableHover` (true), `tooltip` (boolean | `(datum) => ReactNode` | config object), `showLegend`, `showGrid` (false), `frameProps`, `onObservation` (callback, see below), `chartId`, `loading` (false), `emptyContent`, `legendInteraction` ("none"|"highlight"|"isolate"), `legendPosition` ("right"|"left"|"top"|"bottom", default "right"), `emphasis` ("primary"|"secondary")
16
+ `title`, `description` (overrides aria-label), `summary` (sr-only note), `width` (600), `height` (400), `responsiveWidth`, `responsiveHeight`, `margin`, `className`, `color` (uniform fill — overrides theme/colorScheme), `enableHover` (true), `tooltip` (boolean | `(datum) => ReactNode` | `{ fields?, title?, format?, style? }`), `showLegend`, `showGrid` (false), `frameProps`, `onObservation`, `onClick`, `chartId`, `loading` (false), `emptyContent`, `legendInteraction` ("none"|"highlight"|"isolate"), `legendPosition` ("right"|"left"|"top"|"bottom"), `emphasis` ("primary"|"secondary"), `annotations` (array), `accessibleTable` (true)
17
17
 
18
- ### tooltip
19
- `tooltip` accepts: `true` (default tooltip), `false` (disabled), a **function** `(datum: Record<string, any>) => ReactNode`, or a config `{ fields?: string[], title?: accessor, format?: fn, style?: CSSProperties }`. The function form receives your raw data object directly.
18
+ `onClick` receives `(datum, { x, y })` — the original datum and pixel coordinates. Works on lines, bars, areas, pie slices, nodes, and geo features.
20
19
 
21
- ### onObservation
22
- `onObservation` receives a `ChartObservation` with `type` and event-specific fields:
23
- - **hover**: `{ type: "hover", datum: <your data>, x, y, timestamp, chartType, chartId }`
24
- - **hover-end**: `{ type: "hover-end", timestamp, chartType, chartId }`
25
- - **click**: `{ type: "click", datum: <your data>, x, y, timestamp, chartType, chartId }`
26
- - **brush**: `{ type: "brush", extent: { x: [min, max], y: [min, max] }, timestamp, chartType }`
27
- - **selection**: `{ type: "selection", selection: { name, fields }, timestamp, chartType }`
28
-
29
- The `datum` field contains your original data object (not a wrapper).
20
+ `onObservation` receives `{ type: "hover"|"hover-end"|"click"|"brush"|"selection", datum?, x?, y?, timestamp, chartType, chartId }`. The `datum` is your original data object.
30
21
 
31
22
  ## XY Charts (`semiotic/xy`)
32
23
 
33
- **LineChart** — `data`, `xAccessor` ("x"), `yAccessor` ("y"), `lineBy`, `lineDataAccessor` ("coordinates"), `colorBy`, `colorScheme`, `curve`, `lineWidth` (2), `showPoints`, `pointRadius` (3), `fillArea`, `areaOpacity` (0.3), `anomaly` (AnomalyConfig), `forecast` (ForecastConfig), `directLabel` (boolean|{position,fontSize}), `gapStrategy` ("break"|"interpolate"|"zero"), `xScaleType` ("linear"|"log"), `yScaleType` ("linear"|"log")
34
-
24
+ **LineChart** — `data`, `xAccessor` ("x"), `yAccessor` ("y"), `lineBy`, `lineDataAccessor` ("coordinates"), `colorBy`, `colorScheme`, `curve`, `lineWidth` (2), `showPoints`, `pointRadius` (3), `fillArea`, `areaOpacity` (0.3), `anomaly`, `forecast`, `directLabel`, `gapStrategy` ("break"|"interpolate"|"zero"), `xScaleType`/`yScaleType` ("linear"|"log")
35
25
  **AreaChart** — LineChart props + `areaBy`, `y0Accessor` (band/ribbon), `gradientFill` (boolean|{topOpacity,bottomOpacity}), `areaOpacity` (0.7), `showLine` (true)
36
-
37
- **StackedAreaChart** — flat array data + `areaBy` (required, groups into stacked areas), `colorBy`, `normalize` (false). Do NOT use `lineBy` or `lineDataAccessor` — those are LineChart props.
38
-
26
+ **StackedAreaChart** — flat array + `areaBy` (required), `colorBy`, `normalize`. Do NOT use `lineBy` or `lineDataAccessor`.
39
27
  **Scatterplot** — `data`, `xAccessor`, `yAccessor`, `colorBy`, `sizeBy`, `sizeRange`, `pointRadius` (5), `pointOpacity` (0.8), `marginalGraphics`
40
-
41
28
  **BubbleChart** — Scatterplot + `sizeBy` (required), `sizeRange` ([5,40]), `bubbleOpacity` (0.6)
42
-
43
- **ConnectedScatterplot** — `data`, `xAccessor`, `yAccessor`, `orderAccessor` (number|Date field for sequencing), `pointRadius` (4). Viridis colored start→end, line width = point radius, white halo under lines when <100 points.
44
-
45
- **QuadrantChart** — Scatterplot divided into four labeled, colored quadrants. `data`, `xAccessor`, `yAccessor`, `quadrants` (required: `{ topRight, topLeft, bottomRight, bottomLeft }` each with `label`, `color`, optional `opacity`), `xCenter` (vertical center line in data units), `yCenter` (horizontal center line), `centerlineStyle` (`{ stroke, strokeWidth, strokeDasharray }`), `showQuadrantLabels` (true), `quadrantLabelSize` (12), `colorBy`, `sizeBy`, `sizeRange`, `pointRadius` (5), `pointOpacity` (0.8). Supports push API. Quadrant fills and labels drawn via `canvasPreRenderers`.
46
-
47
- **Heatmap** — `data`, `xAccessor`, `yAccessor`, `valueAccessor`, `colorScheme` ("blues"|"reds"|"greens"|"viridis" or custom), `showValues`, `cellBorderColor`. Accessors can be string field names (including string/categorical fields) or functions.
29
+ **ConnectedScatterplot** — `data`, `xAccessor`, `yAccessor`, `orderAccessor` (sequencing field), `pointRadius` (4)
30
+ **QuadrantChart** — Scatterplot + `quadrants` (required: `{ topRight, topLeft, bottomRight, bottomLeft }` each `{ label, color, opacity? }`), `xCenter`, `yCenter`, `centerlineStyle`, `showQuadrantLabels` (true). Supports push API.
31
+ **MultiAxisLineChart** — Dual Y-axis. `data`, `xAccessor` ("x"), `series` (required: array of `{ yAccessor, label?, color?, format?, extent? }`), `colorScheme`, `curve` ("monotoneX"), `lineWidth` (2). Data unitized to [0,1] internally; left axis=series[0], right axis=series[1] in original units. For push API, provide `series[].extent` for stable unitization. Falls back to standard multi-line if not exactly 2 series.
32
+ **Heatmap** — `data`, `xAccessor`, `yAccessor`, `valueAccessor`, `colorScheme` ("blues"|"reds"|"greens"|"viridis"), `showValues`, `cellBorderColor`. Supports string/categorical axes.
48
33
 
49
34
  ## Ordinal Charts (`semiotic/ordinal`)
50
35
 
@@ -53,284 +38,259 @@ The `datum` field contains your original data object (not a wrapper).
53
38
  **GroupedBarChart** — + `groupBy` (required), `barPadding` (60)
54
39
  **SwarmPlot** — `data`, `categoryAccessor`, `valueAccessor`, `colorBy`, `sizeBy`, `pointRadius`, `pointOpacity`
55
40
  **BoxPlot** — + `showOutliers`, `outlierRadius`
56
- **Histogram** — + `bins` (25), `relative`. Always horizontal. `categoryAccessor` is optional (defaults to `"category"`) — for a single-group histogram, either omit it or ensure your data has a `category` field with a single value.
41
+ **Histogram** — + `bins` (25), `relative`. Always horizontal. `categoryAccessor` optional (defaults to `"category"`).
57
42
  **ViolinPlot** — + `bins`, `curve`, `showIQR`
43
+ **RidgelinePlot** — + `bins`, `amplitude` (1.5, unitless multiplier of lane width)
58
44
  **DotPlot** — + `sort` (true), `dotRadius`, `showGrid` default true
59
- **PieChart** — `data`, `categoryAccessor`, `valueAccessor`, `colorBy`, `startAngle`, `slicePadding`
60
- **DonutChart** — PieChart + `innerRadius` (60), `centerContent` (ReactNode — any React element, e.g. `<div>50%</div>`)
45
+ **PieChart** — `data`, `categoryAccessor`, `valueAccessor`, `colorBy`, `startAngle`
46
+ **DonutChart** — PieChart + `innerRadius` (60), `centerContent` (ReactNode)
47
+ **FunnelChart** — `data`, `stepAccessor` ("step"), `valueAccessor` ("value"), `categoryAccessor` (optional), `colorBy`, `connectorOpacity` (0.3), `orientation` ("horizontal"|"vertical"). Horizontal: centered bars with trapezoid connectors. Vertical: bars with diagonal hatch for dropoff. Multi-category: `categoryAccessor="channel"` mirrors (horizontal) or groups (vertical).
48
+ **SwimlaneChart** — `data`, `categoryAccessor` ("category"), `subcategoryAccessor` (required), `valueAccessor` ("value"), `colorBy` (defaults to subcategoryAccessor), `colorScheme`, `orientation` ("horizontal"|"vertical"), `barPadding` (40). Renders categorical lanes with items stacked sequentially — unlike StackedBarChart, the same subcategory can appear multiple times in the same lane. Items stack left-to-right (horizontal) or bottom-to-top (vertical) in data order. Wraps StreamOrdinalFrame with `chartType="swimlane"`. Supports push API for streaming.
49
+
50
+ **LikertChart** — `data`, `categoryAccessor` ("question"), `valueAccessor` ("score", raw mode) or `levelAccessor`+`countAccessor` ("count", pre-aggregated mode), `levels` (required, ordered negative→positive), `orientation` ("horizontal"|"vertical"), `colorScheme`. Horizontal (default): diverging bar chart centered at 0% — negative levels extend left, positive right, neutral (odd count) split 50/50 across centerline. Vertical: stacked 100% bar chart. Supports any scale size (3-point to 7-point+). Raw mode aggregates integer scores automatically (1-based: score 1 → levels[0]). The `levels` array order defines polarity — first half negative, second half positive, center neutral if odd. Supports push API for streaming — accumulates raw data internally and re-aggregates percentages on each push.
51
+
52
+ All ordinal HOCs support `colorBy` and `colorScheme`. `categoryFormat` (`(label: string, index?: number) => string`) customizes individual tick labels (truncation, formatting). `showCategoryTicks` (default true) hides per-tick labels when false — margins auto-adjust. For distribution charts with `colorBy`, set `showCategoryTicks={false}` since the legend identifies categories.
61
53
 
62
54
  ## Network Charts (`semiotic/network`)
63
55
 
64
- **ForceDirectedGraph** — `nodes`, `edges`, `nodeIDAccessor`, `sourceAccessor`, `targetAccessor`, `colorBy`, `colorScheme`, `nodeSize` (number|string|fn), `nodeSizeRange`, `edgeWidth`, `edgeColor`, `edgeOpacity`, `iterations` (300), `forceStrength` (0.1), `showLabels`, `nodeLabel`, `tooltip`, `showLegend`, `legendInteraction`
65
- **SankeyDiagram** — `edges`, `nodes`, `valueAccessor`, `edgeColorBy`, `orientation`, `nodeAlign`, `nodeWidth`, `showLabels`, `edgeOpacity`
56
+ **ForceDirectedGraph** — `nodes`, `edges`, `nodeIDAccessor`, `sourceAccessor`, `targetAccessor`, `colorBy`, `colorScheme`, `nodeSize` (number|string|fn), `nodeSizeRange`, `edgeWidth`, `edgeColor`, `edgeOpacity`, `iterations` (300), `forceStrength` (0.1), `showLabels`, `nodeLabel`, `legendInteraction`
57
+ **SankeyDiagram** — `edges`, `nodes`, `valueAccessor`, `nodeIdAccessor` ("id"), `sourceAccessor` ("source"), `targetAccessor` ("target"), `colorBy`, `edgeColorBy` ("source"|"target"|"gradient"|fn), `orientation`, `nodeAlign`, `nodeWidth`, `nodePaddingRatio`, `nodeLabel`, `showLabels`, `edgeOpacity`
66
58
  **ChordDiagram** — `edges`, `nodes`, `valueAccessor`, `edgeColorBy`, `padAngle`, `groupWidth`, `showLabels`
67
59
  **TreeDiagram** — `data` (root), `layout`, `orientation`, `childrenAccessor`, `colorBy`, `colorByDepth`, `edgeStyle`
68
60
  **Treemap** — `data` (root), `childrenAccessor`, `valueAccessor`, `colorBy`, `colorByDepth`, `showLabels`, `labelMode`
69
61
  **CirclePack** — `data` (root), `childrenAccessor`, `valueAccessor`, `colorBy`, `colorByDepth`, `circleOpacity`
70
- **OrbitDiagram** — animated radial/orbital hierarchy. Use this (not TreeDiagram) when you want animated orbiting nodes. `data` (root), `childrenAccessor`, `nodeIdAccessor`, `orbitMode` ("flat"|"solar"|"atomic"|number[]), `speed` (0.25), `revolution`, `eccentricity`, `orbitSize`, `nodeRadius`, `showRings`, `showLabels`, `animated` (true), `colorBy`, `colorByDepth`, `annotations` (widget annotations anchor by nodeId). For static radial trees, use `TreeDiagram layout="radial"` instead.
62
+ **OrbitDiagram** — animated radial/orbital hierarchy. `data` (root), `childrenAccessor`, `nodeIdAccessor`, `orbitMode` ("flat"|"solar"|"atomic"|number[]), `speed` (0.25), `revolution`, `eccentricity`, `orbitSize`, `nodeRadius`, `showRings`, `showLabels`, `animated` (true), `colorBy`, `colorByDepth`. For static radial trees, use `TreeDiagram layout="radial"`.
71
63
 
72
64
  ## Geo Charts (`semiotic/geo`)
73
65
 
74
- Geographic visualization with d3-geo projections. Canvas-rendered via `StreamGeoFrame`. Import from `semiotic/geo` to avoid adding d3-geo to non-geo bundles.
66
+ Import from `semiotic/geo` NOT `semiotic` to avoid pulling d3-geo into non-geo bundles.
75
67
 
76
- **ChoroplethMap** — `areas` (GeoJSON Feature[] or reference string like "world-110m"), `valueAccessor`, `colorScheme` ("blues"|"reds"|"greens"|"viridis"), `areaOpacity` (1), `projection` ("equalEarth"), `graticule`, `tooltip`, `showLegend`
68
+ **ChoroplethMap** — `areas` (GeoJSON Feature[] or "world-110m"), `valueAccessor`, `colorScheme`, `areaOpacity` (1), `projection` ("equalEarth"), `graticule`, `tooltip`, `showLegend`
77
69
  **ProportionalSymbolMap** — `points`, `xAccessor` ("lon"), `yAccessor` ("lat"), `sizeBy`, `sizeRange` ([3,30]), `colorBy`, `areas` (optional background), `projection`
78
- **FlowMap** — `flows` ({source, target, value}), `nodes`, `xAccessor`, `yAccessor`, `nodeIdAccessor` ("id"), `valueAccessor` ("value"), `edgeColorBy`, `edgeOpacity` (0.6), `edgeWidthRange` ([1,8]), `edgeLinecap` ("round"), `lineType` ("geo"|"line"), `areas` (optional background), `showParticles`, `particleStyle` ({ radius, color, opacity, speedMultiplier, maxPerLine, spawnRate }). Particle `color` accepts a string, `"source"` (inherit line stroke), or `(datum) => string`.
79
- **DistanceCartogram** — `points`, `center` (id of center node), `costAccessor`, `strength` (0-1), `lineMode` ("straight"|"fractional"), `nodeIdAccessor` ("id"), `lines`, `projection`, `showRings` (true|false|number[]), `ringStyle` ({ stroke, strokeWidth, ... }), `showNorth` (true), `costLabel` (string for ring labels), `transition` (ms for smooth animation), `pointRadius`
80
-
81
- All geo HOCs support: `selection`, `linkedHover`, `onObservation`, `showLegend`, `legendInteraction`, `tooltip`, `loading`, `emptyContent`, `frameProps`, `fitPadding` (0–1 fraction, insets auto-fit projection from edges), `zoomable` (defaults true with tileURL, false otherwise), `zoomExtent`, `onZoom`, `dragRotate`, `graticule`, `tileURL`, `tileAttribution`, `tileCacheSize`
82
-
83
- **Zoom/Pan**: All geo charts accept `zoomable` (boolean), `zoomExtent` ([minZoom, maxZoom], default [1, 8]), and `onZoom` (callback with `{ projection, zoom }`). Re-renders projection directly on every zoom tick (no CSS transform). Imperative API: `ref.current.getZoom()`, `ref.current.resetZoom()`.
84
-
85
- **Geo Particles**: `FlowMap` and `StreamGeoFrame` support `showParticles` (boolean) and `particleStyle` to animate dots flowing along line paths. Uses `GeoParticlePool` — an object-pool polyline particle system. Particle `color` accepts: `"source"` (inherit line stroke), a CSS string, or `(datum) => string` for per-line color.
70
+ **FlowMap** — `flows`, `nodes`, `xAccessor`, `yAccessor`, `nodeIdAccessor`, `valueAccessor`, `edgeColorBy`, `edgeOpacity` (0.6), `edgeWidthRange` ([1,8]), `lineType` ("geo"|"line"), `showParticles`, `particleStyle`
71
+ **DistanceCartogram** — `points`, `center` (id), `costAccessor`, `strength` (0-1), `lineMode`, `showRings` (true|false|number[]), `ringStyle`, `showNorth`, `costLabel`, `transition`, `pointRadius`
86
72
 
87
- **Drag Rotate (Globe Spinning)**: `dragRotate` (boolean) when true, drag gestures rotate the projection (globe spinning) instead of panning. **Defaults to true for orthographic projection.** Scroll-wheel zoom still works normally. Explicitly set `dragRotate={false}` on orthographic to get standard pan behavior, or `dragRotate={true}` on other projections to enable rotation. Latitude rotation is clamped to [-90, 90] to prevent flipping.
73
+ All geo HOCs: `fitPadding` (0–1), `zoomable` (defaults true with tileURL), `zoomExtent` ([1,8]), `onZoom`, `dragRotate` (true for orthographic), `graticule`, `tileURL`, `tileAttribution`, `tileCacheSize`, `selection`, `linkedHover`, `onObservation`
88
74
 
89
- **Tile Maps**: All geo charts accept `tileURL` (string template like `"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"` or `(z, x, y, dpr) => string`), `tileAttribution` (e.g., `"© OpenStreetMap contributors"`), `tileCacheSize` (default 256). Tiles render on a background canvas behind data layers. **Mercator projection only** — a dev warning is emitted for non-Mercator projections. Tiles update on zoom/pan. Retina support via `{r}` placeholder or DPR parameter. **Production**: OpenStreetMap tiles are for development/demo only. For production, use a commercial tile provider (Mapbox, MapTiler, Stadia Maps) with your own API key passed via environment variable (never hard-code keys in client code). Example: `tileURL={\`https://api.mapbox.com/styles/v1/mapbox/light-v11/tiles/{z}/{x}/{y}?access_token=\${process.env.MAPBOX_TOKEN}\`}`.
90
-
91
- **StreamGeoFrame** low-level frame with full control. Props: `projection`, `areas`, `points`, `lines`, `xAccessor`, `yAccessor`, `areaStyle`, `pointStyle`, `lineStyle`, `graticule`, `projectionTransform` (distance cartogram config), `projectionExtent`, `enableHover`, `tooltipContent`, `zoomable`, `zoomExtent`, `onZoom`, `tileURL`, `tileAttribution`, `tileCacheSize`, `decay`, `pulse`, `transition`. Push API: `ref.current.push(datum)`, `ref.current.pushMany(data)`, `ref.current.clear()`.
92
-
93
- **Reference geography**: `resolveReferenceGeography("world-110m")` returns GeoJSON features from Natural Earth data (world-atlas). Supported: `"world-110m"`, `"world-50m"`, `"land-110m"`, `"land-50m"`. All geo HOCs accept `areas` as `GeoJSON.Feature[]` or a reference string.
94
-
95
- **mergeData(features, data, { featureKey, dataKey })** — join external data into GeoJSON features by key field. Supports nested paths (e.g., `"properties.iso_a3"`). World-atlas uses ISO 3166-1 numeric codes as the `id` field. Also available from `semiotic/data` as a general join-by-key utility.
75
+ **Tiles**: `tileURL` accepts string template (`{z}/{x}/{y}`) or function. Mercator only. OSM tiles are dev-only use commercial provider with env var key in production.
76
+ **Zoom**: Imperative: `ref.current.getZoom()`, `ref.current.resetZoom()`.
77
+ **Reference geography**: `resolveReferenceGeography("world-110m"|"world-50m"|"land-110m"|"land-50m")` returns GeoJSON features.
78
+ **mergeData(features, data, { featureKey, dataKey })** — join data into GeoJSON by key. World-atlas uses ISO numeric codes as `id`.
96
79
 
97
80
  ```jsx
98
- // World choropleth with reference geography + data joining
99
81
  import { ChoroplethMap, resolveReferenceGeography, mergeData } from "semiotic/geo"
100
82
  const world = await resolveReferenceGeography("world-110m")
101
83
  const areas = mergeData(world, gdpData, { featureKey: "id", dataKey: "id" })
102
84
  <ChoroplethMap areas={areas} valueAccessor="gdpPerCapita" colorScheme="viridis"
103
85
  projection="equalEarth" zoomable tooltip />
104
-
105
- // Distance cartogram (ORBIS-style) with concentric rings overlay
106
- import { DistanceCartogram } from "semiotic/geo"
107
- <DistanceCartogram
108
- points={cities} center="rome" costAccessor="travelDays"
109
- strength={0.8} lines={routes} showLegend zoomable
110
- showRings costLabel="days" showNorth
111
- ringStyle={{ stroke: "#999", strokeWidth: 0.5 }}
112
- />
113
-
114
- // Tile map basemap with proportional symbols
115
- <ProportionalSymbolMap
116
- points={earthquakes} xAccessor="lon" yAccessor="lat"
117
- sizeBy="magnitude" sizeRange={[2, 20]}
118
- projection="mercator" zoomable
119
- tileURL="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
120
- tileAttribution="© OpenStreetMap contributors"
121
- />
122
-
123
- // Streaming geo points with zoom
124
- const geoRef = useRef()
125
- geoRef.current.push({ lon: -122.4, lat: 37.8, value: 42 })
126
- <StreamGeoFrame ref={geoRef} projection="mercator" xAccessor="lon" yAccessor="lat"
127
- runtimeMode="streaming" decay={{ type: "linear", minOpacity: 0.1 }}
128
- zoomable zoomExtent={[1, 12]} onZoom={({ zoom }) => console.log(zoom)} />
129
86
  ```
130
87
 
88
+ **StreamGeoFrame** — low-level frame. Push API: `ref.current.push(datum)`, `.pushMany()`, `.clear()`. Props: `projection`, `areas`, `points`, `lines`, `xAccessor`, `yAccessor`, `areaStyle`, `pointStyle`, `lineStyle`, `graticule`, `zoomable`, `decay`, `pulse`, `transition`.
89
+
131
90
  ## Realtime Charts (`semiotic/realtime`)
132
91
 
133
92
  Push API: `chartRef.current.push({ time, value })`
134
93
 
135
- **IMPORTANT**: All pushed data must include a time field (default: `"time"`). If your data uses a different field name, set `timeAccessor` explicitly. Without a valid time field, charts render blank with no error.
94
+ **IMPORTANT**: All pushed data must include a time field (default: `"time"`). Set `timeAccessor` if your field differs. Without valid time field, charts render blank.
136
95
 
137
- Sizing: all Realtime HOCs accept both `size={[600, 400]}` (tuple) and `width={600} height={400}`. Either works.
96
+ **RealtimeLineChart** `timeAccessor` ("time"), `valueAccessor` ("value"), `windowSize` (200), `windowMode`, `stroke`, `strokeWidth`
97
+ **RealtimeHistogram** — `binSize` (required), `timeAccessor`, `valueAccessor`, `categoryAccessor`, `colors`, `brush` (boolean|"x"|object, defaults to `{ dimension: "x", snap: "bin" }` when `true`), `onBrush`, `linkedBrush` (cross-chart coordination)
98
+ **RealtimeSwarmChart** — `timeAccessor`, `valueAccessor`, `categoryAccessor`, `radius`, `opacity`
99
+ **RealtimeWaterfallChart** — `timeAccessor`, `valueAccessor`, `positiveColor`, `negativeColor`
100
+ **RealtimeHeatmap** — `timeAccessor`, `valueAccessor`, `heatmapXBins`, `heatmapYBins`, `aggregation`
101
+ **Streaming Sankey** — `StreamNetworkFrame` with `chartType="sankey"`, `showParticles`, `particleStyle`. Push individual edges: `ref.current.push({ source, target, value })`.
138
102
 
139
- **RealtimeLineChart** `size`|`width`+`height`, **`timeAccessor`** ("time"), **`valueAccessor`** ("value"), `windowSize` (200), `windowMode`, `stroke`, `strokeWidth`
140
- **RealtimeHistogram** — **`binSize`** (required), **`timeAccessor`** ("time"), **`valueAccessor`** ("value"), `categoryAccessor`, `colors`. Time field is required even though this shows a distribution — it's used for windowing.
141
- **RealtimeSwarmChart** — **`timeAccessor`** ("time"), **`valueAccessor`** ("value"), `categoryAccessor`, `radius`, `opacity`
142
- **RealtimeWaterfallChart** — **`timeAccessor`** ("time"), **`valueAccessor`** ("value"), `positiveColor`, `negativeColor`
143
- **RealtimeHeatmap** — **`timeAccessor`** ("time"), **`valueAccessor`** ("value"), `heatmapXBins`, `heatmapYBins`, `aggregation`. Both accessors must match your data fields or the chart renders blank.
144
- **Streaming Sankey** — `StreamNetworkFrame` with `chartType="sankey"`, `showParticles` (boolean), `particleStyle` (`{ radius, opacity, speedMultiplier, maxPerEdge, colorBy }`), `tensionConfig`, `thresholds`. Push **individual edges**: `ref.current.push({ source: "A", target: "B", value: 42 })`. Use `ref.current.pushMany([...edges])` for batches.
103
+ Encoding: `decay`, `pulse`, `transition`, `staleness` compose freely on all streaming charts.
145
104
 
146
- Realtime encoding: `decay`, `pulse`, `transition`, `staleness` compose freely on all streaming charts.
105
+ All Realtime* charts accept `data` props for static mode (no push API needed). RealtimeHistogram brush supports bin-snapping (`snap: "bin"`) and streaming tracking — the brush shrinks as selected bins scroll off and auto-clears when fully evicted. Bin snapping uses actual computed bin boundaries (data-driven), not a uniform grid — works with irregular bin widths. `snapDuring: true` enables continuous snap feedback during drag (not just on release).
147
106
 
148
- ### Realtime data shape
107
+ ### Push API on HOC charts
108
+ Most HOC charts support push via `forwardRef`. **Omit** `data`/`nodes`/`edges` — do NOT pass `data={[]}`.
149
109
  ```jsx
150
- // Every pushed datum should have a time field
151
- ref.current.push({ time: Date.now(), value: 42 }) // line, waterfall
152
- ref.current.push({ time: Date.now(), value: 42, category: "A" }) // histogram, swarm
153
- ref.current.push({ time: Date.now(), value: 42 }) // heatmap (time=x, value=y)
110
+ const ref = useRef()
111
+ ref.current.push({ x: 1, y: 2 }) // single
112
+ ref.current.pushMany([...points]) // batch
113
+ ref.current.clear() // reset
114
+ ref.current.getData() // read
115
+ <Scatterplot ref={ref} xAccessor="x" yAccessor="y" />
154
116
  ```
117
+ Supported: all XY, ordinal, network (Force, Sankey, Chord), geo point charts. **Not supported**: hierarchy charts (Tree, Treemap, CirclePack, Orbit), ChoroplethMap, FlowMap, ScatterplotMatrix.
155
118
 
156
- ### Push API on HOC charts
157
- Many HOC charts support the push API via `forwardRef`. Omit the `data` prop and push data imperatively:
119
+ ## Stream Frame Callbacks (advanced)
120
+ Frame callbacks (`nodeStyle`, `edgeStyle`, `nodeSize` as fn) receive `RealtimeNode`/`RealtimeEdge` wrappers. Access original data via `.data`:
158
121
  ```jsx
159
- const chartRef = useRef()
160
- chartRef.current.push({ x: 1, y: 2 }) // single point
161
- chartRef.current.pushMany([...points]) // batch
162
- chartRef.current.clear() // reset
163
- chartRef.current.getData() // read current data
164
- <Scatterplot ref={chartRef} xAccessor="x" yAccessor="y" />
122
+ // WRONG: nodeSize={(d) => d.weight} — d.weight is undefined
123
+ // RIGHT: nodeSize={(d) => d.data?.weight} or use string: nodeSize="weight"
165
124
  ```
166
- **IMPORTANT**: When using the push API, **omit** the `data`/`nodes`/`edges` prop entirely — do NOT pass `data={[]}`, which clears pushed data on every render. Streaming-specific props (`windowSize`, `decay`, `pulse`) go in `frameProps`.
167
-
168
- Supported: all XY charts (LineChart, AreaChart, Scatterplot, etc.), all ordinal charts (BarChart, Histogram, etc.), network charts (ForceDirectedGraph, SankeyDiagram, ChordDiagram), and geo point charts (ProportionalSymbolMap, DistanceCartogram). **Not supported**: hierarchy charts (TreeDiagram, Treemap, CirclePack, OrbitDiagram) — their root-object data shape is incompatible with flat push. ChoroplethMap (area-based, not point-based), FlowMap (line-based), and ScatterplotMatrix also do not support push.
125
+ Same applies to `frameProps` style functions on HOCs. `customHoverBehavior`/`customClickBehavior` receive `{ type, data, x, y } | null`. `tooltipContent` receives `{ type, data }`.
169
126
 
170
- ## Stream Frame Callbacks (advanced — prefer HOCs)
171
- Stream Frame callbacks (`nodeStyle`, `edgeStyle`, `nodeSize` as function, `colorBy` as function, `nodeLabel` as function) receive **`RealtimeNode`/`RealtimeEdge`** wrappers, NOT your raw data. Access your original data via `.data`:
127
+ ## Hover Indicator
128
+ The hover dot automatically matches the hovered element's color (line stroke, point fill, etc.). Override via `frameProps`:
172
129
  ```jsx
173
- // WRONG: nodeSize={(d) => d.weight} — d is RealtimeNode, d.weight is undefined
174
- // RIGHT: nodeSize={(d) => d.data?.weight} — d.data is your original node object
175
- // RIGHT: nodeSize="weight" — string accessor handles this automatically
176
- // WRONG: nodeStyle={(d) => ({ fill: d.datum.color })} — .datum does not exist
177
- // RIGHT: nodeStyle={(d) => ({ fill: d.data?.color })} — use .data
130
+ <LineChart frameProps={{ hoverAnnotation: { pointColor: "#ff0000" } }} />
178
131
  ```
179
- `customHoverBehavior` and `customClickBehavior` receive `{ type: "node"|"edge", data: <your raw object>, x, y } | null`.
180
- `tooltipContent` receives `{ type: "node"|"edge", data: <your raw object> }`.
132
+ Fallback chain: `pointColor` element color `--semiotic-primary` CSS var `#007bff`.
181
133
 
182
134
  ## Coordinated Views
183
135
 
184
- **LinkedCharts** — wraps charts. Props: `selections` (resolution: "union"|"intersect"|"crossfilter"), `showLegend` (auto when CategoryColorProvider present), `legendPosition` ("top"|"bottom"), `legendInteraction` ("highlight"|"isolate"|"none"), `legendSelectionName` (selection name for legend-driven cross-highlighting), `legendField` (data field for legend selections)
185
- **CategoryColorProvider** — stable category→color mapping. Props: `colors` (map) or `categories` + `colorScheme`
136
+ **LinkedCharts** — `selections` (resolution: "union"|"intersect"|"crossfilter"), `showLegend`, `legendPosition`, `legendInteraction`, `legendSelectionName`, `legendField`
137
+ **CategoryColorProvider** — `colors` (map) or `categories` + `colorScheme`
186
138
  Chart props: `selection`, `linkedHover`, `linkedBrush`. Hooks: `useSelection`, `useLinkedHover`, `useBrushSelection`, `useFilteredData`
187
- **ScatterplotMatrix** — `data`, `fields`, `colorBy`, `cellSize`, `hoverMode`, `brushMode`
188
-
189
- ## ChartContainer
190
-
191
- **ChartContainer** — wrapper with title, subtitle, status indicator, toolbar actions. Props: `title`, `subtitle`, `height` (default **400** — set this to match your chart's height or you'll get extra whitespace), `width` (default "100%"), `status` ("live"|"stale"|"error"), `loading`, `error`, `errorBoundary`, `actions` (`{ export, fullscreen, copyConfig }`), `controls`, `style`, `className`
192
-
193
- When using `ChartContainer` with a chart that has `size={[w, h]}`, always set `height={h}` on the container to avoid a mismatch.
194
139
 
195
- ## Layout & Composition
196
-
197
- **ChartGrid** — CSS Grid layout. `columns` (number|"auto"), `minCellWidth` (300), `gap` (16). Children with `emphasis="primary"` span two columns.
198
- **ContextLayout** — primary + context panel. `context` (ReactNode), `position`, `contextSize` (250)
140
+ **Linked crosshair** (coordinate-based hover sync): `linkedHover={{ name: "sync", mode: "x-position", xField: "time" }}` broadcasts the hovered X data value. Other charts with the same `linkedHover` name render a synced vertical crosshair at that X position. Each chart shows its own Y values independently. Use for dashboards with multiple time-series at different scales.
141
+ **ScatterplotMatrix** — `data`, `fields`, `colorBy`, `cellSize`, `hoverMode`, `brushMode`
142
+ **ChartContainer** — `title`, `subtitle`, `height` (400), `width` ("100%"), `status`, `loading`, `error`, `errorBoundary`, `actions` ({ export, fullscreen, copyConfig, dataSummary }), `controls`
143
+ **ChartGrid** — `columns` (number|"auto"), `minCellWidth` (300), `gap` (16). `emphasis="primary"` spans two columns.
144
+ **ContextLayout** — `context` (ReactNode), `position`, `contextSize` (250)
199
145
 
200
146
  ## Key Patterns
201
147
 
202
148
  ```jsx
203
- // Force-directed graph with custom sizing and hover
204
- <ForceDirectedGraph
205
- nodes={[{ id: "A", group: "eng", weight: 10 }, { id: "B", group: "design", weight: 5 }]}
206
- edges={[{ source: "A", target: "B" }]}
207
- colorBy="group"
208
- nodeSize="weight" // string accessor → reads node.weight, scales to nodeSizeRange
209
- nodeSizeRange={[5, 25]}
210
- showLabels
211
- showLegend
212
- tooltip={(d) => <div>{d.data.id}: {d.data.weight}</div>}
213
- frameProps={{
214
- customClickBehavior: (d) => { if (d?.type === "node") console.log(d.data) },
215
- background: "#f5f5f5",
216
- }}
217
- />
218
-
219
- // Cross-highlighting dashboard with column spanning
220
- // emphasis="primary" makes a chart span 2 columns in ChartGrid
149
+ // Cross-highlighting dashboard
221
150
  <CategoryColorProvider categories={["North", "South", "East"]}>
222
151
  <LinkedCharts>
223
152
  <ChartGrid columns={2}>
224
153
  <LineChart data={d} colorBy="region" linkedHover={{ name: "hl", fields: ["region"] }} selection={{ name: "hl" }} emphasis="primary" responsiveWidth />
225
154
  <BarChart data={d} colorBy="region" linkedHover={{ name: "hl", fields: ["region"] }} selection={{ name: "hl" }} responsiveWidth />
226
- <Scatterplot data={d} colorBy="region" linkedHover={{ name: "hl", fields: ["region"] }} selection={{ name: "hl" }} responsiveWidth />
227
155
  </ChartGrid>
228
156
  </LinkedCharts>
229
157
  </CategoryColorProvider>
230
158
 
231
- // Forecast + anomaly (auto)
159
+ // Forecast + anomaly
232
160
  <LineChart data={ts} xAccessor="time" yAccessor="value"
233
161
  forecast={{ trainEnd: 60, steps: 15, confidence: 0.95 }}
234
162
  anomaly={{ threshold: 2 }} />
235
163
 
236
- // Forecast (pre-computed ML bounds)
164
+ // Pre-computed forecast bounds
237
165
  <LineChart data={ml} xAccessor="time" yAccessor="value"
238
166
  forecast={{ isTraining: "isTraining", isForecast: "isForecast", isAnomaly: "isAnomaly", upperBounds: "upper", lowerBounds: "lower" }} />
239
167
 
240
- // Stacked area (flat array + areaBy, NOT lineBy)
241
- <StackedAreaChart data={flatData} xAccessor="month" yAccessor="value"
242
- areaBy="category" colorBy="category" />
243
-
244
- // Percentile band (p5–p95) with main line (p50) — MUST layer two charts
245
- // AreaChart with y0Accessor renders the band; showLine only draws the TOP edge (p95), not p50
246
- // To show a separate main line, add a LineChart on top:
168
+ // Percentile band layer AreaChart + LineChart
247
169
  <>
248
170
  <AreaChart data={d} xAccessor="x" yAccessor="p95" y0Accessor="p5"
249
171
  showLine={false} areaOpacity={0.3} gradientFill />
250
172
  <LineChart data={d} xAccessor="x" yAccessor="p50" lineWidth={2} />
251
173
  </>
252
- // Simple gradient area (no band):
253
- <AreaChart data={d} xAccessor="x" yAccessor="y" gradientFill />
254
174
 
255
- // Realtime always include time field in pushed data
256
- const ref = useRef()
257
- ref.current.push({ time: Date.now(), value: 42 })
258
- <RealtimeLineChart ref={ref} timeAccessor="time" valueAccessor="value" />
259
-
260
- // Realtime histogram — time field required even for distribution charts
261
- const histRef = useRef()
262
- histRef.current.push({ time: Date.now(), value: Math.abs(delta) })
263
- <RealtimeHistogram ref={histRef} timeAccessor="time" valueAccessor="value" binSize={100} />
264
-
265
- // Streaming sankey with particles — push individual edges, NOT full snapshots
175
+ // Streaming sankey with particles
266
176
  const sankeyRef = useRef()
267
- sankeyRef.current.push({ source: "Web", target: "API", value: 1 }) // one edge at a time
268
- sankeyRef.current.pushMany([ // or batch
269
- { source: "Web", target: "API", value: 3 },
270
- { source: "API", target: "DB", value: 2 },
271
- ])
272
- <StreamNetworkFrame
273
- ref={sankeyRef}
274
- chartType="sankey"
275
- showParticles={true}
276
- particleStyle={{ radius: 2, colorBy: "source", speedMultiplier: 1.5 }}
277
- width={600} height={400}
278
- />
279
-
280
- // SSR — renderToStaticSVG takes frame type string, not component name
177
+ sankeyRef.current.push({ source: "Web", target: "API", value: 1 })
178
+ <StreamNetworkFrame ref={sankeyRef} chartType="sankey"
179
+ showParticles particleStyle={{ radius: 2, colorBy: "source" }}
180
+ width={600} height={400} />
181
+
182
+ // SSR
281
183
  import { renderOrdinalToStaticSVG } from "semiotic/server"
282
- const svg = renderOrdinalToStaticSVG({
283
- data, categoryAccessor: "category", valueAccessor: "value", width: 600, height: 400
284
- })
184
+ const svg = renderOrdinalToStaticSVG({ data, categoryAccessor: "cat", valueAccessor: "val", width: 600, height: 400 })
285
185
  ```
286
186
 
287
187
  ## Annotations
288
- - `type: "widget"` — place any React element at data coordinates. Works on all frame types. XY/ordinal use data coordinates (`x`/`y` or field names). Network/orbit use `nodeId`. Default: info emoji. Renders as HTML overlay (not SVG) so popups/threads overflow freely.
188
+
189
+ All HOCs accept `annotations` (array). Coordinates use your data field names. Network/orbit use `nodeId`.
190
+
191
+ **Positioning**: `widget` (React content at data coords — v3 replacement for v2 `htmlAnnotationRules`; props: `content`, `dx`, `dy`, `width`, `height`, `anchor`), `label` (callout with connector), `callout` (circle + label), `text` (plain text), `bracket`
192
+ **Reference lines**: `y-threshold` (`value`, `label`, `color`, `labelPosition`: "left"|"center"|"right", `strokeDasharray`), `x-threshold` (`labelPosition`: "top"|"center"|"bottom"), `band` (`y0`, `y1`, `label`, `fill`)
193
+ **Ordinal**: `category-highlight` (`category`, `color`, `opacity`, `label`) — highlights a category column/row. Works on BarChart, StackedBarChart, etc. `y-threshold` also works on vertical ordinal charts.
194
+ **Enclosures**: `enclose` (circle around `coordinates`), `rect-enclose`, `highlight` (`filter` fn or `field`+`value`)
195
+ **Statistical** (XY): `trend` (`method`: linear/polynomial/loess), `envelope`, `anomaly-band`, `forecast`
196
+ **Streaming anchors**: `"fixed"` (default), `"latest"` (tracks newest datum), `"sticky"` (freezes when evicted)
197
+
198
+ Custom rendering: `frameProps.svgAnnotationRules = (annotation, index, context) => ReactNode | null`. Context has `scales`, `width`, `height`, `data`. Colors inherit from theme (`--semiotic-primary`, `--semiotic-text-secondary`).
199
+
289
200
  ```jsx
290
- annotations={[{ type: "widget", month: 4, revenue: 32, dy: -4, content: <MyAlertButton /> }]}
291
- // OrbitDiagram: annotations={[{ type: "widget", nodeId: "Pipeline", content: <Alert /> }]}
201
+ <LineChart data={data} xAccessor="time" yAccessor="latency"
202
+ annotations={[
203
+ { type: "y-threshold", value: 200, label: "SLA limit", color: "#e45050" },
204
+ { type: "widget", time: 42, latency: 850, dy: -30, content: <span>Incident</span> },
205
+ ]} />
292
206
  ```
293
207
 
208
+ ## Theming
209
+
210
+ Charts are themeable via CSS custom properties on any ancestor element. Key vars: `--semiotic-bg`, `--semiotic-text`, `--semiotic-text-secondary`, `--semiotic-border`, `--semiotic-grid`, `--semiotic-primary`, `--semiotic-focus`, `--semiotic-font-family`, `--semiotic-border-radius`, `--semiotic-tooltip-bg`/`text`/`radius`/`font-size`/`shadow`, `--semiotic-selection-color`/`opacity`, `--semiotic-diverging`.
211
+
212
+ ```jsx
213
+ import { ThemeProvider } from "semiotic"
214
+ <ThemeProvider theme="tufte"> {/* Named preset */}
215
+ <ThemeProvider theme={{ colors: { primary: "#ff6b6b", categorical: [...] } }}> {/* Custom */}
216
+ ```
217
+
218
+ **Color resolution priority** (when `colorBy` is set): explicit `colorScheme` prop > ThemeProvider `colors.categorical` > `"category10"` fallback. This means ThemeProvider categorical colors automatically apply to all charts — no need to pass `colorScheme` on every component.
219
+
220
+ Presets: `light`, `dark`, `high-contrast`, `pastels`, `pastels-dark`, `bi-tool`, `bi-tool-dark`, `italian`, `italian-dark`, `tufte`, `tufte-dark`, `journalist`, `journalist-dark`, `playful`, `playful-dark`, `carbon`, `carbon-dark`.
221
+
222
+ Serialization (`semiotic/themes`): `themeToCSS(theme, selector)`, `themeToTokens(theme)`, `resolveThemePreset(name)`.
223
+ Color-blind palette: `import { COLOR_BLIND_SAFE_CATEGORICAL } from "semiotic"` (8-color Wong 2011).
224
+ IBM Carbon palette: `import { CARBON_CATEGORICAL_14, CARBON_ALERT } from "semiotic"` (14-color categorical + 4 alert colors).
225
+
226
+ **`semiotic/utils`** (~137KB, ~10% of full bundle) — Lightweight entry point for utilities without any chart components:
227
+ - **Theme**: `ThemeProvider`, `useTheme`, `LIGHT_THEME`, `DARK_THEME`, `HIGH_CONTRAST_THEME`, `COLOR_BLIND_SAFE_CATEGORICAL`, `CARBON_CATEGORICAL_14`, `CARBON_ALERT`, `themeToCSS`, `themeToTokens`, `resolveThemePreset`, `THEME_PRESETS`
228
+ - **Format**: `adaptiveTimeTicks`, `smartTickFormat`
229
+ - **Color**: `darkenColor`, `lightenColor`
230
+ - **Patterns**: `createHatchPattern`
231
+ - **Validation**: `validateProps`, `diagnoseConfig`
232
+ - **Serialization**: `toConfig`, `fromConfig`, `toURL`, `fromURL`, `copyConfig`, `configToJSX`, `serializeSelections`, `deserializeSelections`, `exportChart`
233
+ - **Vega-Lite**: `fromVegaLite` — convert Vega-Lite specs to Semiotic configs
234
+ - **Data structures**: `RingBuffer`, `IncrementalExtent`
235
+ - **Tooltip**: `normalizeTooltip`
236
+
237
+ Key: `ThemeProvider` sets CSS vars on a wrapper div (no React context). Canvas charts read vars via `getComputedStyle`. `exportChart` inlines computed styles.
238
+
239
+ **Dark/light mode merge rules:** String preset (e.g. `"dark"`) → full replacement with that preset's theme. Object with `mode` (e.g. `{ mode: "dark", colors: { categorical: [...] } }`) → merges onto the matching base theme (`DARK_THEME` or `LIGHT_THEME`), so background/text/grid adapt while your overrides are preserved. Object without `mode` → shallow-merges onto the current theme (partial override). ThemeProvider is reactive — changing the `theme` prop re-applies immediately.
240
+
241
+ **CSS interop:** Host app `--semiotic-*` vars on `:root` are overridden by ThemeProvider's closer wrapper div. To let app tokens flow through, either skip ThemeProvider and set `--semiotic-*` vars in CSS, or use the hybrid approach (ThemeProvider for palette only, CSS vars for chrome).
242
+
294
243
  ## Server-Side Rendering
295
- - All HOC charts and Stream Frames render SVG automatically in server environments (no window/document)
296
- - `renderToStaticSVG(frameType, props)` standalone SVG string from `semiotic/server`. `frameType` is `"xy"` | `"ordinal"` | `"network"` | `"geo"` (NOT a component name like "BarChart")
297
- - Type-specific shortcuts: `renderXYToStaticSVG(props)`, `renderOrdinalToStaticSVG(props)`, `renderNetworkToStaticSVG(props)`, `renderGeoToStaticSVG(props)`
298
- - For a bar chart: `renderOrdinalToStaticSVG({ data, categoryAccessor: "cat", valueAccessor: "val", width: 600, height: 400 })`
299
- - Works with Next.js App Router, Remix, Astro — same component on server and client
300
- - **Geo SSR requires pre-resolved features**: `renderGeoToStaticSVG` is synchronous — pass GeoJSON features directly, not reference strings like `"world-110m"`. Call `await resolveReferenceGeography("world-110m")` first and pass the result as `areas`.
244
+ - HOC charts and Frames render SVG automatically in server environments
245
+ - `renderXYToStaticSVG(props)`, `renderOrdinalToStaticSVG(props)`, `renderNetworkToStaticSVG(props)`, `renderGeoToStaticSVG(props)` from `semiotic/server`
246
+ - `frameType` is `"xy"|"ordinal"|"network"|"geo"` (NOT component names)
247
+ - Geo SSR requires pre-resolved features (synchronous call `resolveReferenceGeography` first)
248
+ - Works with Next.js App Router, Remix, Astro
301
249
 
302
250
  ## AI Features
303
- - `onObservation` — structured events (hover, click, brush, selection) on all HOCs
304
- - `useChartObserver` — aggregates observations across LinkedCharts
305
- - `toConfig`/`fromConfig`/`toURL`/`fromURL`/`copyConfig`/`configToJSX` — chart state serialization
306
- - `DetailsPanel` — click-driven detail panel inside `ChartContainer`
307
- - `validateProps(componentName, props)` — prop validation with Levenshtein typo suggestions
308
- - `diagnoseConfig(componentName, props)` — anti-pattern detector (12 checks: empty data, bad dimensions, missing accessors, margin overflow, etc.)
309
- - `ChartErrorBoundary` — error boundary
310
- - `exportChart(containerDiv, { format: "png"|"svg" })` — pass the **wrapper div** (not the SVG element); it finds canvas + SVG internally. Default: PNG, composites canvas + SVG layers
311
- - `npx semiotic-ai --doctor` — validate component + props JSON from CLI (uses both validateProps and diagnoseConfig)
251
+ - `onObservation` / `useChartObserver` — structured events across charts
252
+ - `toConfig`/`fromConfig`/`toURL`/`fromURL`/`copyConfig`/`configToJSX` — serialization
253
+ - `DetailsPanel` — click-driven detail panel in `ChartContainer`
254
+ - `validateProps(componentName, props)` — prop validation with typo suggestions
255
+ - `diagnoseConfig(componentName, props)` — anti-pattern detector (13+ checks)
256
+ - `exportChart(containerDiv, { format: "png"|"svg" })` pass wrapper div, composites canvas+SVG
257
+ - `npx semiotic-ai --doctor` — CLI validation
312
258
 
313
- ## Known Pitfalls
259
+ ## Canvas Pattern Fills
314
260
 
315
- **Tooltip datum shape**: HOC tooltip functions receive your raw data object. When using `frameProps.tooltipContent` on Stream Frames, the datum may be wrapped access your data via `d.data`. HOC `tooltip` functions don't need this.
261
+ `createHatchPattern({ background, stroke, lineWidth, spacing, angle })` from `semiotic` returns `CanvasPattern | null` for use as `fill` in style functions. Used by FunnelChart vertical mode for dropoff bars.
316
262
 
317
- **Legend positioning**: `legendPosition` controls where the legend renders. When set to "bottom", the chart automatically expands the bottom margin to ~80px to clear axis labels. For "top", margin expands to ~50px. If you need more space, override `margin` explicitly. For charts narrower than ~400px, prefer `legendPosition="bottom"` or `"top"` (bottom is more common) to avoid squeezing the chart area. Similarly, for short charts (~250px or less), a side legend may compress the chart too much — use top or bottom instead.
263
+ ## Accessibility
318
264
 
319
- **Log scale and zero**: `xScaleType="log"` / `yScaleType="log"` clamp domain minimums to 1e-6 because log(0) is undefined. Data with zero or negative values will be clamped.
265
+ Charts render with `role="group"` (outer interactive wrapper, keyboard/focus) and `role="img"` (inner canvas, read by assistive tech). SVG overlays include `<title>` and `<desc>`.
320
266
 
321
- **Heatmap with string axes**: Heatmap supports string/categorical x and y values (e.g., weekday names, hour labels). The `colorScheme` prop accepts d3-scale-chromatic names: "blues", "reds", "greens", "viridis".
267
+ **Keyboard navigation**: Arrow keys navigate data points. In XY/ordinal charts, ArrowRight/Left moves within a series, ArrowUp/Down switches series. In network charts, arrows move to the spatially nearest node in the pressed direction; Enter cycles edge-connected neighbors. Home/End jump to first/last. PageUp/PageDown skip 10%. Escape clears focus.
322
268
 
323
- **barPadding is in pixels**: `barPadding` on ordinal charts is an absolute pixel value divided by the chart width to compute a band scale padding ratio. The defaults (40 for bar/stacked, 60 for grouped) work well at 600px width. For very small charts, you may need to reduce it.
269
+ **Focus ring**: Shape-adaptive dashed ring (circle for points, rect for bars, arc for wedges). Color: `--semiotic-focus` CSS var.
324
270
 
325
- **Horizontal bar charts need wider left margins**: When using `orientation="horizontal"` with long category labels, increase the left margin manually: `margin={{ left: 120 }}`. There is no auto-measurement of label width.
271
+ **Data summary**: `accessibleTable` (default true) renders a sr-only summary. Activate via keyboard focus or `actions.dataSummary` in ChartContainer. JIT-computed no render cost until activated.
326
272
 
327
- **LinkedCharts suppresses child legends**: When a `CategoryColorProvider` wraps `LinkedCharts`, individual chart legends are suppressed in favor of a unified legend. To force a child chart to show its own legend, set `showLegend={true}` explicitly.
273
+ **Reduced motion**: `prefers-reduced-motion` auto-detected. Transitions skip to end state, orbit stops, pulse/decay disabled.
328
274
 
329
- **Geo bundle isolation**: `semiotic/geo` is a separate entry point. Do NOT import geo components from `semiotic` — use `import { ChoroplethMap } from "semiotic/geo"` to avoid pulling d3-geo (~30KB) into non-geo bundles.
275
+ **High contrast**: `forced-colors` / `prefers-contrast: more` auto-detected. ThemeProvider applies `HIGH_CONTRAST_THEME` automatically.
330
276
 
331
- **Push API: omit data, don't pass empty array**: When using `ref.current.push()` on HOCs, **omit** the `data`/`nodes`/`edges` prop entirely. Passing `data={[]}` clears pushed data on every render because the HOC forwards it to the Stream Frame's `setBoundedData([])`. Similarly, `data={undefined}` is fine (prop not present), but `data={null}` is treated the same as omitted.
277
+ **Hooks** (from `semiotic`): `useReducedMotion()`, `useHighContrast()` SSR-safe, return `false` on server.
332
278
 
333
- **`diagnoseConfig` catches common mistakes**: Run `diagnoseConfig("BarChart", props)` to check for empty data, bad dimensions, missing accessors, margin overflow, invisible bar padding, and more. Use `npx semiotic-ai --doctor` from CLI.
279
+ ## Known Pitfalls
334
280
 
335
- ## Differentiators
336
- Network viz, geographic viz (choropleth, flow maps, distance cartograms), streaming canvas, realtime encoding, coordinated views, statistical summaries, AI hooks, chart serialization, global theming, keyboard navigation, interactive legends (highlight/isolate), direct labeling, gap handling, empty/loading states, landmark tick labels, LinkedCharts unified legend
281
+ - **Tooltip datum shape**: HOC tooltip functions get raw data. Frame `tooltipContent` gets wrapped data — use `d.data`.
282
+ - **Tooltip positioning**: Tooltips auto-flip when near container edges (right→left, bottom→top). Custom `tooltip` content should not add its own background the wrapper provides `--semiotic-tooltip-bg`, `--semiotic-tooltip-text`, etc. Override wrapper styles via CSS custom properties, not inline styles.
283
+ - **Legend positioning**: "bottom" auto-expands margin ~80px. For narrow charts (<400px), prefer "bottom" or "top".
284
+ - **MultiAxisLineChart legend**: Always use `legendPosition="bottom"` (or `"top"`) — the right-hand axis occupies the space where a right-side legend would go.
285
+ - **Log scale**: Clamps domain min to 1e-6 (log(0) undefined).
286
+ - **barPadding**: Pixel value, defaults 40/60. Reduce for small charts.
287
+ - **Horizontal bars**: Need wider left margin with long labels: `margin={{ left: 120 }}`.
288
+ - **LinkedCharts legends**: `CategoryColorProvider` suppresses child legends. Force with `showLegend={true}`.
289
+ - **Push API**: Omit `data` prop entirely. `data={[]}` clears pushed data every render.
290
+ - **frameProps style functions**: Bypass HOC color resolution — use `colorBy` prop instead. Frame style functions receive `(datum, categoryName)`, not `(datum, index)`.
291
+ - **v2 migration**: `htmlAnnotationRules` → `widget` annotations + `svgAnnotationRules`. v2 `summaryStyle` index-based coloring → v3 category-string-based.
292
+ - **accessibleTable**: Direct prop on HOCs. Set `accessibleTable={false}` to disable the sr-only data summary.
293
+
294
+ ## Performance
295
+
296
+ Prefer string accessors (`xAccessor="value"`) over function accessors — always referentially stable. If you must use functions, memoize with `useCallback` or define outside the component. The pipeline uses `.toString()` comparison for inline arrows but this fails for closures capturing changing variables.