semiotic 3.1.1 → 3.2.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 (83) hide show
  1. package/CLAUDE.md +134 -216
  2. package/LICENSE +197 -10
  3. package/README.md +1 -0
  4. package/ai/dist/componentRegistry.js +6 -0
  5. package/ai/dist/mcp-server.js +232 -65
  6. package/ai/dist/renderHOCToSVG.js +5 -3
  7. package/ai/examples.md +93 -0
  8. package/ai/schema.json +3916 -878
  9. package/ai/system-prompt.md +27 -0
  10. package/dist/components/ThemeProvider.d.ts +5 -3
  11. package/dist/components/charts/geo/ChoroplethMap.d.ts +1 -1
  12. package/dist/components/charts/index.d.ts +8 -1
  13. package/dist/components/charts/ordinal/BarChart.d.ts +1 -0
  14. package/dist/components/charts/ordinal/BoxPlot.d.ts +1 -0
  15. package/dist/components/charts/ordinal/DonutChart.d.ts +1 -0
  16. package/dist/components/charts/ordinal/DotPlot.d.ts +1 -0
  17. package/dist/components/charts/ordinal/FunnelChart.d.ts +55 -0
  18. package/dist/components/charts/ordinal/GroupedBarChart.d.ts +1 -0
  19. package/dist/components/charts/ordinal/Histogram.d.ts +1 -0
  20. package/dist/components/charts/ordinal/PieChart.d.ts +1 -0
  21. package/dist/components/charts/ordinal/RidgelinePlot.d.ts +1 -0
  22. package/dist/components/charts/ordinal/StackedBarChart.d.ts +1 -0
  23. package/dist/components/charts/ordinal/SwarmPlot.d.ts +1 -0
  24. package/dist/components/charts/ordinal/ViolinPlot.d.ts +1 -0
  25. package/dist/components/charts/shared/colorManipulation.d.ts +15 -0
  26. package/dist/components/charts/shared/formatUtils.d.ts +28 -0
  27. package/dist/components/charts/shared/hatchPattern.d.ts +35 -0
  28. package/dist/components/charts/shared/hooks.d.ts +16 -1
  29. package/dist/components/charts/shared/legendUtils.d.ts +2 -1
  30. package/dist/components/charts/shared/selectionUtils.d.ts +11 -0
  31. package/dist/components/charts/shared/statisticalOverlays.d.ts +49 -5
  32. package/dist/components/charts/shared/types.d.ts +4 -1
  33. package/dist/components/charts/xy/Heatmap.d.ts +1 -1
  34. package/dist/components/charts/xy/MultiAxisLineChart.d.ts +71 -0
  35. package/dist/components/realtime/types.d.ts +2 -0
  36. package/dist/components/semiotic-ai.d.ts +3 -0
  37. package/dist/components/semiotic-ordinal.d.ts +3 -0
  38. package/dist/components/semiotic-themes.d.ts +64 -0
  39. package/dist/components/semiotic-xy.d.ts +1 -0
  40. package/dist/components/semiotic.d.ts +11 -5
  41. package/dist/components/store/ThemeStore.d.ts +22 -2
  42. package/dist/components/stream/OrdinalSVGOverlay.d.ts +1 -0
  43. package/dist/components/stream/PipelineStore.d.ts +2 -0
  44. package/dist/components/stream/SVGOverlay.d.ts +5 -3
  45. package/dist/components/stream/accessorUtils.d.ts +14 -0
  46. package/dist/components/stream/networkTypes.d.ts +2 -0
  47. package/dist/components/stream/ordinalSceneBuilders/barFunnelScene.d.ts +27 -0
  48. package/dist/components/stream/ordinalSceneBuilders/funnelScene.d.ts +26 -0
  49. package/dist/components/stream/ordinalTypes.d.ts +16 -2
  50. package/dist/components/stream/renderers/barFunnelCanvasRenderer.d.ts +12 -0
  51. package/dist/components/stream/renderers/trapezoidCanvasRenderer.d.ts +15 -0
  52. package/dist/components/stream/sceneUtils.d.ts +10 -0
  53. package/dist/components/stream/types.d.ts +10 -3
  54. package/dist/geo.min.js +1 -1
  55. package/dist/geo.module.min.js +1 -1
  56. package/dist/network.min.js +1 -1
  57. package/dist/network.module.min.js +1 -1
  58. package/dist/ordinal.min.js +1 -1
  59. package/dist/ordinal.module.min.js +1 -1
  60. package/dist/realtime.min.js +1 -1
  61. package/dist/realtime.module.min.js +1 -1
  62. package/dist/semiotic-ai-statisticalOverlays-C2PPlmXv.js +1 -0
  63. package/dist/semiotic-ai.d.ts +3 -0
  64. package/dist/semiotic-ai.min.js +1 -1
  65. package/dist/semiotic-ai.module.min.js +1 -1
  66. package/dist/semiotic-ordinal.d.ts +3 -0
  67. package/dist/semiotic-statisticalOverlays-D8LhSbQt.js +1 -0
  68. package/dist/semiotic-themes.d.ts +64 -0
  69. package/dist/semiotic-themes.min.js +1 -0
  70. package/dist/semiotic-themes.module.min.js +1 -0
  71. package/dist/semiotic-xy.d.ts +1 -0
  72. package/dist/semiotic.d.ts +11 -5
  73. package/dist/semiotic.min.js +1 -1
  74. package/dist/semiotic.module.min.js +1 -1
  75. package/dist/server.min.js +1 -1
  76. package/dist/server.module.min.js +1 -1
  77. package/dist/xy-statisticalOverlays-C2PPlmXv.js +1 -0
  78. package/dist/xy.min.js +1 -1
  79. package/dist/xy.module.min.js +1 -1
  80. package/package.json +24 -5
  81. package/dist/semiotic-ai-statisticalOverlays-C1f7TYyD.js +0 -1
  82. package/dist/semiotic-statisticalOverlays-C1f7TYyD.js +0 -1
  83. package/dist/xy-statisticalOverlays-C1f7TYyD.js +0 -1
package/CLAUDE.md CHANGED
@@ -2,49 +2,32 @@
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`
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`, `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`, `chartId`, `loading` (false), `emptyContent`, `legendInteraction` ("none"|"highlight"|"isolate"), `legendPosition` ("right"|"left"|"top"|"bottom"), `emphasis` ("primary"|"secondary"), `annotations` (array)
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.
20
-
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).
18
+ `onObservation` receives `{ type: "hover"|"hover-end"|"click"|"brush"|"selection", datum?, x?, y?, timestamp, chartType, chartId }`. The `datum` is your original data object.
30
19
 
31
20
  ## XY Charts (`semiotic/xy`)
32
21
 
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
-
22
+ **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
23
  **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
-
24
+ **StackedAreaChart** — flat array + `areaBy` (required), `colorBy`, `normalize`. Do NOT use `lineBy` or `lineDataAccessor`.
39
25
  **Scatterplot** — `data`, `xAccessor`, `yAccessor`, `colorBy`, `sizeBy`, `sizeRange`, `pointRadius` (5), `pointOpacity` (0.8), `marginalGraphics`
40
-
41
26
  **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.
27
+ **ConnectedScatterplot** — `data`, `xAccessor`, `yAccessor`, `orderAccessor` (sequencing field), `pointRadius` (4)
28
+ **QuadrantChart** — Scatterplot + `quadrants` (required: `{ topRight, topLeft, bottomRight, bottomLeft }` each `{ label, color, opacity? }`), `xCenter`, `yCenter`, `centerlineStyle`, `showQuadrantLabels` (true). Supports push API.
29
+ **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.
30
+ **Heatmap** — `data`, `xAccessor`, `yAccessor`, `valueAccessor`, `colorScheme` ("blues"|"reds"|"greens"|"viridis"), `showValues`, `cellBorderColor`. Supports string/categorical axes.
48
31
 
49
32
  ## Ordinal Charts (`semiotic/ordinal`)
50
33
 
@@ -53,284 +36,219 @@ The `datum` field contains your original data object (not a wrapper).
53
36
  **GroupedBarChart** — + `groupBy` (required), `barPadding` (60)
54
37
  **SwarmPlot** — `data`, `categoryAccessor`, `valueAccessor`, `colorBy`, `sizeBy`, `pointRadius`, `pointOpacity`
55
38
  **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.
39
+ **Histogram** — + `bins` (25), `relative`. Always horizontal. `categoryAccessor` optional (defaults to `"category"`).
57
40
  **ViolinPlot** — + `bins`, `curve`, `showIQR`
41
+ **RidgelinePlot** — + `bins`, `amplitude` (1.5, unitless multiplier of lane width)
58
42
  **DotPlot** — + `sort` (true), `dotRadius`, `showGrid` default true
59
43
  **PieChart** — `data`, `categoryAccessor`, `valueAccessor`, `colorBy`, `startAngle`, `slicePadding`
60
- **DonutChart** — PieChart + `innerRadius` (60), `centerContent` (ReactNode — any React element, e.g. `<div>50%</div>`)
44
+ **DonutChart** — PieChart + `innerRadius` (60), `centerContent` (ReactNode)
45
+ **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).
46
+
47
+ All ordinal HOCs support `colorBy` and `colorScheme`. `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
48
 
62
49
  ## Network Charts (`semiotic/network`)
63
50
 
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`
51
+ **ForceDirectedGraph** — `nodes`, `edges`, `nodeIDAccessor`, `sourceAccessor`, `targetAccessor`, `colorBy`, `colorScheme`, `nodeSize` (number|string|fn), `nodeSizeRange`, `edgeWidth`, `edgeColor`, `edgeOpacity`, `iterations` (300), `forceStrength` (0.1), `showLabels`, `nodeLabel`, `legendInteraction`
52
+ **SankeyDiagram** — `edges`, `nodes`, `valueAccessor`, `nodeIdAccessor` ("id"), `sourceAccessor` ("source"), `targetAccessor` ("target"), `colorBy`, `edgeColorBy` ("source"|"target"|"gradient"|fn), `orientation`, `nodeAlign`, `nodeWidth`, `nodePaddingRatio`, `nodeLabel`, `showLabels`, `edgeOpacity`
66
53
  **ChordDiagram** — `edges`, `nodes`, `valueAccessor`, `edgeColorBy`, `padAngle`, `groupWidth`, `showLabels`
67
54
  **TreeDiagram** — `data` (root), `layout`, `orientation`, `childrenAccessor`, `colorBy`, `colorByDepth`, `edgeStyle`
68
55
  **Treemap** — `data` (root), `childrenAccessor`, `valueAccessor`, `colorBy`, `colorByDepth`, `showLabels`, `labelMode`
69
56
  **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.
57
+ **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
58
 
72
59
  ## Geo Charts (`semiotic/geo`)
73
60
 
74
- Geographic visualization with d3-geo projections. Canvas-rendered via `StreamGeoFrame`. Import from `semiotic/geo` to avoid adding d3-geo to non-geo bundles.
61
+ Import from `semiotic/geo` NOT `semiotic` to avoid pulling d3-geo into non-geo bundles.
75
62
 
76
- **ChoroplethMap** — `areas` (GeoJSON Feature[] or reference string like "world-110m"), `valueAccessor`, `colorScheme` ("blues"|"reds"|"greens"|"viridis"), `areaOpacity` (1), `projection` ("equalEarth"), `graticule`, `tooltip`, `showLegend`
63
+ **ChoroplethMap** — `areas` (GeoJSON Feature[] or "world-110m"), `valueAccessor`, `colorScheme`, `areaOpacity` (1), `projection` ("equalEarth"), `graticule`, `tooltip`, `showLegend`
77
64
  **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()`.
65
+ **FlowMap** — `flows`, `nodes`, `xAccessor`, `yAccessor`, `nodeIdAccessor`, `valueAccessor`, `edgeColorBy`, `edgeOpacity` (0.6), `edgeWidthRange` ([1,8]), `lineType` ("geo"|"line"), `showParticles`, `particleStyle`
66
+ **DistanceCartogram** — `points`, `center` (id), `costAccessor`, `strength` (0-1), `lineMode`, `showRings` (true|false|number[]), `ringStyle`, `showNorth`, `costLabel`, `transition`, `pointRadius`
84
67
 
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.
68
+ 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`
86
69
 
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.
88
-
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.
70
+ **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.
71
+ **Zoom**: Imperative: `ref.current.getZoom()`, `ref.current.resetZoom()`.
72
+ **Reference geography**: `resolveReferenceGeography("world-110m"|"world-50m"|"land-110m"|"land-50m")` returns GeoJSON features.
73
+ **mergeData(features, data, { featureKey, dataKey })** — join data into GeoJSON by key. World-atlas uses ISO numeric codes as `id`.
96
74
 
97
75
  ```jsx
98
- // World choropleth with reference geography + data joining
99
76
  import { ChoroplethMap, resolveReferenceGeography, mergeData } from "semiotic/geo"
100
77
  const world = await resolveReferenceGeography("world-110m")
101
78
  const areas = mergeData(world, gdpData, { featureKey: "id", dataKey: "id" })
102
79
  <ChoroplethMap areas={areas} valueAccessor="gdpPerCapita" colorScheme="viridis"
103
80
  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
81
  ```
130
82
 
83
+ **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`.
84
+
131
85
  ## Realtime Charts (`semiotic/realtime`)
132
86
 
133
87
  Push API: `chartRef.current.push({ time, value })`
134
88
 
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.
89
+ **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
90
 
137
- Sizing: all Realtime HOCs accept both `size={[600, 400]}` (tuple) and `width={600} height={400}`. Either works.
91
+ **RealtimeLineChart** `timeAccessor` ("time"), `valueAccessor` ("value"), `windowSize` (200), `windowMode`, `stroke`, `strokeWidth`
92
+ **RealtimeHistogram** — `binSize` (required), `timeAccessor`, `valueAccessor`, `categoryAccessor`, `colors`
93
+ **RealtimeSwarmChart** — `timeAccessor`, `valueAccessor`, `categoryAccessor`, `radius`, `opacity`
94
+ **RealtimeWaterfallChart** — `timeAccessor`, `valueAccessor`, `positiveColor`, `negativeColor`
95
+ **RealtimeHeatmap** — `timeAccessor`, `valueAccessor`, `heatmapXBins`, `heatmapYBins`, `aggregation`
96
+ **Streaming Sankey** — `StreamNetworkFrame` with `chartType="sankey"`, `showParticles`, `particleStyle`. Push individual edges: `ref.current.push({ source, target, value })`.
138
97
 
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.
98
+ Encoding: `decay`, `pulse`, `transition`, `staleness` compose freely on all streaming charts.
145
99
 
146
- Realtime encoding: `decay`, `pulse`, `transition`, `staleness` — compose freely on all streaming charts.
147
-
148
- ### Realtime data shape
100
+ ### Push API on HOC charts
101
+ Most HOC charts support push via `forwardRef`. **Omit** `data`/`nodes`/`edges` — do NOT pass `data={[]}`.
149
102
  ```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)
103
+ const ref = useRef()
104
+ ref.current.push({ x: 1, y: 2 }) // single
105
+ ref.current.pushMany([...points]) // batch
106
+ ref.current.clear() // reset
107
+ ref.current.getData() // read
108
+ <Scatterplot ref={ref} xAccessor="x" yAccessor="y" />
154
109
  ```
110
+ Supported: all XY, ordinal, network (Force, Sankey, Chord), geo point charts. **Not supported**: hierarchy charts (Tree, Treemap, CirclePack, Orbit), ChoroplethMap, FlowMap, ScatterplotMatrix.
155
111
 
156
- ### Push API on HOC charts
157
- Many HOC charts support the push API via `forwardRef`. Omit the `data` prop and push data imperatively:
112
+ ## Stream Frame Callbacks (advanced)
113
+ Frame callbacks (`nodeStyle`, `edgeStyle`, `nodeSize` as fn) receive `RealtimeNode`/`RealtimeEdge` wrappers. Access original data via `.data`:
158
114
  ```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" />
115
+ // WRONG: nodeSize={(d) => d.weight} — d.weight is undefined
116
+ // RIGHT: nodeSize={(d) => d.data?.weight} or use string: nodeSize="weight"
165
117
  ```
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.
118
+ Same applies to `frameProps` style functions on HOCs. `customHoverBehavior`/`customClickBehavior` receive `{ type, data, x, y } | null`. `tooltipContent` receives `{ type, data }`.
169
119
 
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`:
120
+ ## Hover Indicator
121
+ The hover dot automatically matches the hovered element's color (line stroke, point fill, etc.). Override via `frameProps`:
172
122
  ```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
123
+ <LineChart frameProps={{ hoverAnnotation: { pointColor: "#ff0000" } }} />
178
124
  ```
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> }`.
125
+ Fallback chain: `pointColor` element color `--semiotic-primary` CSS var `#007bff`.
181
126
 
182
127
  ## Coordinated Views
183
128
 
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`
129
+ **LinkedCharts** — `selections` (resolution: "union"|"intersect"|"crossfilter"), `showLegend`, `legendPosition`, `legendInteraction`, `legendSelectionName`, `legendField`
130
+ **CategoryColorProvider** — `colors` (map) or `categories` + `colorScheme`
186
131
  Chart props: `selection`, `linkedHover`, `linkedBrush`. Hooks: `useSelection`, `useLinkedHover`, `useBrushSelection`, `useFilteredData`
187
132
  **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
-
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)
133
+ **ChartContainer** — `title`, `subtitle`, `height` (400), `width` ("100%"), `status`, `loading`, `error`, `errorBoundary`, `actions` ({ export, fullscreen, copyConfig }), `controls`
134
+ **ChartGrid** — `columns` (number|"auto"), `minCellWidth` (300), `gap` (16). `emphasis="primary"` spans two columns.
135
+ **ContextLayout** — `context` (ReactNode), `position`, `contextSize` (250)
199
136
 
200
137
  ## Key Patterns
201
138
 
202
139
  ```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
140
+ // Cross-highlighting dashboard
221
141
  <CategoryColorProvider categories={["North", "South", "East"]}>
222
142
  <LinkedCharts>
223
143
  <ChartGrid columns={2}>
224
144
  <LineChart data={d} colorBy="region" linkedHover={{ name: "hl", fields: ["region"] }} selection={{ name: "hl" }} emphasis="primary" responsiveWidth />
225
145
  <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
146
  </ChartGrid>
228
147
  </LinkedCharts>
229
148
  </CategoryColorProvider>
230
149
 
231
- // Forecast + anomaly (auto)
150
+ // Forecast + anomaly
232
151
  <LineChart data={ts} xAccessor="time" yAccessor="value"
233
152
  forecast={{ trainEnd: 60, steps: 15, confidence: 0.95 }}
234
153
  anomaly={{ threshold: 2 }} />
235
154
 
236
- // Forecast (pre-computed ML bounds)
155
+ // Pre-computed forecast bounds
237
156
  <LineChart data={ml} xAccessor="time" yAccessor="value"
238
157
  forecast={{ isTraining: "isTraining", isForecast: "isForecast", isAnomaly: "isAnomaly", upperBounds: "upper", lowerBounds: "lower" }} />
239
158
 
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:
159
+ // Percentile band layer AreaChart + LineChart
247
160
  <>
248
161
  <AreaChart data={d} xAccessor="x" yAccessor="p95" y0Accessor="p5"
249
162
  showLine={false} areaOpacity={0.3} gradientFill />
250
163
  <LineChart data={d} xAccessor="x" yAccessor="p50" lineWidth={2} />
251
164
  </>
252
- // Simple gradient area (no band):
253
- <AreaChart data={d} xAccessor="x" yAccessor="y" gradientFill />
254
-
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
165
 
265
- // Streaming sankey with particles — push individual edges, NOT full snapshots
166
+ // Streaming sankey with particles
266
167
  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
168
+ sankeyRef.current.push({ source: "Web", target: "API", value: 1 })
169
+ <StreamNetworkFrame ref={sankeyRef} chartType="sankey"
170
+ showParticles particleStyle={{ radius: 2, colorBy: "source" }}
171
+ width={600} height={400} />
172
+
173
+ // SSR
281
174
  import { renderOrdinalToStaticSVG } from "semiotic/server"
282
- const svg = renderOrdinalToStaticSVG({
283
- data, categoryAccessor: "category", valueAccessor: "value", width: 600, height: 400
284
- })
175
+ const svg = renderOrdinalToStaticSVG({ data, categoryAccessor: "cat", valueAccessor: "val", width: 600, height: 400 })
285
176
  ```
286
177
 
287
178
  ## 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.
179
+
180
+ All HOCs accept `annotations` (array). Coordinates use your data field names. Network/orbit use `nodeId`.
181
+
182
+ **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`
183
+ **Reference lines**: `y-threshold` (`value`, `label`, `color`), `x-threshold`, `band` (`y0`, `y1`, `label`, `fill`)
184
+ **Enclosures**: `enclose` (circle around `coordinates`), `rect-enclose`, `highlight` (`filter` fn or `field`+`value`)
185
+ **Statistical** (XY): `trend` (`method`: linear/polynomial/loess), `envelope`, `anomaly-band`, `forecast`
186
+ **Streaming anchors**: `"fixed"` (default), `"latest"` (tracks newest datum), `"sticky"` (freezes when evicted)
187
+
188
+ Custom rendering: `frameProps.svgAnnotationRules = (annotation, index, context) => ReactNode | null`. Context has `scales`, `width`, `height`, `data`. Colors inherit from theme (`--semiotic-primary`, `--semiotic-text-secondary`).
189
+
289
190
  ```jsx
290
- annotations={[{ type: "widget", month: 4, revenue: 32, dy: -4, content: <MyAlertButton /> }]}
291
- // OrbitDiagram: annotations={[{ type: "widget", nodeId: "Pipeline", content: <Alert /> }]}
191
+ <LineChart data={data} xAccessor="time" yAccessor="latency"
192
+ annotations={[
193
+ { type: "y-threshold", value: 200, label: "SLA limit", color: "#e45050" },
194
+ { type: "widget", time: 42, latency: 850, dy: -30, content: <span>Incident</span> },
195
+ ]} />
292
196
  ```
293
197
 
294
- ## 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`.
198
+ ## Theming
301
199
 
302
- ## 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)
200
+ 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`.
312
201
 
313
- ## Known Pitfalls
202
+ ```jsx
203
+ import { ThemeProvider } from "semiotic"
204
+ <ThemeProvider theme="tufte"> {/* Named preset */}
205
+ <ThemeProvider theme={{ colors: { primary: "#ff6b6b", categorical: [...] } }}> {/* Custom */}
206
+ ```
314
207
 
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.
208
+ 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`.
316
209
 
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.
210
+ Serialization (`semiotic/themes`): `themeToCSS(theme, selector)`, `themeToTokens(theme)`, `resolveThemePreset(name)`.
211
+ Color-blind palette: `import { COLOR_BLIND_SAFE_CATEGORICAL } from "semiotic"` (8-color Wong 2011).
318
212
 
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.
213
+ Key: `ThemeProvider` sets CSS vars on a wrapper div (no React context). Canvas charts read vars via `getComputedStyle`. `exportChart` inlines computed styles.
320
214
 
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".
215
+ **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.
322
216
 
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.
217
+ **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).
324
218
 
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.
219
+ ## Server-Side Rendering
220
+ - HOC charts and Frames render SVG automatically in server environments
221
+ - `renderXYToStaticSVG(props)`, `renderOrdinalToStaticSVG(props)`, `renderNetworkToStaticSVG(props)`, `renderGeoToStaticSVG(props)` from `semiotic/server`
222
+ - `frameType` is `"xy"|"ordinal"|"network"|"geo"` (NOT component names)
223
+ - Geo SSR requires pre-resolved features (synchronous — call `resolveReferenceGeography` first)
224
+ - Works with Next.js App Router, Remix, Astro
225
+
226
+ ## AI Features
227
+ - `onObservation` / `useChartObserver` — structured events across charts
228
+ - `toConfig`/`fromConfig`/`toURL`/`fromURL`/`copyConfig`/`configToJSX` — serialization
229
+ - `DetailsPanel` — click-driven detail panel in `ChartContainer`
230
+ - `validateProps(componentName, props)` — prop validation with typo suggestions
231
+ - `diagnoseConfig(componentName, props)` — anti-pattern detector (13+ checks)
232
+ - `exportChart(containerDiv, { format: "png"|"svg" })` — pass wrapper div, composites canvas+SVG
233
+ - `npx semiotic-ai --doctor` — CLI validation
326
234
 
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.
235
+ ## Canvas Pattern Fills
328
236
 
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.
237
+ `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.
238
+
239
+ ## Known Pitfalls
330
240
 
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.
241
+ - **Tooltip datum shape**: HOC tooltip functions get raw data. Frame `tooltipContent` gets wrapped data use `d.data`.
242
+ - **Legend positioning**: "bottom" auto-expands margin ~80px. For narrow charts (<400px), prefer "bottom" or "top".
243
+ - **MultiAxisLineChart legend**: Always use `legendPosition="bottom"` (or `"top"`) — the right-hand axis occupies the space where a right-side legend would go.
244
+ - **Log scale**: Clamps domain min to 1e-6 (log(0) undefined).
245
+ - **barPadding**: Pixel value, defaults 40/60. Reduce for small charts.
246
+ - **Horizontal bars**: Need wider left margin with long labels: `margin={{ left: 120 }}`.
247
+ - **LinkedCharts legends**: `CategoryColorProvider` suppresses child legends. Force with `showLegend={true}`.
248
+ - **Push API**: Omit `data` prop entirely. `data={[]}` clears pushed data every render.
249
+ - **frameProps style functions**: Bypass HOC color resolution — use `colorBy` prop instead. Frame style functions receive `(datum, categoryName)`, not `(datum, index)`.
250
+ - **v2 migration**: `htmlAnnotationRules` → `widget` annotations + `svgAnnotationRules`. v2 `summaryStyle` index-based coloring → v3 category-string-based.
332
251
 
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.
252
+ ## Performance
334
253
 
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
254
+ 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.