semiotic 3.5.1 → 3.5.2

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 (92) hide show
  1. package/CLAUDE.md +21 -19
  2. package/README.md +22 -16
  3. package/ai/chartSuggestions.cjs +191 -3
  4. package/ai/componentMetadata.cjs +3 -3
  5. package/ai/dist/mcp-server.js +266 -48
  6. package/ai/examples.md +68 -0
  7. package/ai/schema.json +900 -1
  8. package/ai/system-prompt.md +4 -1
  9. package/dist/components/Tooltip/FlippingTooltip.d.ts +16 -1
  10. package/dist/components/charts/geo/FlowMap.d.ts +13 -4
  11. package/dist/components/charts/index.d.ts +6 -0
  12. package/dist/components/charts/network/OrbitDiagram.d.ts +5 -5
  13. package/dist/components/charts/network/ProcessSankey.d.ts +141 -0
  14. package/dist/components/charts/network/processSankey/algorithm.d.ts +136 -0
  15. package/dist/components/charts/network/processSankey/buildScenes.d.ts +45 -0
  16. package/dist/components/charts/network/processSankey/ribbonInputs.d.ts +32 -0
  17. package/dist/components/charts/network/processSankey/streamingLayout.d.ts +58 -0
  18. package/dist/components/charts/network/processSankey/tooltipUtils.d.ts +41 -0
  19. package/dist/components/charts/ordinal/BarChart.d.ts +12 -0
  20. package/dist/components/charts/ordinal/DotPlot.d.ts +9 -0
  21. package/dist/components/charts/ordinal/GaugeChart.d.ts +20 -0
  22. package/dist/components/charts/ordinal/SwimlaneChart.d.ts +5 -0
  23. package/dist/components/charts/realtime/RealtimeHistogram.d.ts +14 -11
  24. package/dist/components/charts/realtime/defaultRealtimeTooltip.d.ts +19 -0
  25. package/dist/components/charts/shared/axisExtent.d.ts +59 -0
  26. package/dist/components/charts/shared/chartSpecs.d.ts +75 -0
  27. package/dist/components/charts/shared/colorUtils.d.ts +8 -2
  28. package/dist/components/charts/shared/networkUtils.d.ts +3 -5
  29. package/dist/components/charts/shared/radialGeometry.d.ts +99 -0
  30. package/dist/components/charts/shared/regressionUtils.d.ts +59 -0
  31. package/dist/components/charts/shared/selectionUtils.d.ts +8 -1
  32. package/dist/components/charts/shared/streamPropsHelpers.d.ts +5 -0
  33. package/dist/components/charts/shared/types.d.ts +13 -0
  34. package/dist/components/charts/shared/useAreaSeriesSetup.d.ts +75 -0
  35. package/dist/components/charts/shared/useEncodingDomain.d.ts +48 -0
  36. package/dist/components/charts/shared/useFrameImperativeHandle.d.ts +1 -1
  37. package/dist/components/charts/shared/useNetworkChartSetup.d.ts +148 -0
  38. package/dist/components/charts/shared/useOrdinalPieceStyle.d.ts +87 -0
  39. package/dist/components/charts/shared/useSeriesFeatures.d.ts +57 -0
  40. package/dist/components/charts/shared/useStreamStatus.d.ts +33 -0
  41. package/dist/components/charts/shared/useXYLineStyle.d.ts +69 -0
  42. package/dist/components/charts/shared/useXYPointStyle.d.ts +87 -0
  43. package/dist/components/charts/xy/AreaChart.d.ts +24 -0
  44. package/dist/components/charts/xy/BubbleChart.d.ts +9 -0
  45. package/dist/components/charts/xy/ConnectedScatterplot.d.ts +16 -0
  46. package/dist/components/charts/xy/DifferenceChart.d.ts +172 -0
  47. package/dist/components/charts/xy/Scatterplot.d.ts +34 -0
  48. package/dist/components/geometry/ribbonGeometry.d.ts +76 -0
  49. package/dist/components/semiotic-ai.d.ts +2 -0
  50. package/dist/components/semiotic-network.d.ts +4 -0
  51. package/dist/components/semiotic-realtime.d.ts +2 -0
  52. package/dist/components/semiotic-utils.d.ts +4 -0
  53. package/dist/components/semiotic-xy.d.ts +2 -0
  54. package/dist/components/semiotic.d.ts +3 -3
  55. package/dist/components/server/serverChartConfigs.d.ts +2 -0
  56. package/dist/components/stream/GeoPipelineStore.d.ts +21 -0
  57. package/dist/components/stream/OrdinalSVGOverlay.d.ts +8 -0
  58. package/dist/components/stream/PipelineStore.d.ts +5 -0
  59. package/dist/components/stream/SVGOverlay.d.ts +18 -0
  60. package/dist/components/stream/annotationAccessorResolver.d.ts +39 -0
  61. package/dist/components/stream/geoTypes.d.ts +12 -0
  62. package/dist/components/stream/ordinalTypes.d.ts +12 -0
  63. package/dist/components/stream/renderers/cornerRadii.d.ts +33 -0
  64. package/dist/components/stream/types.d.ts +23 -0
  65. package/dist/components/types/legendTypes.d.ts +1 -1
  66. package/dist/geo.min.js +1 -1
  67. package/dist/geo.module.min.js +1 -1
  68. package/dist/network.min.js +1 -1
  69. package/dist/network.module.min.js +1 -1
  70. package/dist/ordinal.min.js +1 -1
  71. package/dist/ordinal.module.min.js +1 -1
  72. package/dist/realtime.min.js +1 -1
  73. package/dist/realtime.module.min.js +1 -1
  74. package/dist/semiotic-ai.d.ts +2 -0
  75. package/dist/semiotic-ai.min.js +1 -1
  76. package/dist/semiotic-ai.module.min.js +1 -1
  77. package/dist/semiotic-network.d.ts +4 -0
  78. package/dist/semiotic-realtime.d.ts +2 -0
  79. package/dist/semiotic-recipes.min.js +1 -1
  80. package/dist/semiotic-recipes.module.min.js +1 -1
  81. package/dist/semiotic-utils.d.ts +4 -0
  82. package/dist/semiotic-utils.min.js +1 -1
  83. package/dist/semiotic-utils.module.min.js +1 -1
  84. package/dist/semiotic-xy.d.ts +2 -0
  85. package/dist/semiotic.d.ts +3 -3
  86. package/dist/semiotic.min.js +1 -1
  87. package/dist/semiotic.module.min.js +1 -1
  88. package/dist/server.min.js +1 -1
  89. package/dist/server.module.min.js +1 -1
  90. package/dist/xy.min.js +1 -1
  91. package/dist/xy.module.min.js +1 -1
  92. package/package.json +10 -5
package/CLAUDE.md CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  ## Quick Start
4
4
  - Install: `npm install semiotic`
5
- - **Use sub-path imports** — `semiotic/xy` (77KB gz), `semiotic/ordinal` (64KB), `semiotic/network` (51KB), `semiotic/geo` (49KB), `semiotic/realtime` (84KB), `semiotic/server` (64KB), `semiotic/recipes` (4KB), `semiotic/utils` (20KB), `semiotic/themes` (4KB), `semiotic/data` (3KB). Full `semiotic` is 165KB gz.
5
+ <!-- semiotic-bundle-sizes:start -->
6
+ <!-- Auto-generated by scripts/sync-bundle-sizes.mjs — do not edit by hand. -->
7
+ - **Use sub-path imports** — `semiotic/xy` (83KB gz), `semiotic/ordinal` (67KB gz), `semiotic/network` (62KB gz), `semiotic/geo` (50KB gz), `semiotic/realtime` (88KB gz), `semiotic/server` (114KB gz), `semiotic/utils` (22KB gz), `semiotic/recipes` (5KB gz), `semiotic/themes` (4KB gz), `semiotic/data` (3KB gz), `semiotic/ai` (184KB gz). Full `semiotic` is 183KB gz.
8
+ <!-- semiotic-bundle-sizes:end -->
6
9
  - CLI: `npx semiotic-ai [--schema|--compact|--examples|--doctor]`
7
10
  - MCP: `npx semiotic-mcp`
8
11
 
@@ -12,7 +15,7 @@
12
15
  - Every HOC accepts `frameProps` for pass-through. TypeScript `strict: true`. Every HOC has error boundary + dev-mode validation.
13
16
 
14
17
  ## Common Props (all HOCs)
15
- `title`, `description` (aria-label), `summary` (sr-only), `width` (600), `height` (400), `responsiveWidth`, `responsiveHeight`, `margin`, `className`, `color` (uniform fill), `stroke` (uniform stroke color — CSS var OK), `strokeWidth` (uniform stroke width in px), `opacity` (uniform 0–1 opacity), `enableHover` (true), `tooltip` (boolean | "multi" | function | config object), `showLegend`, `showGrid` (false), `frameProps`, `onObservation`, `onClick`, `chartId`, `loading` (false), `emptyContent`, `legendInteraction` ("none"|"highlight"|"isolate"), `legendPosition` ("right"|"left"|"top"|"bottom"), `emphasis` ("primary"|"secondary"), `annotations` (array), `accessibleTable` (true), `hoverHighlight` (boolean — dims non-hovered series, requires `colorBy`), `hoverRadius` (30), `animate` (boolean | { duration?, easing?, intro? } — animated intro on first render + smooth transitions on data change; intro defaults to true when animate is enabled)
18
+ `title`, `description` (aria-label), `summary` (sr-only), `width` (600), `height` (400), `responsiveWidth`, `responsiveHeight`, `margin`, `className`, `color` (uniform fill), `stroke` (uniform stroke color — CSS var OK), `strokeWidth` (uniform stroke width in px), `opacity` (uniform 0–1 opacity), `enableHover` (true), `tooltip` (boolean | "multi" | function | config object), `showLegend`, `showGrid` (false), `frameProps`, `onObservation`, `onClick`, `chartId`, `loading` (false), `emptyContent`, `legendInteraction` ("none"|"highlight"|"isolate"), `legendPosition` ("right"|"left"|"top"|"bottom"), `emphasis` ("primary"|"secondary"), `annotations` (array), `accessibleTable` (true), `hoverHighlight` (boolean — dims non-hovered series, requires `colorBy`), `hoverRadius` (30), `animate` (boolean | { duration?, easing?, intro? } — animated intro on first render + smooth transitions on data change; intro defaults to true when animate is enabled), `axisExtent` ("nice" default | "exact" — pins first/last tick to actual data min/max with equidistant intermediates. Applies to XY x/y axes and ordinal value axis only; no-op on network/geo/hierarchy. Explicit `tickValues` still wins.)
16
19
 
17
20
  **Primitive styling props** (`color`, `stroke`, `strokeWidth`, `opacity`) apply to any shape the chart draws (bars, circles, lines, wedges, rects). Precedence: top-level prop > `frameProps.*Style` function return > HOC base > theme fallback. Use CSS variables (`stroke="var(--semiotic-border)"`) for theme-aware, cascade-overridable styling. For per-datum customization, keep using the function-form `frameProps.pieceStyle` / `pointStyle` / `lineStyle` etc. — the top-level prop overlays on top of whatever the function returns.
18
21
 
@@ -22,10 +25,11 @@
22
25
 
23
26
  **LineChart** — `data`, `xAccessor` ("x"), `yAccessor` ("y"), `lineBy`, `lineDataAccessor`, `colorBy`, `colorScheme`, `curve`, `lineWidth` (2), `showPoints`, `pointRadius` (3), `fillArea` (boolean|string[]), `areaOpacity` (0.3), `lineGradient`, `anomaly`, `forecast`, `directLabel`, `gapStrategy`, `xScaleType`/`yScaleType` ("linear"|"log"|"time"), `tooltip="multi"` for hover-anywhere series comparison
24
27
  **AreaChart** — LineChart props + `areaBy`, `y0Accessor`, `gradientFill`, `areaOpacity` (0.7), `showLine` (true), `tooltip="multi"` for hover-anywhere area comparison
25
- **StackedAreaChart** — flat array + `areaBy` (required), `colorBy`, `normalize`, `baseline` (`"zero"` default | `"wiggle"` for streamgraph | `"silhouette"` for centered), `stackOrder` (`"key"` default alpha | `"insideOut"` largest-in-middle | `"asc"`/`"desc"` by total). For canonical streamgraph aesthetic: `baseline="wiggle"` + `stackOrder="insideOut"` puts the largest series in the middle as a central anchor with smaller series wrapping outward. `baseline` is mutually exclusive with `normalize`. No `lineBy`/`lineDataAccessor`. Pass `tooltip="multi"` for hover-anywhere mode: a single tooltip lists every stacked series at the hovered x, with values interpolated between rendered path samples.
26
- **Scatterplot** — `data`, `xAccessor`, `yAccessor`, `colorBy`, `sizeBy`, `sizeRange`, `pointRadius` (5), `pointOpacity` (0.8), `marginalGraphics`
27
- **BubbleChart** — Scatterplot + `sizeBy` (required), `sizeRange` ([5,40])
28
- **ConnectedScatterplot** — + `orderAccessor`
28
+ **DifferenceChart** — Two-series A/B comparison. Fills the area between with `seriesAColor` where A > B and `seriesBColor` where B > A; crossovers interpolated. Props: `data`, `xAccessor` ("x"), `seriesAAccessor` ("a"), `seriesBAccessor` ("b"), `seriesALabel`/`seriesBLabel`, `seriesAColor` (var(--semiotic-danger))/`seriesBColor` (var(--semiotic-info)), `showLines` (true), `lineWidth` (1.5), `showPoints` (false), `pointRadius` (3), `curve` ("linear"), `areaOpacity` (0.6), `gradientFill`, `xExtent`/`yExtent`, `pointIdAccessor`, `windowSize` (max raw rows in push buffer; FIFO eviction). Push API via `ref.current.push({x, a, b})`. Accessor outputs coerce through `toNumber` so `Date` + numeric strings are accepted.
29
+ **StackedAreaChart** — flat array + `areaBy` (required), `colorBy`, `normalize`, `baseline` (`"zero"` default | `"wiggle"` streamgraph | `"silhouette"` centered), `stackOrder` (`"key"` alpha | `"insideOut"` largest-in-middle | `"asc"`/`"desc"`). Streamgraph: `baseline="wiggle"` + `stackOrder="insideOut"`. `baseline` ⊥ `normalize`. No `lineBy`/`lineDataAccessor`. `tooltip="multi"` lists every series at the hovered x (values interpolated between samples).
30
+ **Scatterplot** — `data`, `xAccessor`, `yAccessor`, `colorBy`, `sizeBy`, `sizeRange`, `pointRadius` (5), `pointOpacity` (0.8), `marginalGraphics`, `regression` (boolean | "linear" | "polynomial" | "loess" | RegressionConfig — sugar for a trend-annotation overlay; sits underneath user annotations)
31
+ **BubbleChart** — Scatterplot + `sizeBy` (required), `sizeRange` ([5,40]), `regression`
32
+ **ConnectedScatterplot** — + `orderAccessor`, `regression`
29
33
  **QuadrantChart** — Scatterplot + `quadrants` (required), `xCenter`, `yCenter`
30
34
  **MultiAxisLineChart** — Dual Y-axis. `series` (required: `[{ yAccessor, label?, color?, format?, extent? }]`). Falls back to multi-line if not 2 series.
31
35
  **Heatmap** — `data`, `xAccessor`, `yAccessor`, `valueAccessor`, `colorScheme`, `showValues`, `cellBorderColor`
@@ -35,7 +39,7 @@
35
39
 
36
40
  ## Ordinal Charts (`semiotic/ordinal`)
37
41
 
38
- **BarChart** — `data`, `categoryAccessor`, `valueAccessor`, `orientation`, `colorBy`, `sort`, `barPadding` (40), `roundedTop`, `gradientFill` (`true` | `{topOpacity, bottomOpacity}` | `{colorStops}` — same API as AreaChart; runs tip→base)
42
+ **BarChart** — `data`, `categoryAccessor`, `valueAccessor`, `orientation`, `colorBy`, `sort`, `barPadding` (40), `roundedTop`, `gradientFill` (`true` | `{topOpacity, bottomOpacity}` | `{colorStops}` — same API as AreaChart; runs tip→base), `regression` (boolean | "linear" | "polynomial" | "loess" | RegressionConfig — sugar for a trend-annotation overlay; categories regressed as category-index, line projects through band scale)
39
43
  **StackedBarChart** — + `stackBy` (required), `normalize`, `sort` (default false — insertion order)
40
44
  **GroupedBarChart** — + `groupBy` (required), `barPadding` (60), `sort` (default false — insertion order)
41
45
  **SwarmPlot** — `colorBy`, `sizeBy`, `pointRadius`, `pointOpacity`
@@ -43,13 +47,13 @@
43
47
  **Histogram** — + `bins` (25), `relative`. Always horizontal.
44
48
  **ViolinPlot** — + `bins`, `curve`, `showIQR`
45
49
  **RidgelinePlot** — + `bins`, `amplitude` (1.5)
46
- **DotPlot** — + `sort` ("auto" — insertion order when streaming, value-desc on static), `dotRadius`, `showGrid` default true
50
+ **DotPlot** — + `sort` ("auto" — insertion order when streaming, value-desc on static), `dotRadius`, `showGrid` default true, `regression`
47
51
  **PieChart** — `categoryAccessor`, `valueAccessor`, `colorBy`, `startAngle`
48
52
  **DonutChart** — PieChart + `innerRadius` (60), `centerContent`
49
53
  **FunnelChart** — `stepAccessor`, `valueAccessor`, `categoryAccessor` (optional), `connectorOpacity`, `orientation`
50
- **SwimlaneChart** — `categoryAccessor`, `subcategoryAccessor` (required), `valueAccessor`, `colorBy` (defaults to subcategoryAccessor), `orientation`
54
+ **SwimlaneChart** — `categoryAccessor`, `subcategoryAccessor` (required), `valueAccessor`, `colorBy` (defaults to subcategoryAccessor), `orientation`, `roundedTop` (pixel radius applied to both outer ends of each lane — left+right for horizontal, top+bottom for vertical. Middle segments stay square so adjacent pieces butt against each other; single-segment lanes round all four corners.)
51
55
  **LikertChart** — `categoryAccessor`, `valueAccessor`|`levelAccessor`+`countAccessor`, `levels` (required), `orientation`, `colorScheme`
52
- **GaugeChart** — `value` (required), `min`, `max`, `thresholds`, `arcWidth`, `sweep`, `fillZones`, `showNeedle`, `centerContent`
56
+ **GaugeChart** — `value` (required), `min`, `max`, `thresholds`, `arcWidth`, `cornerRadius` (pixel radius for rounded segment ends — same semantics as DonutChart), `sweep`, `fillZones`, `showNeedle`, `centerContent`
53
57
 
54
58
  All ordinal: `colorBy`, `colorScheme`, `categoryFormat` (string|ReactNode), `showCategoryTicks` (true).
55
59
 
@@ -57,6 +61,7 @@ All ordinal: `colorBy`, `colorScheme`, `categoryFormat` (string|ReactNode), `sho
57
61
 
58
62
  **ForceDirectedGraph** — `nodes`, `edges`, `nodeIDAccessor`, `sourceAccessor`, `targetAccessor`, `colorBy`, `nodeSize`, `nodeSizeRange`, `edgeWidth`, `iterations` (300), `forceStrength` (0.1), `showLabels`, `nodeLabel`
59
63
  **SankeyDiagram** — `edges`, `nodes`, `valueAccessor`, `nodeIdAccessor`, `colorBy`, `edgeColorBy`, `orientation`, `nodeAlign`, `nodeWidth`, `nodePaddingRatio`, `showLabels`
64
+ **ProcessSankey** — temporal sankey with a real time x-axis. `nodes`, `edges` (each with `startTime`/`endTime`), `domain` (required `[t0, t1]`), `axisTicks?`, `xExtentAccessor` (optional `[start, end]` lifetime per node — lane spans `min(xExtent[0], earliestEdge)` to `max(xExtent[1], latestEdge)`), `colorBy`/`colorScheme`/`showLegend`/`legendPosition`, `pairing` ("value"|"temporal"), `packing` ("off"|"reuse"), `laneOrder` ("crossing-min"|"inside-out"|"crossing-min+inside-out"|"insertion"), `lifetimeMode` ("full"|"half"), `ribbonLane` ("source"|"target"|"both"), `showLaneRails`, `showQualityReadout`, `showParticles` + `particleStyle` (same shape as SankeyDiagram, canvas + ParticlePool), `timeFormat`/`valueFormat`, push API via ref. Static-graph cycles are valid as long as edges move forward in time. Use for time-stamped flow events; use `SankeyDiagram` for static total-flow snapshots.
60
65
  **ChordDiagram** — `edges`, `nodes`, `valueAccessor`, `edgeColorBy`, `padAngle`, `showLabels`
61
66
  **TreeDiagram** — `data` (root), `layout`, `orientation`, `childrenAccessor`, `colorBy`, `colorByDepth`
62
67
  **Treemap** — `data` (root), `childrenAccessor`, `valueAccessor`, `colorBy`, `colorByDepth`, `showLabels`
@@ -131,7 +136,7 @@ import {
131
136
  <NetworkCustomChart nodes={nodes} edges={edges} layout={flextreeLayout} layoutConfig={{ ... }} />
132
137
  ```
133
138
 
134
- **Recipes subpath** (`semiotic/recipes`, 4KB gz) ships pure layout functions. They emit standard SceneNodes — no chart code. BYO heavy deps (`d3-flextree`, `dagre`) live in user code.
139
+ **Recipes subpath** (`semiotic/recipes`) ships pure layout functions. They emit standard SceneNodes — no chart code. BYO heavy deps (`d3-flextree`, `dagre`) live in user code.
135
140
 
136
141
  ### Chrome (labels, axes, legends): the recipe owns it
137
142
 
@@ -170,11 +175,9 @@ const [hovered, setHovered] = useState(null)
170
175
  />
171
176
  ```
172
177
 
173
- The same predicate hook is the integration point for richer interactions on the roadmap: a future `<ParallelCoordinatesBrushes>` overlay that drag-brushes per-axis ranges, and `useBrushSelection`-style linked brushing across coordinated charts (selection store would carry per-field range constraints, downstream charts consume them via the same hook).
174
-
175
178
  ### Built-in chrome works when the layout uses standard scales
176
179
 
177
- Layouts that draw *within* the frame's standard scales can just use the HOC's chrome. Pass `showAxes` on `XYCustomChart` / `OrdinalCustomChart` and the frame will render the same x/y or o/r axes the built-in HOCs do — useful for custom XY layouts that overlay points/lines on regular axes. The recipe-managed chrome is for the cases where standard axes can't help (variable-width bars under a band scale, per-row independent scales, etc.).
180
+ Pass `showAxes` on `XYCustomChart` / `OrdinalCustomChart` to get the same x/y or o/r axes the built-in HOCs render — useful for custom layouts that overlay points/lines on regular scales. Recipe-managed chrome is for cases where standard axes don't fit (variable-width bars under a band scale, per-row independent scales, etc.).
178
181
 
179
182
  **Notes:**
180
183
  - Coords are plot-relative (the frame translates the canvas/SVG group by `margin`). Read `ctx.dimensions.plot` for the drawing rect. Radial ordinal projection is the one exception: `plot.x = -width/2`, `plot.y = -height/2` because the canvas ctx is center-translated.
@@ -219,7 +222,7 @@ Server SVGs include `role="img"`, `<title>`, `<desc>`, grid, legend, annotations
219
222
  - **GroupedBarChart** — `data`, `categoryAccessor`, `valueAccessor`, `groupBy` (required).
220
223
  - **PieChart/DonutChart** — `data`, `categoryAccessor`, `valueAccessor`.
221
224
  - **FunnelChart** — `data`, `stepAccessor` ("step"), `valueAccessor` ("value"). Renders with trapezoid connectors, no axes.
222
- - **GaugeChart** — `value`. Optional: `thresholds` (array of `{value, color, label}`), `min`, `max`, `sweep`, `arcWidth`.
225
+ - **GaugeChart** — `value`. Optional: `thresholds` (array of `{value, color, label}`), `min`, `max`, `sweep`, `arcWidth`, `cornerRadius`.
223
226
  - **SwimlaneChart** — `data`, `categoryAccessor`, `subcategoryAccessor` (required), `valueAccessor`.
224
227
  - **ForceDirectedGraph** — `nodes`, `edges` (both required). If deriving nodes from edge endpoints, materialize `nodes` before returning JSX/renderChart props.
225
228
  - **SankeyDiagram** — `edges` (required), `valueAccessor`.
@@ -297,9 +300,8 @@ These rules are generated from `ai/behaviorContracts.cjs` and are consumed by `s
297
300
  - **Legend**: "bottom" expands margin ~80px. MultiAxisLineChart: use `legendPosition="bottom"`.
298
301
  - **Log scale**: Domain min clamped to 1e-6.
299
302
  - **barPadding**: Pixel value (40/60 default). Reduce for small charts.
300
- - **sort on StackedBarChart/GroupedBarChart**: Default `false` preserves insertion order. The underlying frame defaults to value-descending if `oSort` is undefined, so always pass `sort` explicitly if order matters.
301
- - **sort `"auto"`** (BarChart/StackedBarChart/GroupedBarChart/DotPlot): insertion order while streaming, value-desc on static data. The right choice when using the push APIavoids categories shuffling as values fluctuate. DotPlot's default; opt-in on others.
302
- - **Tooltip format cascade**: `valueFormat` (ordinal) / `xFormat` / `yFormat` (XY) / `valueFormat` on Heatmap flow to the default tooltip automatically, so axis and tooltip read identically. Only applies to the default tooltip — a custom `tooltip` prop fully overrides; re-pass the formatter inside `Tooltip({format})` / `MultiLineTooltip({fields:[{format}]})` if you want it there. Bespoke-tooltip charts (Histogram, FunnelChart, LikertChart, GaugeChart) don't participate; customize via `tooltip`.
303
+ - **sort** (BarChart/StackedBarChart/GroupedBarChart/DotPlot): `false` preserves insertion order; `"auto"` = insertion-order while streaming, value-desc on static (DotPlot default — opt-in on others, avoids category shuffling under the push API). StackedBar/GroupedBar default to `false`; the underlying frame value-sorts when `oSort` is undefined, so always pass `sort` explicitly if order matters.
304
+ - **Tooltip format cascade**: `valueFormat`/`xFormat`/`yFormat` flow to the default tooltip automatically, so axis and tooltip read identically. A custom `tooltip` prop fully overridesre-pass via `Tooltip({format})` / `MultiLineTooltip({fields:[{format}]})`. Bespoke-tooltip charts (Histogram, FunnelChart, LikertChart, GaugeChart) don't participate; customize via `tooltip`.
303
305
  - **Horizontal bars**: Need wider left margin: `margin={{ left: 120 }}`.
304
306
  - **Push API**: Omit `data` entirely. `data={[]}` clears on every render.
305
307
  - **frameProps style functions**: Bypass HOC color resolution — use `colorBy` prop instead.
@@ -312,7 +314,7 @@ These rules are generated from `ai/behaviorContracts.cjs` and are consumed by `s
312
314
  - **scalePadding**: Pixel inset on scale ranges. Pass via `frameProps={{ scalePadding: 12 }}`.
313
315
  - **categoryFormat/xFormat/yFormat**: Can return ReactNode (renders in `<foreignObject>`).
314
316
  - **Tick deduplication**: Adjacent identical labels auto-removed.
315
- - **Composing overlays**: XY/Ordinal charts paint `--semiotic-bg` across the full canvas by default, hiding anything beneath. When stacking charts (e.g. `position: absolute` overlay on top of a base), pass `frameProps={{ background: "transparent" }}` on the overlay to skip the fill. Network/Geo frames don't paint bg by default, so this only matters for XY/Ordinal.
317
+ - **Composing overlays**: XY/Ordinal charts paint `--semiotic-bg` across the canvas; stack with `frameProps={{ background: "transparent" }}` on the overlay. Network/Geo don't paint bg by default.
316
318
 
317
319
  ## Performance
318
320
 
package/README.md CHANGED
@@ -13,7 +13,7 @@ dashboards when you need them. Structured schemas and an MCP server so
13
13
  AI coding assistants generate correct chart code on the first try.
14
14
 
15
15
  ```jsx
16
- import { LineChart } from "semiotic/xy" // 143 KB gzip
16
+ import { LineChart } from "semiotic/xy"
17
17
 
18
18
  <LineChart
19
19
  data={salesData}
@@ -35,7 +35,7 @@ generate correct code without examples.
35
35
  Semiotic ships with everything an AI coding assistant needs to generate
36
36
  correct visualizations without trial and error:
37
37
 
38
- - **`semiotic/ai`** — a single import with 35 HOC charts (XY, ordinal, network, realtime), optimized for LLM code generation. Geo charts are in `semiotic/geo` to keep d3-geo out of non-geo bundles.
38
+ - **`semiotic/ai`** — a single import with 40 HOC charts (XY, ordinal, network, realtime), optimized for LLM code generation. Geo charts are in `semiotic/geo` to keep d3-geo out of non-geo bundles.
39
39
  - **`ai/schema.json`** — machine-readable prop schemas for every component
40
40
  - **`npx semiotic-mcp`** — an MCP server for tool-based chart rendering in any MCP client
41
41
  - **`npx semiotic-ai --doctor`** — validate component + props JSON from the command line with typo suggestions and anti-pattern detection
@@ -280,21 +280,27 @@ for color, size, aggregation, and binning.
280
280
 
281
281
  ## Bundle Sizes
282
282
 
283
- Semiotic ships 11 entry points. **Don't import from `"semiotic"` unless you need everything** — use the sub-path that matches your chart type:
283
+ Semiotic ships 12 entry points. **Don't import from `"semiotic"` unless you need everything** — use the sub-path that matches your chart type:
284
+
285
+ <!-- semiotic-bundle-sizes:start -->
286
+ <!-- Auto-generated by `scripts/sync-bundle-sizes.mjs`. Edit dist/*, not this block. -->
284
287
 
285
288
  | Entry Point | gzip | What's inside |
286
289
  |---|---|---|
287
- | `semiotic/xy` | **143 KB** | LineChart, AreaChart, Scatterplot, Heatmap, + 7 more XY charts |
288
- | `semiotic/ordinal` | **109 KB** | BarChart, PieChart, BoxPlot, Histogram, + 11 more categorical charts |
289
- | `semiotic/network` | **98 KB** | ForceDirectedGraph, SankeyDiagram, Treemap, + 4 more |
290
- | `semiotic/geo` | **93 KB** | ChoroplethMap, FlowMap, DistanceCartogram, ProportionalSymbolMap |
291
- | `semiotic/realtime` | **145 KB** | RealtimeLineChart, RealtimeHistogram, + 3 streaming charts |
292
- | `semiotic/server` | **100 KB** | renderChart, renderDashboard, renderToImage, renderToAnimatedGif |
293
- | `semiotic/utils` | **31 KB** | ThemeProvider, validators, serialization — no chart components |
294
- | `semiotic/themes` | **5 KB** | Theme presets only (tufte, carbon, etc.) |
295
- | `semiotic/data` | **5 KB** | bin, rollup, groupBy, pivot, fromVegaLite |
296
- | `semiotic/ai` | **269 KB** | All 38 HOCs + validation — optimized for LLM code generation |
297
- | `semiotic` | **278 KB** | Everything above (full bundle) |
290
+ | `semiotic/xy` | **83 KB** | LineChart, AreaChart, Scatterplot, Heatmap, + 8 more XY charts |
291
+ | `semiotic/ordinal` | **67 KB** | BarChart, PieChart, BoxPlot, Histogram, + 11 more categorical charts |
292
+ | `semiotic/network` | **62 KB** | ForceDirectedGraph, SankeyDiagram, ProcessSankey, Treemap, + 4 more |
293
+ | `semiotic/geo` | **50 KB** | ChoroplethMap, FlowMap, DistanceCartogram, ProportionalSymbolMap |
294
+ | `semiotic/realtime` | **88 KB** | RealtimeLineChart, RealtimeHistogram, + 3 streaming charts |
295
+ | `semiotic/server` | **114 KB** | renderChart, renderDashboard, renderToImage, renderToAnimatedGif |
296
+ | `semiotic/utils` | **22 KB** | ThemeProvider, validators, serialization — no chart components |
297
+ | `semiotic/recipes` | **5 KB** | Pure layout functions (waffle, marimekko, flextree, dagre, …) |
298
+ | `semiotic/themes` | **4 KB** | Theme presets only (tufte, carbon, etc.) |
299
+ | `semiotic/data` | **3 KB** | bin, rollup, groupBy, pivot, fromVegaLite |
300
+ | `semiotic/ai` | **184 KB** | All 40 HOCs + validation — optimized for LLM code generation |
301
+ | `semiotic` | **183 KB** | Everything below (full bundle) |
302
+
303
+ <!-- semiotic-bundle-sizes:end -->
298
304
 
299
305
  ```jsx
300
306
  // Import from the sub-path, not from "semiotic"
@@ -306,7 +312,7 @@ import { ChoroplethMap } from "semiotic/geo"
306
312
 
307
313
  **Tree-shaking**: Each sub-path is a self-contained bundle with `"sideEffects": false`. Bundlers (webpack, Rollup, Vite, esbuild) will tree-shake unused exports. If you only use `LineChart` from `semiotic/xy`, the bar/pie/network code is never included.
308
314
 
309
- **When to use `"semiotic"`**: Only if your app uses charts from 3+ categories (XY + ordinal + network) and you'd rather have one import than three. The full bundle is 278 KB gzipcomparable to a single D3 import.
315
+ **When to use `"semiotic"`**: Only if your app uses charts from 3+ categories (XY + ordinal + network) and you'd rather have one import than three. The full bundle is roughly the sum of every sub-path bundle above see the `semiotic` row of the table for the current number.
310
316
 
311
317
  ## TypeScript
312
318
 
@@ -496,7 +502,7 @@ Semiotic is indexed by AI-coding-agent documentation tools so your assistant (Cl
496
502
 
497
503
  Agent-facing API surface:
498
504
 
499
- - **`CLAUDE.md`**, **`ai/schema.json`**, **`ai/behaviorContracts.cjs`** — bundled in the npm tarball (see `package.json#files`); agents that install Semiotic locally read these directly. `CLAUDE.md` is the quick-start cheat sheet (HOC props, push API, theming, usage notes); `ai/schema.json` is the JSON Schema for every chart's prop surface (43 charts); `ai/behaviorContracts.cjs` carries the agent-visible semantic rules (color precedence, push-mode requirements, ID-accessor contracts).
505
+ - **`CLAUDE.md`**, **`ai/schema.json`**, **`ai/behaviorContracts.cjs`** — bundled in the npm tarball (see `package.json#files`); agents that install Semiotic locally read these directly. `CLAUDE.md` is the quick-start cheat sheet (HOC props, push API, theming, usage notes); `ai/schema.json` is the JSON Schema for every chart's prop surface (44 charts); `ai/behaviorContracts.cjs` carries the agent-visible semantic rules (color precedence, push-mode requirements, ID-accessor contracts).
500
506
  - [**`semiotic.nteract.io/llms.txt`**](https://semiotic.nteract.io/llms.txt) + [**`/llms-full.txt`**](https://semiotic.nteract.io/llms-full.txt) — deployed at the docs site per the [llms.txt standard](https://llmstxt.org). Agents fetch the navigation map (`llms.txt`) or the full inlined docs (`llms-full.txt`) over HTTP; they're not part of the npm package itself.
501
507
 
502
508
  ## Documentation
@@ -1,11 +1,133 @@
1
1
  "use strict"
2
2
 
3
+ const path = require("path")
4
+
3
5
  const VALID_INTENTS = [
4
6
  "comparison", "trend", "distribution", "relationship", "composition",
5
7
  "geographic", "network", "hierarchy",
6
8
  ]
7
9
  const MAX_SAMPLE_SIZE = 5
8
10
 
11
+ // Capability matrix loaded from `ai/capabilities.json` — generated
12
+ // from `chartSpecs.ts` via `npm run docs:capabilities`. Used to
13
+ // filter suggestions when the caller passes capability constraints.
14
+ // Loaded lazily so a test or build that hasn't generated the file
15
+ // yet still imports cleanly; if the JSON is missing AND a caller
16
+ // passes capability constraints, `suggestCharts` returns an explicit
17
+ // error rather than silently filtering everything out (would happen
18
+ // because `chartSatisfiesCapabilities` fails closed for unknown
19
+ // chart names).
20
+ //
21
+ // `__dirname` points at the cjs source location at runtime —
22
+ // when bundled into `ai/dist/mcp-server.js` `__dirname` is
23
+ // `ai/dist/`, so we also probe the parent directory. Mirrors the
24
+ // `schema.json` loader's two-candidate pattern.
25
+ //
26
+ // `_capabilityMatrixLoaded` differentiates "load attempted, file
27
+ // missing → empty {}" from "load not yet attempted". Without this
28
+ // flag, a build that's missing capabilities.json would set
29
+ // `_capabilityMatrix = {}` and every subsequent call would short-
30
+ // circuit on `if (_capabilityMatrix)` (because `{}` is truthy) and
31
+ // never re-probe — the right behavior for a steady state but
32
+ // incompatible with detecting "loader failed".
33
+ let _capabilityMatrix = null
34
+ let _capabilityMatrixLoaded = false
35
+ function loadCapabilityMatrix() {
36
+ if (_capabilityMatrixLoaded) return _capabilityMatrix
37
+ const candidates = [
38
+ path.join(__dirname, "capabilities.json"),
39
+ path.join(__dirname, "..", "capabilities.json"),
40
+ ]
41
+ for (const candidate of candidates) {
42
+ try {
43
+ const json = require(candidate)
44
+ if (json && json.charts) {
45
+ _capabilityMatrix = json.charts
46
+ _capabilityMatrixLoaded = true
47
+ return _capabilityMatrix
48
+ }
49
+ } catch {
50
+ // try next candidate
51
+ }
52
+ }
53
+ _capabilityMatrixLoaded = true
54
+ return _capabilityMatrix
55
+ }
56
+
57
+ // Mapping from the capability arg names callers pass (push,
58
+ // linkedHover, ssr, selection, legend) to the chartSpecs field
59
+ // names in capabilities.json (supportsPush, supportsLinkedHover,
60
+ // etc.). Caller-side names are short for ergonomics; matrix-side
61
+ // names mirror the chartSpecs schema so search/grep finds both.
62
+ const CAPABILITY_KEY_MAP = {
63
+ push: "supportsPush",
64
+ linkedHover: "supportsLinkedHover",
65
+ ssr: "supportsSSR",
66
+ selection: "supportsSelection",
67
+ legend: "supportsLegend",
68
+ }
69
+
70
+ const VALID_CAPABILITY_KEYS = Object.keys(CAPABILITY_KEY_MAP)
71
+
72
+ /**
73
+ * Test a chart name against a capability constraint object. Returns
74
+ * `true` when the chart satisfies every constraint (or when no
75
+ * constraints are set). Unknown chart names fail closed — better to
76
+ * drop a suggestion than recommend a chart we can't confirm
77
+ * supports the caller's needs.
78
+ *
79
+ * `requirements` shape:
80
+ * { push?: boolean, linkedHover?: boolean, ssr?: boolean,
81
+ * selection?: boolean, legend?: boolean }
82
+ *
83
+ * Each key, when set to `true`, means "the chart MUST support this".
84
+ * `false` means "the chart MUST NOT support this" (rare, but
85
+ * symmetric — useful for "give me a chart that intentionally has no
86
+ * legend"). Unset keys are ignored.
87
+ */
88
+ function chartSatisfiesCapabilities(chartName, requirements) {
89
+ if (!requirements || Object.keys(requirements).length === 0) return true
90
+ const matrix = loadCapabilityMatrix()
91
+ if (matrix == null) return false // matrix unavailable — fail closed
92
+ const spec = matrix[chartName]
93
+ if (!spec) return false // unknown chart — fail closed
94
+ for (const [shortKey, want] of Object.entries(requirements)) {
95
+ if (want == null) continue
96
+ const matrixKey = CAPABILITY_KEY_MAP[shortKey]
97
+ if (!matrixKey) continue // unknown short key — ignore (validated upstream)
98
+ const has = spec[matrixKey] === true
99
+ if (has !== want) return false
100
+ }
101
+ return true
102
+ }
103
+
104
+ /**
105
+ * Explain why a chart doesn't satisfy a capability constraint set —
106
+ * used in the `filteredOut[].reason` payload so callers see the
107
+ * specific mismatched constraint(s) rather than the chart's
108
+ * data-shape rationale. Returns null when no mismatch (defensive;
109
+ * callers should only invoke this on charts that already failed
110
+ * `chartSatisfiesCapabilities`).
111
+ */
112
+ function explainCapabilityMismatch(chartName, requirements) {
113
+ if (!requirements || Object.keys(requirements).length === 0) return null
114
+ const matrix = loadCapabilityMatrix()
115
+ if (matrix == null) return "capability matrix unavailable (run `npm run docs:capabilities`)"
116
+ const spec = matrix[chartName]
117
+ if (!spec) return `${chartName} not found in capability matrix`
118
+ const mismatches = []
119
+ for (const [shortKey, want] of Object.entries(requirements)) {
120
+ if (want == null) continue
121
+ const matrixKey = CAPABILITY_KEY_MAP[shortKey]
122
+ if (!matrixKey) continue
123
+ const has = spec[matrixKey] === true
124
+ if (has !== want) {
125
+ mismatches.push(`requires ${shortKey}=${want} but ${matrixKey}=${has}`)
126
+ }
127
+ }
128
+ return mismatches.length > 0 ? mismatches.join("; ") : null
129
+ }
130
+
9
131
  function summarizeFields(data, keys) {
10
132
  const numericFields = []
11
133
  const stringFields = []
@@ -72,6 +194,7 @@ function uniqueNetworkNodes(data, sourceField, targetField) {
72
194
  function suggestCharts(args = {}) {
73
195
  const data = args.data
74
196
  const intent = args.intent
197
+ const capabilities = args.capabilities
75
198
 
76
199
  if (intent && !VALID_INTENTS.includes(intent)) {
77
200
  return {
@@ -80,10 +203,35 @@ function suggestCharts(args = {}) {
80
203
  }
81
204
  }
82
205
 
206
+ if (capabilities) {
207
+ if (typeof capabilities !== "object" || Array.isArray(capabilities)) {
208
+ return {
209
+ ok: false,
210
+ error: "capabilities must be an object like { push: true, linkedHover: true, ssr: true, selection: true, legend: true }.",
211
+ }
212
+ }
213
+ const unknown = Object.keys(capabilities).filter((k) => !VALID_CAPABILITY_KEYS.includes(k))
214
+ if (unknown.length > 0) {
215
+ return {
216
+ ok: false,
217
+ error: `Unknown capability key(s): ${unknown.join(", ")}. Expected: ${VALID_CAPABILITY_KEYS.join(", ")}.`,
218
+ }
219
+ }
220
+ // Probe matrix availability up front so callers see a clear error
221
+ // instead of an empty-suggestions silent-failure when
222
+ // capabilities.json is missing from the build.
223
+ if (loadCapabilityMatrix() == null) {
224
+ return {
225
+ ok: false,
226
+ error: "Capability matrix unavailable: ai/capabilities.json is missing. Run `npm run docs:capabilities` to generate it. (Capability filtering requires the matrix; suggestions without a `capabilities` arg still work.)",
227
+ }
228
+ }
229
+ }
230
+
83
231
  if (!data || !Array.isArray(data) || data.length === 0) {
84
232
  return {
85
233
  ok: false,
86
- error: "Pass { data: [{ ... }, ...] } with 1-5 sample data objects. Optionally include intent: 'comparison' | 'trend' | 'distribution' | 'relationship' | 'composition' | 'geographic' | 'network' | 'hierarchy'.",
234
+ error: "Pass { data: [{ ... }, ...] } with 1-5 sample data objects. Optionally include intent: 'comparison' | 'trend' | 'distribution' | 'relationship' | 'composition' | 'geographic' | 'network' | 'hierarchy', or capabilities: { push, linkedHover, ssr, selection, legend }.",
87
235
  }
88
236
  }
89
237
 
@@ -257,12 +405,37 @@ function suggestCharts(args = {}) {
257
405
  })
258
406
  }
259
407
 
408
+ // Apply capability filter as the last step so data-shape inference
409
+ // logic stays oblivious to it. Suggestions are kept in their
410
+ // confidence-ranked order; we just drop anything that doesn't
411
+ // satisfy the caller's constraints.
412
+ const filteredSuggestions = capabilities
413
+ ? suggestions.filter((s) => chartSatisfiesCapabilities(s.component, capabilities))
414
+ : suggestions
415
+
260
416
  return {
261
417
  ok: true,
262
418
  intent,
419
+ capabilities,
263
420
  fieldSummary: `Fields: ${keys.join(", ")} (${numericFields.length} numeric, ${stringFields.length} categorical, ${dateFields.length} date)`,
264
421
  fields,
265
- suggestions,
422
+ suggestions: filteredSuggestions,
423
+ // Surface the pre-filter set when a capability constraint was
424
+ // applied — caller can see which suggestions were dropped and
425
+ // whether to relax the constraint.
426
+ ...(capabilities && filteredSuggestions.length < suggestions.length && {
427
+ filteredOut: suggestions
428
+ .filter((s) => !chartSatisfiesCapabilities(s.component, capabilities))
429
+ .map((s) => ({
430
+ component: s.component,
431
+ // The `reason` here is the capability mismatch (which
432
+ // constraint failed), not the original data-shape rationale
433
+ // — callers debugging an empty result need to know which
434
+ // capability to relax, not why the chart was originally
435
+ // suggested.
436
+ reason: explainCapabilityMismatch(s.component, capabilities) || "did not satisfy capability constraints",
437
+ })),
438
+ }),
266
439
  }
267
440
  }
268
441
 
@@ -270,7 +443,10 @@ function formatSuggestionReport(result) {
270
443
  if (!result.ok) return result.error
271
444
 
272
445
  if (result.suggestions.length === 0) {
273
- return `Could not confidently recommend a chart type.\n\n${result.fieldSummary}\n\nTry providing intent ('${VALID_INTENTS.join("', '")}') to narrow recommendations, or use getSchema to browse available components.`
446
+ const tail = result.capabilities && result.filteredOut && result.filteredOut.length > 0
447
+ ? `\n\nDropped by capability filter (${formatCapabilityConstraints(result.capabilities)}):\n${result.filteredOut.map((s) => `- ${s.component}: ${s.reason}`).join("\n")}\n\nRelax the capability constraints, or use getSchema to browse alternatives.`
448
+ : `\n\nTry providing intent ('${VALID_INTENTS.join("', '")}') to narrow recommendations, or use getSchema to browse available components.`
449
+ return `Could not confidently recommend a chart type.\n\n${result.fieldSummary}${tail}`
274
450
  }
275
451
 
276
452
  const lines = result.suggestions.map((suggestion, i) => {
@@ -284,8 +460,20 @@ function formatSuggestionReport(result) {
284
460
  return lines.join("\n\n") + themingTip
285
461
  }
286
462
 
463
+ function formatCapabilityConstraints(capabilities) {
464
+ return Object.entries(capabilities)
465
+ .filter(([, v]) => v != null)
466
+ .map(([k, v]) => `${k}=${v}`)
467
+ .join(", ")
468
+ }
469
+
287
470
  module.exports = {
288
471
  VALID_INTENTS,
472
+ VALID_CAPABILITY_KEYS,
289
473
  formatSuggestionReport,
290
474
  suggestCharts,
475
+ // Exported for tests + callers that want to filter their own
476
+ // chart-name set without re-running the suggestion pipeline.
477
+ chartSatisfiesCapabilities,
478
+ explainCapabilityMismatch,
291
479
  }
@@ -4,7 +4,7 @@ const CATEGORY_ORDER = ["xy", "ordinal", "network", "geo", "realtime"]
4
4
 
5
5
  const COMPONENTS_BY_CATEGORY = {
6
6
  xy: [
7
- "LineChart", "AreaChart", "StackedAreaChart", "Scatterplot", "QuadrantChart",
7
+ "LineChart", "AreaChart", "DifferenceChart", "StackedAreaChart", "Scatterplot", "QuadrantChart",
8
8
  "MultiAxisLineChart", "CandlestickChart", "BubbleChart", "Heatmap",
9
9
  "ConnectedScatterplot", "ScatterplotMatrix", "MinimapChart",
10
10
  ],
@@ -14,8 +14,8 @@ const COMPONENTS_BY_CATEGORY = {
14
14
  "DonutChart", "GaugeChart", "FunnelChart", "SwimlaneChart",
15
15
  ],
16
16
  network: [
17
- "ForceDirectedGraph", "SankeyDiagram", "ChordDiagram", "TreeDiagram",
18
- "Treemap", "CirclePack", "OrbitDiagram",
17
+ "ForceDirectedGraph", "SankeyDiagram", "ProcessSankey", "ChordDiagram",
18
+ "TreeDiagram", "Treemap", "CirclePack", "OrbitDiagram",
19
19
  ],
20
20
  geo: [
21
21
  "ChoroplethMap", "ProportionalSymbolMap", "FlowMap", "DistanceCartogram",