semiotic 3.4.1 → 3.5.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.
- package/CLAUDE.md +114 -9
- package/README.md +45 -4
- package/ai/behaviorContracts.cjs +311 -0
- package/ai/chartSuggestions.cjs +291 -0
- package/ai/cli.js +255 -30
- package/ai/componentMetadata.cjs +107 -0
- package/ai/dist/mcp-server.js +907 -227
- package/ai/schema.json +3954 -2537
- package/ai/system-prompt.md +23 -4
- package/dist/components/LinkedCharts.d.ts +5 -1
- package/dist/components/Tooltip/Tooltip.d.ts +1 -1
- package/dist/components/charts/custom/NetworkCustomChart.d.ts +64 -0
- package/dist/components/charts/custom/OrdinalCustomChart.d.ts +71 -0
- package/dist/components/charts/custom/XYCustomChart.d.ts +59 -0
- package/dist/components/charts/geo/ChoroplethMap.d.ts +93 -2
- package/dist/components/charts/geo/DistanceCartogram.d.ts +51 -4
- package/dist/components/charts/geo/FlowMap.d.ts +55 -0
- package/dist/components/charts/geo/ProportionalSymbolMap.d.ts +53 -0
- package/dist/components/charts/index.d.ts +6 -0
- package/dist/components/charts/network/ChordDiagram.d.ts +34 -2
- package/dist/components/charts/network/CirclePack.d.ts +36 -1
- package/dist/components/charts/network/ForceDirectedGraph.d.ts +130 -2
- package/dist/components/charts/network/OrbitDiagram.d.ts +37 -0
- package/dist/components/charts/network/SankeyDiagram.d.ts +51 -2
- package/dist/components/charts/network/TreeDiagram.d.ts +37 -2
- package/dist/components/charts/network/Treemap.d.ts +36 -2
- package/dist/components/charts/ordinal/BarChart.d.ts +111 -1
- package/dist/components/charts/ordinal/BoxPlot.d.ts +31 -0
- package/dist/components/charts/ordinal/DonutChart.d.ts +36 -0
- package/dist/components/charts/ordinal/DotPlot.d.ts +31 -0
- package/dist/components/charts/ordinal/FunnelChart.d.ts +40 -0
- package/dist/components/charts/ordinal/GaugeChart.d.ts +45 -0
- package/dist/components/charts/ordinal/GroupedBarChart.d.ts +38 -0
- package/dist/components/charts/ordinal/Histogram.d.ts +95 -0
- package/dist/components/charts/ordinal/LikertChart.d.ts +42 -0
- package/dist/components/charts/ordinal/PieChart.d.ts +90 -1
- package/dist/components/charts/ordinal/RidgelinePlot.d.ts +27 -0
- package/dist/components/charts/ordinal/StackedBarChart.d.ts +38 -0
- package/dist/components/charts/ordinal/SwarmPlot.d.ts +36 -0
- package/dist/components/charts/ordinal/SwimlaneChart.d.ts +60 -0
- package/dist/components/charts/ordinal/ViolinPlot.d.ts +32 -0
- package/dist/components/charts/realtime/RealtimeHeatmap.d.ts +22 -4
- package/dist/components/charts/realtime/RealtimeHistogram.d.ts +5 -2
- package/dist/components/charts/realtime/RealtimeLineChart.d.ts +24 -3
- package/dist/components/charts/realtime/RealtimeSwarmChart.d.ts +12 -0
- package/dist/components/charts/realtime/RealtimeWaterfallChart.d.ts +14 -0
- package/dist/components/charts/realtime/defaultRealtimeTooltip.d.ts +43 -0
- package/dist/components/charts/realtime/resolveWindowSize.d.ts +26 -0
- package/dist/components/charts/shared/chartSpecs.d.ts +91 -0
- package/dist/components/charts/shared/colorPalettes.d.ts +62 -0
- package/dist/components/charts/shared/colorUtils.d.ts +9 -10
- package/dist/components/charts/shared/numberFormat.d.ts +58 -0
- package/dist/components/charts/shared/sparseArray.d.ts +27 -0
- package/dist/components/charts/shared/streamPropsHelpers.d.ts +113 -0
- package/dist/components/charts/shared/timeFormat.d.ts +60 -0
- package/dist/components/charts/shared/useChartSetup.d.ts +8 -0
- package/dist/components/charts/shared/useCustomChartSetup.d.ts +84 -0
- package/dist/components/charts/shared/useFrameImperativeHandle.d.ts +28 -0
- package/dist/components/charts/shared/useOrdinalStreaming.d.ts +6 -19
- package/dist/components/charts/shared/useStreamingLegend.d.ts +27 -11
- package/dist/components/charts/shared/validateProps.d.ts +2 -2
- package/dist/components/charts/shared/validationMap.d.ts +2 -1
- package/dist/components/charts/shared/withChartWrapper.d.ts +13 -4
- package/dist/components/charts/xy/AreaChart.d.ts +30 -1
- package/dist/components/charts/xy/CandlestickChart.d.ts +33 -6
- package/dist/components/charts/xy/ConnectedScatterplot.d.ts +24 -0
- package/dist/components/charts/xy/MinimapChart.d.ts +51 -0
- package/dist/components/charts/xy/MultiAxisLineChart.d.ts +27 -0
- package/dist/components/charts/xy/QuadrantChart.d.ts +21 -0
- package/dist/components/charts/xy/Scatterplot.d.ts +34 -2
- package/dist/components/charts/xy/ScatterplotMatrix.d.ts +16 -0
- package/dist/components/charts/xy/StackedAreaChart.d.ts +49 -1
- package/dist/components/export/selectionSerializer.d.ts +1 -1
- package/dist/components/realtime/types.d.ts +7 -9
- package/dist/components/recipes/bullet.d.ts +86 -0
- package/dist/components/recipes/calendar.d.ts +43 -0
- package/dist/components/recipes/dagre.d.ts +56 -0
- package/dist/components/recipes/flextree.d.ts +55 -0
- package/dist/components/recipes/marimekko.d.ts +55 -0
- package/dist/components/recipes/parallelCoordinates.d.ts +97 -0
- package/dist/components/recipes/recipeUtils.d.ts +27 -0
- package/dist/components/recipes/waffle.d.ts +46 -0
- package/dist/components/semiotic-ai.d.ts +4 -0
- package/dist/components/semiotic-network.d.ts +3 -0
- package/dist/components/semiotic-ordinal.d.ts +3 -0
- package/dist/components/semiotic-recipes.d.ts +24 -0
- package/dist/components/semiotic-xy.d.ts +3 -0
- package/dist/components/semiotic.d.ts +2 -2
- package/dist/components/server/renderToStaticSVG.d.ts +8 -2
- package/dist/components/server/serverChartConfigs.d.ts +47 -1
- package/dist/components/server/staticAnnotations.d.ts +6 -0
- package/dist/components/store/ObservationStore.d.ts +1 -3
- package/dist/components/store/SelectionStore.d.ts +2 -4
- package/dist/components/store/ThemeStore.d.ts +4 -4
- package/dist/components/store/TooltipStore.d.ts +5 -3
- package/dist/components/store/createStore.d.ts +4 -2
- package/dist/components/store/useSelection.d.ts +7 -4
- package/dist/components/stream/CanvasHitTester.d.ts +10 -8
- package/dist/components/stream/DataSourceAdapter.d.ts +9 -0
- package/dist/components/stream/GeoPipelineStore.d.ts +9 -0
- package/dist/components/stream/GeoTileRenderer.d.ts +14 -0
- package/dist/components/stream/NetworkPipelineStore.d.ts +25 -0
- package/dist/components/stream/NetworkSVGOverlay.d.ts +18 -12
- package/dist/components/stream/OrdinalPipelineStore.d.ts +12 -0
- package/dist/components/stream/PipelineStore.d.ts +51 -0
- package/dist/components/stream/SVGOverlay.d.ts +12 -0
- package/dist/components/stream/SceneGraph.d.ts +15 -1
- package/dist/components/stream/SceneToSVG.d.ts +1 -1
- package/dist/components/stream/categoryDomain.d.ts +4 -0
- package/dist/components/stream/composeOverlays.d.ts +15 -0
- package/dist/components/stream/customLayout.d.ts +76 -0
- package/dist/components/stream/customLayoutPalette.d.ts +29 -0
- package/dist/components/stream/geoTypes.d.ts +33 -8
- package/dist/components/stream/hoverUtils.d.ts +4 -10
- package/dist/components/stream/keyboardNav.d.ts +18 -6
- package/dist/components/stream/networkCustomLayout.d.ts +67 -0
- package/dist/components/stream/networkTypes.d.ts +87 -21
- package/dist/components/stream/ordinalCustomLayout.d.ts +84 -0
- package/dist/components/stream/ordinalTypes.d.ts +47 -13
- package/dist/components/stream/renderers/barFunnelCanvasRenderer.d.ts +9 -1
- package/dist/components/stream/renderers/canvasRenderHelpers.d.ts +92 -0
- package/dist/components/stream/sampleCurvePath.d.ts +9 -0
- package/dist/components/stream/types.d.ts +57 -10
- package/dist/components/stream/useHydration.d.ts +89 -0
- package/dist/components/stream/useStableShallow.d.ts +1 -0
- package/dist/components/stream/xySceneBuilders/types.d.ts +4 -0
- package/dist/geo.min.js +2 -1
- package/dist/geo.module.min.js +2 -1
- package/dist/network.min.js +2 -1
- package/dist/network.module.min.js +2 -1
- package/dist/ordinal.min.js +2 -1
- package/dist/ordinal.module.min.js +2 -1
- package/dist/realtime.min.js +2 -1
- package/dist/realtime.module.min.js +2 -1
- package/dist/semiotic-ai.d.ts +69 -65
- package/dist/semiotic-ai.min.js +2 -1
- package/dist/semiotic-ai.module.min.js +2 -1
- package/dist/semiotic-data.d.ts +4 -4
- package/dist/semiotic-geo.d.ts +15 -15
- package/dist/semiotic-network.d.ts +19 -16
- package/dist/semiotic-ordinal.d.ts +31 -28
- package/dist/semiotic-realtime.d.ts +17 -17
- package/dist/semiotic-recipes.d.ts +24 -0
- package/dist/semiotic-recipes.min.js +1 -0
- package/dist/semiotic-recipes.module.min.js +1 -0
- package/dist/semiotic-server.d.ts +6 -6
- package/dist/semiotic-statisticalOverlays-C3DsOgr_.js +1 -0
- package/dist/semiotic-themes.d.ts +3 -3
- package/dist/semiotic-themes.min.js +2 -1
- package/dist/semiotic-themes.module.min.js +2 -1
- package/dist/semiotic-utils.d.ts +23 -23
- package/dist/semiotic-utils.min.js +2 -1
- package/dist/semiotic-utils.module.min.js +2 -1
- package/dist/semiotic-xy.d.ts +27 -24
- package/dist/semiotic.d.ts +63 -63
- package/dist/semiotic.min.js +2 -1
- package/dist/semiotic.module.min.js +2 -1
- package/dist/server.min.js +1 -1
- package/dist/server.module.min.js +1 -1
- package/dist/test-utils/canvasMock.d.ts +34 -5
- package/dist/xy.min.js +2 -1
- package/dist/xy.module.min.js +2 -1
- package/package.json +40 -20
- package/dist/semiotic-statisticalOverlays-Ckd_jM8z.js +0 -1
package/CLAUDE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Quick Start
|
|
4
4
|
- Install: `npm install semiotic`
|
|
5
|
-
- **Use sub-path imports** — `semiotic/xy` (
|
|
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.
|
|
6
6
|
- CLI: `npx semiotic-ai [--schema|--compact|--examples|--doctor]`
|
|
7
7
|
- MCP: `npx semiotic-mcp`
|
|
8
8
|
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
## XY Charts (`semiotic/xy`)
|
|
22
22
|
|
|
23
|
-
**LineChart** — `data`, `xAccessor` ("x"), `yAccessor` ("y"), `lineBy`, `lineDataAccessor`, `colorBy`, `colorScheme`, `curve`, `lineWidth` (2), `showPoints`, `pointRadius` (3), `fillArea` (boolean|string[]), `areaOpacity` (0.3), `lineGradient`, `anomaly`, `forecast`, `directLabel`, `gapStrategy`, `xScaleType`/`yScaleType` ("linear"|"log"|"time")
|
|
24
|
-
**AreaChart** — LineChart props + `areaBy`, `y0Accessor`, `gradientFill`, `areaOpacity` (0.7), `showLine` (true)
|
|
25
|
-
**StackedAreaChart** — flat array + `areaBy` (required), `colorBy`, `normalize`. No `lineBy`/`lineDataAccessor`.
|
|
23
|
+
**LineChart** — `data`, `xAccessor` ("x"), `yAccessor` ("y"), `lineBy`, `lineDataAccessor`, `colorBy`, `colorScheme`, `curve`, `lineWidth` (2), `showPoints`, `pointRadius` (3), `fillArea` (boolean|string[]), `areaOpacity` (0.3), `lineGradient`, `anomaly`, `forecast`, `directLabel`, `gapStrategy`, `xScaleType`/`yScaleType` ("linear"|"log"|"time"), `tooltip="multi"` for hover-anywhere series comparison
|
|
24
|
+
**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
26
|
**Scatterplot** — `data`, `xAccessor`, `yAccessor`, `colorBy`, `sizeBy`, `sizeRange`, `pointRadius` (5), `pointOpacity` (0.8), `marginalGraphics`
|
|
27
27
|
**BubbleChart** — Scatterplot + `sizeBy` (required), `sizeRange` ([5,40])
|
|
28
28
|
**ConnectedScatterplot** — + `orderAccessor`
|
|
@@ -101,6 +101,88 @@ ref.current.getScales() // returns {o, r, projection}
|
|
|
101
101
|
`remove()` and `update()` require an ID accessor: `pointIdAccessor` on XY/realtime charts, `dataIdAccessor` on ordinal charts. `replace()` is ordinal-only and routes through a bounded-ingest path that preserves category insertion-order memory and the transition position snapshot — what aggregator HOCs like LikertChart use under the hood to re-aggregate streaming input without shuffling categories or losing animations. Network HOC refs also use `remove(id)`/`update(id, updater)` (operates on nodes). For edge-level operations, use `StreamNetworkFrameHandle` directly: `removeNode(id)`, `removeEdge(sourceId, targetId)` or `removeEdge(edgeId)` (requires `edgeIdAccessor`), `updateNode(id, updater)`, `updateEdge(sourceId, targetId, updater)`.
|
|
102
102
|
Not supported: Tree, Treemap, CirclePack, Orbit, ChoroplethMap, FlowMap, ScatterplotMatrix.
|
|
103
103
|
|
|
104
|
+
## Custom Charts (escape hatch)
|
|
105
|
+
|
|
106
|
+
When the catalog doesn't fit, three HOCs let you supply a layout function that emits scene primitives directly. The frame still owns hit testing, transitions, decay, theme cascade, and SSR — your layout owns geometry only.
|
|
107
|
+
|
|
108
|
+
- **`XYCustomChart`** (`semiotic/xy`) — XY layouts: waffle, calendar heatmap, custom point/line/area arrangements
|
|
109
|
+
- **`OrdinalCustomChart`** (`semiotic/ordinal`) — category × value layouts: marimekko, parallel coordinates, bullet, fan chart, slope graph
|
|
110
|
+
- **`NetworkCustomChart`** (`semiotic/network`) — graph layouts: flextree, dagre, custom force/radial
|
|
111
|
+
|
|
112
|
+
All three accept `layout` and `layoutConfig` (your own typed config), but the layout context and return shape differ by chart family:
|
|
113
|
+
|
|
114
|
+
- **`XYCustomChart` / `OrdinalCustomChart`** — `layout: (ctx) => { nodes, overlays? }`. Context exposes `data`, `scales`, `dimensions` (with plot rect — center-anchored for radial ordinal, top-left otherwise), `theme` (semantic + categorical), `resolveColor(key)`, and `config`. XY scales: `{ x, y }` (linear). Ordinal scales: `{ o, r, projection }` (band + linear).
|
|
115
|
+
- **`NetworkCustomChart`** — `layout: (ctx) => { sceneNodes?, sceneEdges?, labels?, overlays? }`. Context exposes `nodes`, `edges`, `dimensions`, `theme`, `resolveColor(key)`, and `config` — graph data, no `data`/`scales`. Network layouts often run an external positioner (`d3-flextree`, `dagre`) on nodes/edges, then emit network scene primitives (`circle`, `rect`, `arc` for nodes; `line`, `bezier`, `curved` for edges).
|
|
116
|
+
|
|
117
|
+
XY/ordinal frames render whatever you put in `nodes` (rect, point, area, line, wedge, connector, etc.). Network frames split node-shaped scenes from edge-shaped scenes — give them `sceneNodes` for the round/rect/arc visuals and `sceneEdges` for the connecting paths. All three frames handle painting, hit testing, accessibility, transitions, decay, and SSR for you.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { XYCustomChart } from "semiotic/xy"
|
|
121
|
+
import { OrdinalCustomChart } from "semiotic/ordinal"
|
|
122
|
+
import { NetworkCustomChart } from "semiotic/network"
|
|
123
|
+
import {
|
|
124
|
+
waffleLayout, calendarLayout, // XY recipes
|
|
125
|
+
marimekkoLayout, bulletLayout, parallelCoordinatesLayout, // ordinal
|
|
126
|
+
flextreeLayout, dagreLayout, // network
|
|
127
|
+
} from "semiotic/recipes"
|
|
128
|
+
|
|
129
|
+
<XYCustomChart data={cells} layout={waffleLayout} layoutConfig={{ rows: 10, columns: 10, ... }} />
|
|
130
|
+
<OrdinalCustomChart data={revenue} layout={marimekkoLayout} layoutConfig={{ ... }} />
|
|
131
|
+
<NetworkCustomChart nodes={nodes} edges={edges} layout={flextreeLayout} layoutConfig={{ ... }} />
|
|
132
|
+
```
|
|
133
|
+
|
|
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.
|
|
135
|
+
|
|
136
|
+
### Chrome (labels, axes, legends): the recipe owns it
|
|
137
|
+
|
|
138
|
+
Custom layouts often need chrome the standard chart axes can't render — variable-width bars, per-row independent value scales, parallel axes, asymmetric tree branches. The unified pattern: **the recipe emits its own labels/axes/ticks via the `overlays` return field** (a ReactNode painted on top of the canvas). Built-in axes (via `showAxes` on the HOC) work for layouts that respect the standard scale; everything else, the recipe handles itself.
|
|
139
|
+
|
|
140
|
+
Convention across shipped recipes — every recipe takes a consistent set of label/axis toggles in `layoutConfig`:
|
|
141
|
+
|
|
142
|
+
| Recipe | Toggle | Default | What it draws |
|
|
143
|
+
|---|---|---|---|
|
|
144
|
+
| `marimekkoLayout` | `showCategoryLabels` | `true` | Category names under each variable-width bar |
|
|
145
|
+
| `bulletLayout` | `showLabels` | `true` | Metric name to the left of each row |
|
|
146
|
+
| `bulletLayout` | `showTicks` | `true` | Per-row value-axis ticks (each row independently scaled) |
|
|
147
|
+
| `parallelCoordinatesLayout` | `showAxes` | `true` | Vertical axis line + field name + 5 ticks per axis |
|
|
148
|
+
| `flextreeLayout` / `dagreLayout` | `showLabels` | `true` | Node text rendered inside each rect |
|
|
149
|
+
| `waffleLayout` / `calendarLayout` | — | — | No chrome needed (uniform grid + cell color tells the story) |
|
|
150
|
+
|
|
151
|
+
Writing your own recipe: prefer `showXxx` boolean toggles for chrome opt-out, `xxxFormat` callbacks for custom number/string formatting, and reserve plot-rect padding (`labelWidth`, `labelPadding`, `axisLabelPadding`) when chrome eats space.
|
|
152
|
+
|
|
153
|
+
### Interaction (hover, brush, selection): the parent component owns it
|
|
154
|
+
|
|
155
|
+
Recipes are pure functions, so they can't carry interactive state (hover, brush ranges, selection). The pattern: recipes accept a **predicate prop** (e.g. `parallelCoordinatesLayout`'s `highlightFn?: (d) => boolean`) and the parent component manages state. Wire `onObservation` (`{ type: "hover" | "hover-end" | ... }`) on `OrdinalCustomChart` / `XYCustomChart` / `NetworkCustomChart` to update parent state, then feed a derived predicate back into `layoutConfig`. Matching rows render at full opacity; non-matching dim. Highlighted rows z-order on top so neighbors don't cover them.
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
const [hovered, setHovered] = useState(null)
|
|
159
|
+
<OrdinalCustomChart
|
|
160
|
+
data={rows}
|
|
161
|
+
layout={parallelCoordinatesLayout}
|
|
162
|
+
layoutConfig={{
|
|
163
|
+
fields: ["mpg", "hp", "weight"],
|
|
164
|
+
highlightFn: hovered ? (d) => d.name === hovered : undefined,
|
|
165
|
+
}}
|
|
166
|
+
onObservation={(obs) => {
|
|
167
|
+
if (obs.type === "hover") setHovered(obs.datum?.data?.name ?? obs.datum?.name)
|
|
168
|
+
else if (obs.type === "hover-end") setHovered(null)
|
|
169
|
+
}}
|
|
170
|
+
/>
|
|
171
|
+
```
|
|
172
|
+
|
|
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
|
+
### Built-in chrome works when the layout uses standard scales
|
|
176
|
+
|
|
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.).
|
|
178
|
+
|
|
179
|
+
**Notes:**
|
|
180
|
+
- 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.
|
|
181
|
+
- Layouts that need axis domains: pass `xExtent`/`yExtent` (XY) or `oExtent`/`rExtent` (ordinal) — those flow through scale construction *before* the layout runs.
|
|
182
|
+
- Streaming layouts: ingest data via the chart's ref (`push`/`pushMany`); the layout re-runs on each ingest. Custom overlays update on data-change paths, NOT on per-frame animation rebuilds (intentional — would force a React re-render per frame).
|
|
183
|
+
- Custom layouts own their colors. Always prefer `ctx.resolveColor(key)` over hardcoded literals so `ThemeProvider` / `colorScheme` flow through. `CategoryColorProvider` integration is XY-only; for cross-chart category sync on network/ordinal customLayouts, pass a matching `colorScheme` to each chart.
|
|
184
|
+
- Tooltips: emit datum keys that match the user-visible accessor names (e.g. when `categoryAccessor: "region"` and `valueAccessor: "revenue"` are passed, put `region` and `revenue` on each rect's `datum`). The default tooltip looks them up by accessor name and the user's custom tooltip will read whatever fields they expect. Avoid underscored synthetic keys — the default tooltip filters those out.
|
|
185
|
+
|
|
104
186
|
## Coordinated Views
|
|
105
187
|
|
|
106
188
|
**LinkedCharts** — `selections`, **CategoryColorProvider** — `colors`|`categories` + `colorScheme`
|
|
@@ -137,9 +219,9 @@ Server SVGs include `role="img"`, `<title>`, `<desc>`, grid, legend, annotations
|
|
|
137
219
|
- **GroupedBarChart** — `data`, `categoryAccessor`, `valueAccessor`, `groupBy` (required).
|
|
138
220
|
- **PieChart/DonutChart** — `data`, `categoryAccessor`, `valueAccessor`.
|
|
139
221
|
- **FunnelChart** — `data`, `stepAccessor` ("step"), `valueAccessor` ("value"). Renders with trapezoid connectors, no axes.
|
|
140
|
-
- **GaugeChart** — `value
|
|
222
|
+
- **GaugeChart** — `value`. Optional: `thresholds` (array of `{value, color, label}`), `min`, `max`, `sweep`, `arcWidth`.
|
|
141
223
|
- **SwimlaneChart** — `data`, `categoryAccessor`, `subcategoryAccessor` (required), `valueAccessor`.
|
|
142
|
-
- **ForceDirectedGraph** — `edges` (required). `nodes`
|
|
224
|
+
- **ForceDirectedGraph** — `nodes`, `edges` (both required). If deriving nodes from edge endpoints, materialize `nodes` before returning JSX/renderChart props.
|
|
143
225
|
- **SankeyDiagram** — `edges` (required), `valueAccessor`.
|
|
144
226
|
- **ChoroplethMap** — `areas` (GeoJSON features, pre-resolved).
|
|
145
227
|
|
|
@@ -165,7 +247,7 @@ CSS custom properties: `--semiotic-bg`, `--semiotic-text`, `--semiotic-text-seco
|
|
|
165
247
|
<ThemeProvider theme={{ mode: "dark", colors: { categorical: [...] } }}> {/* Merge onto dark base */}
|
|
166
248
|
```
|
|
167
249
|
|
|
168
|
-
**Color priority** (with `colorBy`): explicit `colorScheme` > ThemeProvider `colors.categorical` > `"category10"`.
|
|
250
|
+
**Color priority** (with `colorBy`): CategoryColorProvider/LinkedCharts category map > explicit `colorScheme` fallback > ThemeProvider `colors.categorical` > `"category10"`.
|
|
169
251
|
Presets: `light`, `dark`, `high-contrast`, `pastels`(-dark), `bi-tool`(-dark), `italian`(-dark), `tufte`(-dark), `journalist`(-dark), `playful`(-dark), `carbon`(-dark).
|
|
170
252
|
Serialization: `themeToCSS(theme, selector)`, `themeToTokens(theme)`, `resolveThemePreset(name)`.
|
|
171
253
|
|
|
@@ -182,11 +264,34 @@ Canvas scene builders read CSS variables via `getComputedStyle` on the canvas DO
|
|
|
182
264
|
## AI Features
|
|
183
265
|
`onObservation`/`useChartObserver`, `toConfig`/`fromConfig`/`toURL`/`fromURL`/`copyConfig`/`configToJSX`, `validateProps(component, props)`, `diagnoseConfig(component, props)`, `exportChart(div, { format })`, `npx semiotic-ai --doctor`
|
|
184
266
|
|
|
267
|
+
|
|
268
|
+
## AI Behavior Contracts
|
|
269
|
+
|
|
270
|
+
<!-- semiotic-behavior-contracts:start -->
|
|
271
|
+
|
|
272
|
+
These rules are generated from `ai/behaviorContracts.cjs` and are consumed by `semiotic-ai --doctor`, MCP resources, and docs checks.
|
|
273
|
+
|
|
274
|
+
- **Data required by usage mode** (`props.data-required-by-usage-mode`): Static usage (`renderChart`, MCP previews, SSR snapshots, and copy/paste examples with immediate data) requires data in props. React push mode selects live ingestion by omitting data and mutating through a ref.
|
|
275
|
+
Agent action: Pass usageMode="push" to `semiotic-ai --doctor` when validating ref-based JSX with no data prop. Keep usageMode="static" or omit it for renderChart/MCP/static configs where data must be present.
|
|
276
|
+
- **Categorical color precedence** (`color.category-precedence`): When colorBy is set, CategoryColorProvider/LinkedCharts category maps win for mapped categories. Unmapped categories fall back to explicit colorScheme, then ThemeProvider colors.categorical, then the built-in categorical fallback.
|
|
277
|
+
Agent action: Use colorBy for categorical encodings. Use CategoryColorProvider or LinkedCharts for cross-chart consistency, colorScheme for per-chart fallback palettes, and avoid frameProps style functions unless intentionally bypassing HOC color resolution.
|
|
278
|
+
- **Required prop combinations** (`props.required-combinations`): Some chart families need semantic props beyond data. These combinations are enforced by validation/schema for static configs and remain required in push mode unless explicitly noted.
|
|
279
|
+
Agent action: Before returning code, check the selected component against the required combinations list. For push mode, omit data but keep semantic props such as areaBy, sizeBy, stackBy, and groupBy.
|
|
280
|
+
Required combinations: StackedAreaChart: static data + areaBy; push areaBy. Stacked areas need a flat data array plus areaBy to identify the stacked series. BubbleChart: static data + sizeBy; push sizeBy. Bubbles need sizeBy in addition to x/y accessors so radius encodes data rather than a constant point size. StackedBarChart: static data + stackBy; push stackBy. Stacked bars need stackBy to split each category into stack segments. GroupedBarChart: static data + groupBy; push groupBy. Grouped bars need groupBy to split each category into side-by-side bars. SwimlaneChart: static data + subcategoryAccessor; push subcategoryAccessor. Swimlanes need subcategoryAccessor; colorBy defaults to the same field when not provided. GaugeChart: static value; push not supported. GaugeChart is value-only. thresholds, min, max, sweep, and arcWidth are optional. ForceDirectedGraph: static nodes + edges; push nodes + edges. ForceDirectedGraph schema/rendering requires nodes and edges. If an agent infers nodes from edge endpoints, it must materialize a nodes array before returning code.
|
|
281
|
+
- **Push mode omits data** (`streaming.push-mode-data`): HOC push mode is selected by omitting the data prop entirely. Passing data={[]} is static empty data and can clear/reinitialize the frame on render.
|
|
282
|
+
Agent action: For live charts, create a ref, omit data, then call ref.current.push() or pushMany(). For static renderChart/MCP snapshots, provide data because renderChart cannot push later.
|
|
283
|
+
- **Ref mutations need stable IDs** (`streaming.ref-mutations-require-id-accessors`): push() and pushMany() can append without IDs, but remove(id) and update(id, updater) require a stable ID accessor: pointIdAccessor for XY/realtime charts, dataIdAccessor for ordinal charts, and nodeIDAccessor/edgeIdAccessor for network operations.
|
|
284
|
+
Agent action: When generating code that calls remove() or update(), include the matching ID accessor and make sure pushed rows carry that ID field.
|
|
285
|
+
- **renderChart uses static props only** (`rendering.renderchart-static-props`): MCP renderChart and semiotic/server renderChart render a single static SVG/PNG snapshot. Browser-only realtime components and future ref pushes are not renderable through that path.
|
|
286
|
+
Agent action: Use renderChart only with renderable HOC components and complete static data. For live behavior, return React code with a ref and do not promise MCP-rendered output.
|
|
287
|
+
|
|
288
|
+
<!-- semiotic-behavior-contracts:end -->
|
|
289
|
+
|
|
185
290
|
## Accessibility
|
|
186
291
|
|
|
187
292
|
`role="group"` (outer) + `role="img"` (inner canvas). Keyboard: arrows navigate points, Enter cycles neighbors, Home/End/PageUp/PageDown. Shape-adaptive focus ring (`--semiotic-focus`). `accessibleTable` (default true) for sr-only data summary. Auto-detects `prefers-reduced-motion` and `forced-colors`. Hooks: `useReducedMotion()`, `useHighContrast()`.
|
|
188
293
|
|
|
189
|
-
##
|
|
294
|
+
## Usage Notes
|
|
190
295
|
|
|
191
296
|
- **Tooltip datum shape**: HOC tooltips get raw data. Frame `tooltipContent` gets wrapped — use `d.data`.
|
|
192
297
|
- **Legend**: "bottom" expands margin ~80px. MultiAxisLineChart: use `legendPosition="bottom"`.
|
|
@@ -201,7 +306,7 @@ Canvas scene builders read CSS variables via `getComputedStyle` on the canvas DO
|
|
|
201
306
|
- **Geo imports**: Always `semiotic/geo`, never `semiotic`, to avoid d3-geo in non-geo bundles.
|
|
202
307
|
- **fillArea**: `fillArea={["seriesA"]}` fills named series only. Names must match `lineBy`/`colorBy` keys.
|
|
203
308
|
- **hoverHighlight**: Requires `colorBy` as a string field.
|
|
204
|
-
- **tooltip="multi"**: Shows all series at hovered X. Custom fn receives `datum.allSeries`.
|
|
309
|
+
- **tooltip="multi"**: Shows all series at hovered X for LineChart, AreaChart, and StackedAreaChart. Custom fn receives `datum.allSeries`.
|
|
205
310
|
- **Axis config**: `frameProps.axes: [{ orient, includeMax, autoRotate, gridStyle, landmarkTicks }]`
|
|
206
311
|
- **xScaleType: "time"**: Creates `scaleTime`. Required for landmark ticks with timestamps.
|
|
207
312
|
- **scalePadding**: Pixel inset on scale ranges. Pass via `frameProps={{ scalePadding: 12 }}`.
|
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/semiotic)
|
|
5
5
|
[](https://www.typescriptlang.org/)
|
|
6
6
|
[](https://glama.ai/mcp/servers/nteract/semiotic)
|
|
7
|
+
[](https://mseep.ai/app/nteract-semiotic)
|
|
7
8
|
|
|
8
9
|
A React data visualization library designed for AI-assisted development.
|
|
9
10
|
|
|
@@ -356,7 +357,9 @@ const gif = await renderToAnimatedGif("line", data, { ... }, { fps: 12 })
|
|
|
356
357
|
|
|
357
358
|
## MCP Server
|
|
358
359
|
|
|
359
|
-
|
|
360
|
+
mcp-name: io.github.nteract/semiotic
|
|
361
|
+
|
|
362
|
+
Semiotic ships with an [MCP server](https://modelcontextprotocol.io) that lets AI coding assistants render charts, diagnose configuration problems, discover schemas, read packaged AI guidance, and get chart recommendations via tool calls.
|
|
360
363
|
|
|
361
364
|
### Setup
|
|
362
365
|
|
|
@@ -373,17 +376,34 @@ Add to your MCP client config (e.g. `claude_desktop_config.json` for Claude Desk
|
|
|
373
376
|
}
|
|
374
377
|
```
|
|
375
378
|
|
|
376
|
-
No API keys or authentication required. The server runs locally via stdio.
|
|
379
|
+
No API keys or authentication required. The server runs locally via stdio. HTTP mode is also available for inspectors and web clients: `npx semiotic-mcp --http --port 3001`.
|
|
377
380
|
|
|
378
381
|
### Tools
|
|
379
382
|
|
|
380
383
|
| Tool | Description |
|
|
381
384
|
|------|-------------|
|
|
382
385
|
| **`renderChart`** | Render a Semiotic chart to static SVG. Supports the components returned by `getSchema` that are marked `[renderable]`. Pass `{ component: "LineChart", props: { data: [...], xAccessor: "x", yAccessor: "y" } }`. Returns SVG string or validation errors with fix suggestions. |
|
|
383
|
-
| **`getSchema`** | Return the prop schema for a specific component. Pass `{ component: "LineChart" }` to get its props, or omit `component` to list all
|
|
386
|
+
| **`getSchema`** | Return the prop schema for a specific component. Pass `{ component: "LineChart" }` to get its props, or omit `component` to list all 43 chart schemas. Components marked `[renderable]` are available through `renderChart`; realtime charts require a browser/live environment. |
|
|
384
387
|
| **`suggestChart`** | Recommend chart types for a data sample. Pass `{ data: [{...}, ...] }` with 1–5 sample objects. Optionally include `intent` (`"comparison"`, `"trend"`, `"distribution"`, `"relationship"`, `"composition"`, `"geographic"`, `"network"`, `"hierarchy"`). Returns ranked suggestions with example props. |
|
|
385
388
|
| **`diagnoseConfig`** | Check a chart configuration for common problems — empty data, bad dimensions, missing accessors, wrong data shape, and more. Returns a human-readable diagnostic report with actionable fixes. |
|
|
386
389
|
| **`reportIssue`** | Generate a pre-filled GitHub issue URL for bug reports or feature requests. Pass `{ title: "...", body: "...", labels: ["bug"] }`. Returns a URL the user can open to submit. |
|
|
390
|
+
| **`applyTheme`** | List named theme presets or return ThemeProvider/CSS/token usage for a preset such as `{ name: "tufte" }`. |
|
|
391
|
+
|
|
392
|
+
### Resources
|
|
393
|
+
|
|
394
|
+
| Resource | Description |
|
|
395
|
+
|----------|-------------|
|
|
396
|
+
| **`semiotic://schema`** | Full machine-readable component schema JSON. |
|
|
397
|
+
| **`semiotic://components`** | Component index showing renderable/browser-only status and MCP categories. |
|
|
398
|
+
| **`semiotic://system-prompt`** | Compact AI instructions with import rules, chart props, SSR guidance, and pitfalls. |
|
|
399
|
+
| **`semiotic://examples`** | Copy-paste chart examples by data shape. |
|
|
400
|
+
|
|
401
|
+
### Prompts
|
|
402
|
+
|
|
403
|
+
| Prompt | Description |
|
|
404
|
+
|--------|-------------|
|
|
405
|
+
| **`build-semiotic-chart`** | Reusable workflow for choosing a chart, reading schema, diagnosing props, and rendering a preview. |
|
|
406
|
+
| **`debug-semiotic-chart`** | Reusable workflow for debugging invalid props, rendering failures, and issue reports. |
|
|
387
407
|
|
|
388
408
|
### Example: get schema for a component
|
|
389
409
|
|
|
@@ -453,17 +473,38 @@ Args: {
|
|
|
453
473
|
For quick validation without an MCP client:
|
|
454
474
|
|
|
455
475
|
```bash
|
|
476
|
+
npx semiotic-ai --list # list components with import paths and renderability
|
|
477
|
+
npx semiotic-ai --list --json # machine-readable component index
|
|
478
|
+
npx semiotic-ai --schema GaugeChart
|
|
479
|
+
npx semiotic-ai --suggest '{"data":[{"category":"A","value":10}],"intent":"comparison"}'
|
|
456
480
|
npx semiotic-ai --doctor # validate component + props JSON
|
|
457
481
|
npx semiotic-ai --schema # dump all chart schemas
|
|
458
482
|
npx semiotic-ai --compact # compact schema (fewer tokens)
|
|
459
483
|
```
|
|
460
484
|
|
|
485
|
+
`--doctor` uses the full `diagnoseConfig` checks when `dist` is available and falls back to schema-only validation in clean source checkouts.
|
|
486
|
+
|
|
487
|
+
## Where to find Semiotic for AI assistants
|
|
488
|
+
|
|
489
|
+
Semiotic is indexed by AI-coding-agent documentation tools so your assistant (Claude Code, Cursor, Cline, Copilot, etc.) can pull current docs and tools without copy-paste:
|
|
490
|
+
|
|
491
|
+
- **Context7** — [context7.com/nteract/semiotic](https://context7.com/nteract/semiotic) (configured via `context7.json`)
|
|
492
|
+
- **DeepWiki** — [deepwiki.com/nteract/semiotic](https://deepwiki.com/nteract/semiotic)
|
|
493
|
+
- **GitMCP** — [gitmcp.io/nteract/semiotic](https://gitmcp.io/nteract/semiotic) (exposes the repo as an MCP endpoint directly)
|
|
494
|
+
- **Official MCP Registry** — search "semiotic" at [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io)
|
|
495
|
+
- **Smithery** — [smithery.ai/server/nteract/semiotic](https://smithery.ai/server/nteract/semiotic)
|
|
496
|
+
|
|
497
|
+
Agent-facing API surface:
|
|
498
|
+
|
|
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).
|
|
500
|
+
- [**`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
|
+
|
|
461
502
|
## Documentation
|
|
462
503
|
|
|
463
504
|
[Interactive docs and examples](https://semiotic.nteract.io)
|
|
464
505
|
|
|
465
506
|
- [Getting Started](https://semiotic.nteract.io/getting-started)
|
|
466
|
-
- [Charts](https://semiotic.nteract.io/charts) —
|
|
507
|
+
- [Charts](https://semiotic.nteract.io/charts) — chart types with live examples
|
|
467
508
|
- [Frames](https://semiotic.nteract.io/frames) — full Frame API reference
|
|
468
509
|
- [Features](https://semiotic.nteract.io/features) — axes, annotations, tooltips, styling, Vega-Lite translator
|
|
469
510
|
- [Cookbook](https://semiotic.nteract.io/cookbook) — advanced patterns and recipes
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
// Maintenance note: when you change the agent-facing rules in this
|
|
4
|
+
// file (especially anything in CONTRACTS), update the parallel `rules`
|
|
5
|
+
// array in `context7.json` so the Context7 index stays in sync. The
|
|
6
|
+
// `check:context7` gate validates format (255-char-per-rule limit,
|
|
7
|
+
// folder references, sub-path drift vs `package.json` exports) but
|
|
8
|
+
// can't detect *content* drift between this file and the manifest —
|
|
9
|
+
// keeping them aligned is part of the same edit.
|
|
10
|
+
|
|
11
|
+
const path = require("path")
|
|
12
|
+
const fs = require("fs")
|
|
13
|
+
|
|
14
|
+
const DOC_MARKER_START = "<!-- semiotic-behavior-contracts:start -->"
|
|
15
|
+
const DOC_MARKER_END = "<!-- semiotic-behavior-contracts:end -->"
|
|
16
|
+
|
|
17
|
+
// Components whose static config requires `data` are derived from
|
|
18
|
+
// `ai/schema.json` rather than maintained as a hand-curated list. The schema
|
|
19
|
+
// already declares which components require data — duplicating that here led
|
|
20
|
+
// to drift (Heatmap, FunnelChart, MinimapChart, ScatterplotMatrix, and the
|
|
21
|
+
// hierarchy charts were schema-required but missing from the local list,
|
|
22
|
+
// which made `dataRequiredForUsageMode` incorrectly return `false` and
|
|
23
|
+
// suppressed the "data is required" error in --doctor / MCP diagnoseConfig
|
|
24
|
+
// even when usageMode wasn't `push`).
|
|
25
|
+
//
|
|
26
|
+
// `STATIC_DATA_COMPONENTS` stays exported as a Set for test/legacy callers
|
|
27
|
+
// that probe the surface, and is rebuilt from disk at module load time.
|
|
28
|
+
function loadStaticDataComponentsFromSchema() {
|
|
29
|
+
// Source layout has this file at `<repo>/ai/behaviorContracts.cjs` and the
|
|
30
|
+
// schema at `<repo>/ai/schema.json` — `__dirname` works directly.
|
|
31
|
+
// The MCP server bundles this module into `<repo>/ai/dist/mcp-server.js`
|
|
32
|
+
// via esbuild, so when invoked from there `__dirname` is `<repo>/ai/dist/`
|
|
33
|
+
// and the schema lives one directory up. Try both layouts; use whichever
|
|
34
|
+
// resolves first.
|
|
35
|
+
const candidates = [
|
|
36
|
+
path.join(__dirname, "schema.json"),
|
|
37
|
+
path.join(__dirname, "..", "schema.json"),
|
|
38
|
+
]
|
|
39
|
+
for (const schemaPath of candidates) {
|
|
40
|
+
try {
|
|
41
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"))
|
|
42
|
+
const out = new Set()
|
|
43
|
+
for (const tool of schema.tools || []) {
|
|
44
|
+
const required = tool.function?.parameters?.required || []
|
|
45
|
+
if (required.includes("data")) out.add(tool.function.name)
|
|
46
|
+
}
|
|
47
|
+
if (out.size > 0) return out
|
|
48
|
+
} catch {
|
|
49
|
+
// try next candidate
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Defensive fallback: if schema.json is unavailable (e.g. unusual install
|
|
53
|
+
// layout), fall back to an empty set so callers fail safe — they'll see
|
|
54
|
+
// "data not required" everywhere, which is permissive but won't crash.
|
|
55
|
+
return new Set()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const REQUIRED_COMBINATIONS = [
|
|
59
|
+
{
|
|
60
|
+
component: "StackedAreaChart",
|
|
61
|
+
required: ["areaBy"],
|
|
62
|
+
staticRequired: ["data", "areaBy"],
|
|
63
|
+
pushRequired: ["areaBy"],
|
|
64
|
+
summary: "Stacked areas need a flat data array plus areaBy to identify the stacked series.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
component: "BubbleChart",
|
|
68
|
+
required: ["sizeBy"],
|
|
69
|
+
staticRequired: ["data", "sizeBy"],
|
|
70
|
+
pushRequired: ["sizeBy"],
|
|
71
|
+
summary: "Bubbles need sizeBy in addition to x/y accessors so radius encodes data rather than a constant point size.",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
component: "StackedBarChart",
|
|
75
|
+
required: ["stackBy"],
|
|
76
|
+
staticRequired: ["data", "stackBy"],
|
|
77
|
+
pushRequired: ["stackBy"],
|
|
78
|
+
summary: "Stacked bars need stackBy to split each category into stack segments.",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
component: "GroupedBarChart",
|
|
82
|
+
required: ["groupBy"],
|
|
83
|
+
staticRequired: ["data", "groupBy"],
|
|
84
|
+
pushRequired: ["groupBy"],
|
|
85
|
+
summary: "Grouped bars need groupBy to split each category into side-by-side bars.",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
component: "SwimlaneChart",
|
|
89
|
+
required: ["subcategoryAccessor"],
|
|
90
|
+
staticRequired: ["data", "subcategoryAccessor"],
|
|
91
|
+
pushRequired: ["subcategoryAccessor"],
|
|
92
|
+
summary: "Swimlanes need subcategoryAccessor; colorBy defaults to the same field when not provided.",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
component: "GaugeChart",
|
|
96
|
+
required: ["value"],
|
|
97
|
+
staticRequired: ["value"],
|
|
98
|
+
pushRequired: [],
|
|
99
|
+
summary: "GaugeChart is value-only. thresholds, min, max, sweep, and arcWidth are optional.",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
component: "ForceDirectedGraph",
|
|
103
|
+
required: ["nodes", "edges"],
|
|
104
|
+
staticRequired: ["nodes", "edges"],
|
|
105
|
+
pushRequired: ["nodes", "edges"],
|
|
106
|
+
summary: "ForceDirectedGraph schema/rendering requires nodes and edges. If an agent infers nodes from edge endpoints, it must materialize a nodes array before returning code.",
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
const PUSH_MODE_COMPONENTS = [
|
|
111
|
+
"LineChart",
|
|
112
|
+
"AreaChart",
|
|
113
|
+
"StackedAreaChart",
|
|
114
|
+
"Scatterplot",
|
|
115
|
+
"BubbleChart",
|
|
116
|
+
"ConnectedScatterplot",
|
|
117
|
+
"BarChart",
|
|
118
|
+
"StackedBarChart",
|
|
119
|
+
"GroupedBarChart",
|
|
120
|
+
"SwarmPlot",
|
|
121
|
+
"BoxPlot",
|
|
122
|
+
"Histogram",
|
|
123
|
+
"ViolinPlot",
|
|
124
|
+
"RidgelinePlot",
|
|
125
|
+
"DotPlot",
|
|
126
|
+
"PieChart",
|
|
127
|
+
"DonutChart",
|
|
128
|
+
"LikertChart",
|
|
129
|
+
"SwimlaneChart",
|
|
130
|
+
"ForceDirectedGraph",
|
|
131
|
+
"SankeyDiagram",
|
|
132
|
+
"ChordDiagram",
|
|
133
|
+
"ProportionalSymbolMap",
|
|
134
|
+
"DistanceCartogram",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
const STATIC_DATA_COMPONENTS = loadStaticDataComponentsFromSchema()
|
|
138
|
+
|
|
139
|
+
const BEHAVIOR_CONTRACTS = [
|
|
140
|
+
{
|
|
141
|
+
id: "props.data-required-by-usage-mode",
|
|
142
|
+
category: "required-props",
|
|
143
|
+
title: "Data required by usage mode",
|
|
144
|
+
severity: "error",
|
|
145
|
+
appliesTo: {
|
|
146
|
+
components: PUSH_MODE_COMPONENTS,
|
|
147
|
+
},
|
|
148
|
+
summary: "Static usage (`renderChart`, MCP previews, SSR snapshots, and copy/paste examples with immediate data) requires data in props. React push mode selects live ingestion by omitting data and mutating through a ref.",
|
|
149
|
+
agentAction: "Pass usageMode=\"push\" to `semiotic-ai --doctor` when validating ref-based JSX with no data prop. Keep usageMode=\"static\" or omit it for renderChart/MCP/static configs where data must be present.",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "color.category-precedence",
|
|
153
|
+
category: "color",
|
|
154
|
+
title: "Categorical color precedence",
|
|
155
|
+
severity: "info",
|
|
156
|
+
appliesTo: {
|
|
157
|
+
propsAny: ["colorBy", "colorScheme"],
|
|
158
|
+
},
|
|
159
|
+
summary: "When colorBy is set, CategoryColorProvider/LinkedCharts category maps win for mapped categories. Unmapped categories fall back to explicit colorScheme, then ThemeProvider colors.categorical, then the built-in categorical fallback.",
|
|
160
|
+
agentAction: "Use colorBy for categorical encodings. Use CategoryColorProvider or LinkedCharts for cross-chart consistency, colorScheme for per-chart fallback palettes, and avoid frameProps style functions unless intentionally bypassing HOC color resolution.",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: "props.required-combinations",
|
|
164
|
+
category: "required-props",
|
|
165
|
+
title: "Required prop combinations",
|
|
166
|
+
severity: "error",
|
|
167
|
+
appliesTo: {
|
|
168
|
+
components: REQUIRED_COMBINATIONS.map((entry) => entry.component),
|
|
169
|
+
},
|
|
170
|
+
summary: "Some chart families need semantic props beyond data. These combinations are enforced by validation/schema for static configs and remain required in push mode unless explicitly noted.",
|
|
171
|
+
agentAction: "Before returning code, check the selected component against the required combinations list. For push mode, omit data but keep semantic props such as areaBy, sizeBy, stackBy, and groupBy.",
|
|
172
|
+
combinations: REQUIRED_COMBINATIONS,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "streaming.push-mode-data",
|
|
176
|
+
category: "streaming",
|
|
177
|
+
title: "Push mode omits data",
|
|
178
|
+
severity: "warning",
|
|
179
|
+
appliesTo: {
|
|
180
|
+
components: PUSH_MODE_COMPONENTS,
|
|
181
|
+
},
|
|
182
|
+
summary: "HOC push mode is selected by omitting the data prop entirely. Passing data={[]} is static empty data and can clear/reinitialize the frame on render.",
|
|
183
|
+
agentAction: "For live charts, create a ref, omit data, then call ref.current.push() or pushMany(). For static renderChart/MCP snapshots, provide data because renderChart cannot push later.",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "streaming.ref-mutations-require-id-accessors",
|
|
187
|
+
category: "streaming",
|
|
188
|
+
title: "Ref mutations need stable IDs",
|
|
189
|
+
severity: "warning",
|
|
190
|
+
appliesTo: {
|
|
191
|
+
components: PUSH_MODE_COMPONENTS,
|
|
192
|
+
},
|
|
193
|
+
summary: "push() and pushMany() can append without IDs, but remove(id) and update(id, updater) require a stable ID accessor: pointIdAccessor for XY/realtime charts, dataIdAccessor for ordinal charts, and nodeIDAccessor/edgeIdAccessor for network operations.",
|
|
194
|
+
agentAction: "When generating code that calls remove() or update(), include the matching ID accessor and make sure pushed rows carry that ID field.",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "rendering.renderchart-static-props",
|
|
198
|
+
category: "rendering",
|
|
199
|
+
title: "renderChart uses static props only",
|
|
200
|
+
severity: "warning",
|
|
201
|
+
appliesTo: {},
|
|
202
|
+
summary: "MCP renderChart and semiotic/server renderChart render a single static SVG/PNG snapshot. Browser-only realtime components and future ref pushes are not renderable through that path.",
|
|
203
|
+
agentAction: "Use renderChart only with renderable HOC components and complete static data. For live behavior, return React code with a ref and do not promise MCP-rendered output.",
|
|
204
|
+
},
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
function hasOwn(value, key) {
|
|
208
|
+
return Object.prototype.hasOwnProperty.call(value, key)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function normalizeProps(props) {
|
|
212
|
+
return props && typeof props === "object" && !Array.isArray(props) ? props : {}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function appliesToComponent(contract, component) {
|
|
216
|
+
if (!component) return !contract.appliesTo?.components
|
|
217
|
+
const components = contract.appliesTo?.components
|
|
218
|
+
return !components || components.includes(component)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function appliesToProps(contract, props) {
|
|
222
|
+
const propsAny = contract.appliesTo?.propsAny
|
|
223
|
+
if (!propsAny || propsAny.length === 0) return true
|
|
224
|
+
return propsAny.some((prop) => hasOwn(props, prop) && props[prop] !== undefined)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function behaviorContractsFor({ component, props } = {}) {
|
|
228
|
+
const normalizedProps = normalizeProps(props)
|
|
229
|
+
return BEHAVIOR_CONTRACTS.filter((contract) =>
|
|
230
|
+
appliesToComponent(contract, component) && appliesToProps(contract, normalizedProps)
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeUsageMode(usageMode) {
|
|
235
|
+
if (usageMode === "push") return "push"
|
|
236
|
+
if (usageMode === "static" || usageMode === "renderChart" || usageMode === "server") return "static"
|
|
237
|
+
return "static"
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function dataRequiredForUsageMode(component, usageMode) {
|
|
241
|
+
if (!STATIC_DATA_COMPONENTS.has(component)) return false
|
|
242
|
+
if (normalizeUsageMode(usageMode) === "push" && PUSH_MODE_COMPONENTS.includes(component)) return false
|
|
243
|
+
return true
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function requiredCombinationsFor(component) {
|
|
247
|
+
return REQUIRED_COMBINATIONS.filter((entry) => !component || entry.component === component)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function formatRequiredCombination(entry) {
|
|
251
|
+
const staticRequired = entry.staticRequired || entry.required
|
|
252
|
+
const pushRequired = entry.pushRequired || entry.required.filter((prop) => prop !== "data")
|
|
253
|
+
const pushText = pushRequired.length > 0 ? pushRequired.join(" + ") : "not supported"
|
|
254
|
+
return `${entry.component}: static ${staticRequired.join(" + ")}; push ${pushText}. ${entry.summary}`
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function formatDoctorBehaviorContracts(contracts) {
|
|
258
|
+
if (!contracts || contracts.length === 0) return ""
|
|
259
|
+
|
|
260
|
+
const lines = ["Behavior contracts:"]
|
|
261
|
+
for (const contract of contracts) {
|
|
262
|
+
lines.push(` - [${contract.id}] ${contract.summary}`)
|
|
263
|
+
if (contract.combinations) {
|
|
264
|
+
for (const combo of contract.combinations) {
|
|
265
|
+
lines.push(` ${formatRequiredCombination(combo)}`)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
lines.push(` Action: ${contract.agentAction}`)
|
|
269
|
+
}
|
|
270
|
+
return lines.join("\n")
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function formatBehaviorContractsMarkdown({ compact = false } = {}) {
|
|
274
|
+
const lines = [
|
|
275
|
+
compact ? "## Behavior Contracts" : "## AI Behavior Contracts",
|
|
276
|
+
"",
|
|
277
|
+
DOC_MARKER_START,
|
|
278
|
+
"",
|
|
279
|
+
"These rules are generated from `ai/behaviorContracts.cjs` and are consumed by `semiotic-ai --doctor`, MCP resources, and docs checks.",
|
|
280
|
+
"",
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
for (const contract of BEHAVIOR_CONTRACTS) {
|
|
284
|
+
lines.push(`- **${contract.title}** (\`${contract.id}\`): ${contract.summary}`)
|
|
285
|
+
if (!compact) {
|
|
286
|
+
lines.push(` Agent action: ${contract.agentAction}`)
|
|
287
|
+
}
|
|
288
|
+
if (contract.combinations) {
|
|
289
|
+
const combos = contract.combinations.map(formatRequiredCombination).join(" ")
|
|
290
|
+
lines.push(` Required combinations: ${combos}`)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
lines.push("", DOC_MARKER_END)
|
|
295
|
+
return lines.join("\n")
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = {
|
|
299
|
+
BEHAVIOR_CONTRACTS,
|
|
300
|
+
DOC_MARKER_END,
|
|
301
|
+
DOC_MARKER_START,
|
|
302
|
+
PUSH_MODE_COMPONENTS,
|
|
303
|
+
REQUIRED_COMBINATIONS,
|
|
304
|
+
STATIC_DATA_COMPONENTS,
|
|
305
|
+
behaviorContractsFor,
|
|
306
|
+
dataRequiredForUsageMode,
|
|
307
|
+
formatBehaviorContractsMarkdown,
|
|
308
|
+
formatDoctorBehaviorContracts,
|
|
309
|
+
normalizeUsageMode,
|
|
310
|
+
requiredCombinationsFor,
|
|
311
|
+
}
|