semiotic 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +190 -227
- package/README.md +44 -14
- package/ai/cli.js +41 -0
- package/ai/componentMetadata.cjs +11 -2
- package/ai/dist/mcp-server.js +209 -6
- package/ai/examples.md +98 -0
- package/ai/schema.json +581 -1
- package/ai/system-prompt.md +5 -2
- package/dist/components/AccessibleNavTree.d.ts +25 -0
- package/dist/components/Annotation.d.ts +40 -14
- package/dist/components/ChartContainer.d.ts +32 -2
- package/dist/components/ai/annotationProvenance.d.ts +349 -0
- package/dist/components/ai/audienceProfile.d.ts +60 -3
- package/dist/components/ai/chartCapabilityTypes.d.ts +60 -2
- package/dist/components/ai/chartRoles.d.ts +27 -0
- package/dist/components/ai/conversationArc.d.ts +379 -0
- package/dist/components/ai/dataScaleProfile.d.ts +320 -0
- package/dist/components/ai/describeChart.d.ts +114 -0
- package/dist/components/ai/navigationTree.d.ts +45 -0
- package/dist/components/ai/readerGrounding.d.ts +70 -0
- package/dist/components/ai/suggestCharts.d.ts +34 -1
- package/dist/components/ai/useConversationArc.d.ts +89 -0
- package/dist/components/ai/useNavigationSync.d.ts +61 -0
- package/dist/components/ai/variantDiscovery.d.ts +168 -0
- package/dist/components/charts/realtime/RealtimeHeatmap.d.ts +3 -0
- package/dist/components/charts/realtime/RealtimeHistogram.d.ts +3 -0
- package/dist/components/charts/realtime/RealtimeLineChart.d.ts +3 -0
- package/dist/components/charts/realtime/RealtimeSwarmChart.d.ts +3 -0
- package/dist/components/charts/realtime/RealtimeWaterfallChart.d.ts +3 -0
- package/dist/components/charts/shared/annotationHierarchy.d.ts +42 -0
- package/dist/components/charts/shared/annotationResolvers.d.ts +3 -2
- package/dist/components/charts/shared/annotationRules.d.ts +16 -0
- package/dist/components/charts/shared/annotationTypes.d.ts +14 -0
- package/dist/components/charts/shared/auditAccessibility.d.ts +90 -0
- package/dist/components/charts/shared/chartSpecs.d.ts +2 -3
- package/dist/components/charts/shared/diagnoseConfig.d.ts +4 -6
- package/dist/components/charts/shared/selectionUtils.d.ts +5 -2
- package/dist/components/charts/shared/streamPropsHelpers.d.ts +2 -0
- package/dist/components/charts/shared/types.d.ts +5 -1
- package/dist/components/charts/value/BigNumber.capability.d.ts +13 -0
- package/dist/components/charts/value/BigNumber.d.ts +14 -0
- package/dist/components/charts/value/formatting.d.ts +40 -0
- package/dist/components/charts/value/thresholdSparkline.d.ts +40 -0
- package/dist/components/charts/value/types.d.ts +292 -0
- package/dist/components/realtime/lifecycleBands.d.ts +44 -0
- package/dist/components/realtime/types.d.ts +23 -8
- package/dist/components/recipes/annotationDensity.d.ts +69 -0
- package/dist/components/recipes/annotationLayout.d.ts +93 -0
- package/dist/components/semiotic-ai.d.ts +38 -15
- package/dist/components/semiotic-realtime.d.ts +2 -0
- package/dist/components/semiotic-recipes.d.ts +4 -0
- package/dist/components/semiotic-utils.d.ts +8 -0
- package/dist/components/semiotic-value.d.ts +55 -0
- package/dist/components/semiotic.d.ts +7 -0
- package/dist/components/server/staticAnnotations.d.ts +2 -0
- package/dist/components/stream/AccessibleDataTable.d.ts +10 -1
- package/dist/components/stream/NetworkSVGOverlay.d.ts +11 -5
- package/dist/components/stream/OrdinalSVGOverlay.d.ts +2 -0
- package/dist/components/stream/SVGOverlay.d.ts +2 -0
- package/dist/components/stream/geoTypes.d.ts +3 -0
- package/dist/components/stream/networkTypes.d.ts +2 -0
- package/dist/components/stream/ordinalTypes.d.ts +2 -0
- package/dist/components/stream/types.d.ts +2 -0
- package/dist/geo.min.js +1 -1
- package/dist/geo.module.min.js +1 -1
- package/dist/network.min.js +1 -1
- package/dist/network.module.min.js +1 -1
- package/dist/ordinal.min.js +1 -1
- package/dist/ordinal.module.min.js +1 -1
- package/dist/realtime.min.js +1 -1
- package/dist/realtime.module.min.js +1 -1
- package/dist/semiotic-ai.d.ts +38 -15
- package/dist/semiotic-ai.min.js +1 -1
- package/dist/semiotic-ai.module.min.js +1 -1
- package/dist/semiotic-realtime.d.ts +2 -0
- package/dist/semiotic-recipes.d.ts +4 -0
- package/dist/semiotic-recipes.min.js +1 -1
- package/dist/semiotic-recipes.module.min.js +1 -1
- package/dist/semiotic-themes.min.js +1 -1
- package/dist/semiotic-themes.module.min.js +1 -1
- package/dist/semiotic-utils.d.ts +8 -0
- package/dist/semiotic-utils.min.js +1 -1
- package/dist/semiotic-utils.module.min.js +1 -1
- package/dist/semiotic-value.d.ts +55 -0
- package/dist/semiotic-value.min.js +2 -0
- package/dist/semiotic-value.module.min.js +2 -0
- package/dist/semiotic.d.ts +7 -0
- package/dist/semiotic.min.js +1 -1
- package/dist/semiotic.module.min.js +1 -1
- package/dist/server.min.js +1 -1
- package/dist/server.module.min.js +1 -1
- package/dist/xy.min.js +1 -1
- package/dist/xy.module.min.js +1 -1
- package/package.json +18 -4
package/CLAUDE.md
CHANGED
|
@@ -4,56 +4,53 @@
|
|
|
4
4
|
- Install: `npm install semiotic`
|
|
5
5
|
<!-- semiotic-bundle-sizes:start -->
|
|
6
6
|
<!-- Auto-generated by scripts/sync-bundle-sizes.mjs — do not edit by hand. -->
|
|
7
|
-
- **Use sub-path imports** — `semiotic/xy` (
|
|
7
|
+
- **Use sub-path imports** — `semiotic/xy` (90KB gz), `semiotic/ordinal` (74KB gz), `semiotic/network` (68KB gz), `semiotic/geo` (55KB gz), `semiotic/realtime` (95KB gz), `semiotic/server` (127KB gz), `semiotic/utils` (37KB gz), `semiotic/recipes` (9KB gz), `semiotic/themes` (4KB gz), `semiotic/data` (3KB gz), `semiotic/value` (6KB gz), `semiotic/ai` (246KB gz). Full `semiotic` is 203KB gz.
|
|
8
8
|
<!-- semiotic-bundle-sizes:end -->
|
|
9
|
-
- CLI: `npx semiotic-ai [--schema|--compact|--examples|--doctor]`
|
|
10
|
-
- MCP: `npx semiotic-mcp`
|
|
9
|
+
- CLI: `npx semiotic-ai [--schema|--compact|--examples|--doctor|--audit-a11y]` · MCP: `npx semiotic-mcp`
|
|
11
10
|
|
|
12
11
|
## Architecture
|
|
13
|
-
|
|
14
|
-
- **Always use HOC charts** unless you need control they don't expose. Stream Frames pass `RealtimeNode`/`RealtimeEdge` wrappers in callbacks, not your data.
|
|
15
|
-
- Every HOC accepts `frameProps` for pass-through. TypeScript `strict: true`. Every HOC has error boundary + dev-mode validation.
|
|
12
|
+
HOC Charts (simple, default) → Stream Frames (full control). Use HOCs unless you need control they don't expose. Stream Frames pass `RealtimeNode`/`RealtimeEdge` wrappers in callbacks, not your data. Every HOC accepts `frameProps`, has an error boundary + dev-mode validation. TypeScript `strict: true`.
|
|
16
13
|
|
|
17
14
|
## Common Props (all HOCs)
|
|
18
|
-
`title`, `description` (aria-label), `summary` (sr-only), `width` (600), `height` (400), `responsiveWidth`, `responsiveHeight`, `margin`, `className`, `color` (uniform fill), `stroke
|
|
15
|
+
`title`, `description` (aria-label), `summary` (sr-only), `width` (600), `height` (400), `responsiveWidth`, `responsiveHeight`, `margin`, `className`, `color` (uniform fill), `stroke`, `strokeWidth`, `opacity`, `enableHover` (true), `tooltip` (boolean | "multi" | function | config), `showLegend`, `showGrid` (false), `frameProps`, `onObservation`, `onClick`, `chartId`, `loading`, `loadingContent` (ReactNode; `false` suppresses), `emptyContent`, `legendInteraction` ("none"|"highlight"|"isolate"), `legendPosition` ("right"|"left"|"top"|"bottom"), `emphasis` ("primary"|"secondary"), `annotations`, `accessibleTable` (true), `hoverHighlight` (requires `colorBy`), `hoverRadius` (30), `animate` (boolean | {duration?, easing?, intro?}), `axisExtent` ("nice"|"exact" — pins first/last tick to data min/max; XY x/y + ordinal value axis only).
|
|
19
16
|
|
|
20
|
-
**Primitive styling
|
|
17
|
+
**Primitive styling** (`color`/`stroke`/`strokeWidth`/`opacity`): apply to any shape the chart draws. Precedence: top-level prop > `frameProps.*Style` fn return > HOC base > theme. Use CSS vars (`stroke="var(--semiotic-border)"`) for cascade-overridable theming. Per-datum: use `frameProps.pieceStyle`/`pointStyle`/`lineStyle` fn form.
|
|
21
18
|
|
|
22
19
|
`onClick` receives `(datum, { x, y })`. `onObservation` receives `{ type, datum?, x?, y?, timestamp, chartType, chartId }`.
|
|
23
20
|
|
|
24
21
|
## XY Charts (`semiotic/xy`)
|
|
25
22
|
|
|
26
|
-
**LineChart** — `data`, `xAccessor` ("x"), `yAccessor` ("y"), `lineBy`, `lineDataAccessor`, `colorBy`, `colorScheme`, `curve`, `lineWidth` (2), `showPoints`, `pointRadius` (3), `fillArea` (boolean|string[]), `areaOpacity` (0.3), `lineGradient`, `anomaly`, `forecast`, `band` (
|
|
27
|
-
**AreaChart** — LineChart props + `areaBy`, `y0Accessor`, `gradientFill`, `areaOpacity` (0.7), `showLine` (true), `band
|
|
28
|
-
**DifferenceChart** — Two-series A/B
|
|
29
|
-
**StackedAreaChart** — flat array + `areaBy` (required), `colorBy`, `normalize`, `baseline` (
|
|
30
|
-
**Scatterplot** — `
|
|
23
|
+
**LineChart** — `data`, `xAccessor` ("x"), `yAccessor` ("y"), `lineBy`, `lineDataAccessor`, `colorBy`, `colorScheme`, `curve`, `lineWidth` (2), `showPoints`, `pointRadius` (3), `fillArea` (boolean|string[]), `areaOpacity` (0.3), `lineGradient`, `anomaly`, `forecast`, `band` ({y0Accessor, y1Accessor, style?, perSeries?, interactive?} or array for fan charts; participates in yExtent; non-interactive by default), `directLabel`, `gapStrategy`, `xScaleType`/`yScaleType` ("linear"|"log"|"time"), `tooltip="multi"` for hover-anywhere
|
|
24
|
+
**AreaChart** — LineChart props + `areaBy`, `y0Accessor`, `gradientFill`, `areaOpacity` (0.7), `showLine` (true), `band`, `tooltip="multi"`
|
|
25
|
+
**DifferenceChart** — Two-series A/B. Fills between with `seriesAColor` where A>B, `seriesBColor` where B>A; crossovers interpolated. `data`, `xAccessor`, `seriesAAccessor` ("a"), `seriesBAccessor` ("b"), `seriesALabel`/`seriesBLabel`, `seriesAColor` (var(--semiotic-danger))/`seriesBColor` (var(--semiotic-info)), `showLines` (true), `lineWidth` (1.5), `showPoints` (false), `pointRadius` (3), `curve` ("linear"), `areaOpacity` (0.6), `gradientFill`, `xExtent`/`yExtent`, `pointIdAccessor`, `windowSize`. Push via `ref.push({x,a,b})`. Accessor outputs coerce through `toNumber`.
|
|
26
|
+
**StackedAreaChart** — flat array + `areaBy` (required), `colorBy`, `normalize`, `baseline` ("zero"|"wiggle" streamgraph|"silhouette" centered), `stackOrder` ("key"|"insideOut"|"asc"|"desc"). Streamgraph: `baseline="wiggle"` + `stackOrder="insideOut"`. `baseline` ⊥ `normalize`. No `lineBy`. `tooltip="multi"` interpolates between samples.
|
|
27
|
+
**Scatterplot** — `xAccessor`, `yAccessor`, `colorBy`, `sizeBy`, `sizeRange`, `pointRadius` (5), `pointOpacity` (0.8), `marginalGraphics`, `regression` (boolean | "linear"|"polynomial"|"loess" | RegressionConfig — sugar for trend overlay)
|
|
31
28
|
**BubbleChart** — Scatterplot + `sizeBy` (required), `sizeRange` ([5,40]), `regression`
|
|
32
29
|
**ConnectedScatterplot** — + `orderAccessor`, `regression`
|
|
33
|
-
**QuadrantChart** — Scatterplot +
|
|
34
|
-
**MultiAxisLineChart** — Dual Y-axis. `series` (
|
|
35
|
-
**Heatmap** — `
|
|
36
|
-
**ScatterplotMatrix** — `
|
|
30
|
+
**QuadrantChart** — Scatterplot + `quadrants`, `xCenter`, `yCenter`
|
|
31
|
+
**MultiAxisLineChart** — Dual Y-axis. `series` (`[{yAccessor, label?, color?, format?, extent?}]`). Falls back to multi-line if ≠ 2 series.
|
|
32
|
+
**Heatmap** — `xAccessor`, `yAccessor`, `valueAccessor`, `colorScheme`, `showValues`, `cellBorderColor`
|
|
33
|
+
**ScatterplotMatrix** — `fields` (numeric field names)
|
|
37
34
|
**MinimapChart** — Overview + detail with linked zoom. Wraps an XY chart.
|
|
38
|
-
**CandlestickChart** — `
|
|
35
|
+
**CandlestickChart** — `xAccessor`, `highAccessor` (req), `lowAccessor` (req), `openAccessor`+`closeAccessor` (optional → OHLC; high/low only → range). `candlestickStyle` ({upColor, downColor, wickColor, rangeColor, bodyWidth, wickWidth}). Honors `mode`.
|
|
39
36
|
|
|
40
37
|
## Ordinal Charts (`semiotic/ordinal`)
|
|
41
38
|
|
|
42
|
-
**BarChart** — `
|
|
43
|
-
**StackedBarChart** — + `stackBy` (required), `normalize`, `sort` (default
|
|
44
|
-
**GroupedBarChart** — + `groupBy` (required), `barPadding` (60), `sort` (
|
|
39
|
+
**BarChart** — `categoryAccessor`, `valueAccessor`, `orientation`, `colorBy`, `sort`, `barPadding` (40), `roundedTop`, `gradientFill` (true | {topOpacity, bottomOpacity} | {colorStops}; tip→base), `regression`
|
|
40
|
+
**StackedBarChart** — + `stackBy` (required), `normalize`, `sort` (false default — insertion order)
|
|
41
|
+
**GroupedBarChart** — + `groupBy` (required), `barPadding` (60), `sort` (false default)
|
|
45
42
|
**SwarmPlot** — `colorBy`, `sizeBy`, `pointRadius`, `pointOpacity`
|
|
46
43
|
**BoxPlot** — + `showOutliers`, `outlierRadius`
|
|
47
44
|
**Histogram** — + `bins` (25), `relative`. Always horizontal.
|
|
48
45
|
**ViolinPlot** — + `bins`, `curve`, `showIQR`
|
|
49
46
|
**RidgelinePlot** — + `bins`, `amplitude` (1.5)
|
|
50
|
-
**DotPlot** — + `sort` ("auto"
|
|
47
|
+
**DotPlot** — + `sort` ("auto"), `dotRadius`, `showGrid` (true default), `regression`
|
|
51
48
|
**PieChart** — `categoryAccessor`, `valueAccessor`, `colorBy`, `startAngle`
|
|
52
49
|
**DonutChart** — PieChart + `innerRadius` (60), `centerContent`
|
|
53
|
-
**FunnelChart** — `stepAccessor`, `valueAccessor`, `categoryAccessor
|
|
54
|
-
**SwimlaneChart** — `categoryAccessor`, `subcategoryAccessor` (
|
|
55
|
-
**LikertChart** — `categoryAccessor`, `valueAccessor`|`levelAccessor`+`countAccessor`,
|
|
56
|
-
**GaugeChart** — `value` (
|
|
50
|
+
**FunnelChart** — `stepAccessor`, `valueAccessor`, `categoryAccessor?`, `connectorOpacity`, `orientation`
|
|
51
|
+
**SwimlaneChart** — `categoryAccessor`, `subcategoryAccessor` (req), `valueAccessor`, `colorBy` (defaults to subcategoryAccessor), `orientation`, `roundedTop` (pixel radius on outer ends of each lane; middles stay square; single-segment lanes round all four)
|
|
52
|
+
**LikertChart** — `categoryAccessor`, `valueAccessor`|`levelAccessor`+`countAccessor`, `levels?`, `orientation`, `colorScheme`
|
|
53
|
+
**GaugeChart** — `value` (req), `min`, `max`, `thresholds`, `arcWidth`, `cornerRadius` (rounded segment ends), `sweep`, `fillZones`, `showNeedle`, `centerContent`
|
|
57
54
|
|
|
58
55
|
All ordinal: `colorBy`, `colorScheme`, `categoryFormat` (string|ReactNode), `showCategoryTicks` (true).
|
|
59
56
|
|
|
@@ -61,7 +58,7 @@ All ordinal: `colorBy`, `colorScheme`, `categoryFormat` (string|ReactNode), `sho
|
|
|
61
58
|
|
|
62
59
|
**ForceDirectedGraph** — `nodes`, `edges`, `nodeIDAccessor`, `sourceAccessor`, `targetAccessor`, `colorBy`, `nodeSize`, `nodeSizeRange`, `edgeWidth`, `iterations` (300), `forceStrength` (0.1), `showLabels`, `nodeLabel`
|
|
63
60
|
**SankeyDiagram** — `edges`, `nodes`, `valueAccessor`, `nodeIdAccessor`, `colorBy`, `edgeColorBy`, `orientation`, `nodeAlign`, `nodeWidth`, `nodePaddingRatio`, `showLabels`
|
|
64
|
-
**ProcessSankey** — temporal sankey with
|
|
61
|
+
**ProcessSankey** — temporal sankey with real time x-axis. `nodes`, `edges` (each with `startTime`/`endTime`), `domain` (req `[t0, t1]`), `axisTicks?`, `xExtentAccessor` (optional `[start, end]` lifetime per node), `colorBy`/`colorScheme`/`showLegend`/`legendPosition`, `pairing` ("value"|"temporal"), `packing` ("off"|"reuse"), `laneOrder` ("crossing-min"|"inside-out"|"crossing-min+inside-out"|"insertion"), `lifetimeMode` ("full"|"half"), `ribbonLane` ("source"|"target"|"both"), `showLaneRails`, `showLabels` (true), `showQualityReadout`, `showParticles` + `particleStyle`, `timeFormat`/`valueFormat`, push API via ref. Static-graph cycles OK as long as edges move forward in time. Use ProcessSankey for time-stamped events; SankeyDiagram for static snapshots.
|
|
65
62
|
**ChordDiagram** — `edges`, `nodes`, `valueAccessor`, `edgeColorBy`, `padAngle`, `showLabels`
|
|
66
63
|
**TreeDiagram** — `data` (root), `layout`, `orientation`, `childrenAccessor`, `colorBy`, `colorByDepth`
|
|
67
64
|
**Treemap** — `data` (root), `childrenAccessor`, `valueAccessor`, `colorBy`, `colorByDepth`, `showLabels`
|
|
@@ -70,21 +67,40 @@ All ordinal: `colorBy`, `colorScheme`, `categoryFormat` (string|ReactNode), `sho
|
|
|
70
67
|
|
|
71
68
|
## Geo Charts (`semiotic/geo`)
|
|
72
69
|
|
|
73
|
-
Import from `semiotic/geo`
|
|
70
|
+
Import from `semiotic/geo` only — avoids d3-geo in non-geo bundles.
|
|
74
71
|
|
|
75
72
|
**ChoroplethMap** — `areas` (GeoJSON Feature[] or "world-110m"), `valueAccessor`, `colorScheme`, `projection` ("equalEarth"), `graticule`, `tooltip`, `showLegend`
|
|
76
|
-
**ProportionalSymbolMap** — `points`, `xAccessor` ("lon"), `yAccessor` ("lat"), `sizeBy`, `sizeRange`, `colorBy`, `areas
|
|
73
|
+
**ProportionalSymbolMap** — `points`, `xAccessor` ("lon"), `yAccessor` ("lat"), `sizeBy`, `sizeRange`, `colorBy`, `areas?`
|
|
77
74
|
**FlowMap** — `flows`, `nodes`, `valueAccessor`, `edgeColorBy`, `lineType`, `showParticles`
|
|
78
75
|
**DistanceCartogram** — `points`, `center`, `costAccessor`, `strength`, `showRings`
|
|
79
76
|
|
|
80
|
-
All geo: `fitPadding`, `zoomable`, `zoomExtent`, `onZoom`, `dragRotate`, `graticule`, `tileURL`, `tileAttribution`
|
|
81
|
-
|
|
77
|
+
All geo: `fitPadding`, `zoomable`, `zoomExtent`, `onZoom`, `dragRotate`, `graticule`, `tileURL`, `tileAttribution`. Helpers: `resolveReferenceGeography("world-110m"|"world-50m")`, `mergeData(features, data, {featureKey, dataKey})`.
|
|
78
|
+
|
|
79
|
+
## Value Charts (`semiotic/value`)
|
|
80
|
+
|
|
81
|
+
Single-focal-value displays — when one number is the answer, a chart is the wrong abstraction. Plain React (no Stream Frame); SSR-clean; ~7KB gz. **Ships no chart-family dependency** — embed your own Semiotic chart via two slots picked by aspect ratio.
|
|
82
|
+
|
|
83
|
+
**BigNumber** — `value` (required), `label`, `caption`, `format` ("number"|"currency"|"percent"|"compact"|"duration"|fn), `locale`, `currency`, `precision`, `prefix`/`suffix`/`unit`, `comparison` ({value, label?, format?, direction?}), `target` ({value, label?, format?, direction?}), `delta` (explicit override), `deltaFormat`, `showDeltaPercent` (true), `direction` ("higher-is-better" default | "lower-is-better" | "neutral"), `sentiment` ("auto" default | "positive" | "negative" | "neutral"), `thresholds` ([{at, level: "success"|"warning"|"danger"|"info"|"neutral", color?, label?}] — resolved by highest `at` ≤ value, painted via `--semiotic-{level}`), `windowSize` (60 — caps push buffer surfaced via `getData()` / `slotCtx.pushBuffer`), `mode` ("tile" default | "presentation" | "inline" | "thumbnail"), `align`, `padding`, `emphasis`, `color`/`background`/`borderColor`/`borderRadius`, `animate` (boolean | {duration?, easing?, intro?} — tweens between value changes), `stalenessThreshold` (ms; dims after no-push interval), `staleLabel`, slot overrides (`headerSlot`/`valueSlot`/`deltaSlot`/`footerSlot`/`trendSlot`/`chartSlot` — ReactNode or `(ctx) => ReactNode`), `chartSize` (px reserved for `chartSlot`; defaults to inner card height).
|
|
84
|
+
|
|
85
|
+
**Two chart slot positions, picked by chart aspect:**
|
|
86
|
+
- **`trendSlot`** — wide / rectangular charts beneath the value (LineChart, AreaChart, DifferenceChart in `mode="sparkline"`). Renders at full card width.
|
|
87
|
+
- **`chartSlot`** — square charts beside the value (DonutChart, PieChart, Scatterplot, Treemap, CirclePack). Splits the card horizontally: text-on-left, chart-on-right.
|
|
88
|
+
- Pair both: square chart anchors top-right, wide trend stretches across the bottom.
|
|
89
|
+
- Slot context `(ctx) => ReactNode` exposes `{ value, formattedValue, level, color, delta, deltaFormatted, deltaPercent, sentiment, isStale, pushBuffer }` — embedded charts read `ctx.color` to theme-link to the resolved threshold.
|
|
90
|
+
|
|
91
|
+
Push API via `forwardRef`: `ref.current.push(value | {value, time?, comparison?})`, `pushMany`, `clear`, `getValue()`, `getData()`. Stable across renders (refs back the imperative handle).
|
|
92
|
+
|
|
93
|
+
ARIA: auto sentence-form label combining `{label}: {formatted} {unit}, {up|down} {delta} ({percent}) from {comparison.label}, {target%} of {target.label}[, stale]`. Override via `description`; supplement via `summary` (sr-only).
|
|
94
|
+
|
|
95
|
+
Semantic classes: `semiotic-bignumber` root + `--mode-{...}` / `--level-{...}` / `--sentiment-{...}` / `--stale` modifiers; `__text-region`, `__value`, `__delta`, `__delta-row--{up|down|flat}`, `__arrow--{up|down|flat}`, `__trend` (wide slot wrapper), `__chart` (square slot wrapper), etc.
|
|
96
|
+
|
|
97
|
+
Helpers exported: `buildFormatter`, `formatSignedDelta`, `formatDeltaPercent`, `formatDuration`, `resolveThreshold`, `colorForLevel`, `buildSparklinePath` (for custom-slot rendering).
|
|
82
98
|
|
|
83
99
|
## Realtime Charts (`semiotic/realtime`)
|
|
84
100
|
|
|
85
|
-
Push API: `ref.current.push({
|
|
101
|
+
Push API: `ref.current.push({time, value})`. All pushed data must include a time field.
|
|
86
102
|
|
|
87
|
-
**RealtimeLineChart**, **RealtimeHistogram** (+ `brush`, `onBrush`, `linkedBrush`, `direction`), **TemporalHistogram** (static
|
|
103
|
+
**RealtimeLineChart**, **RealtimeHistogram** (+ `brush`, `onBrush`, `linkedBrush`, `direction`), **TemporalHistogram** (static sibling — same props minus `windowSize`/`windowMode`), **RealtimeSwarmChart**, **RealtimeWaterfallChart**, **RealtimeHeatmap**, **Streaming Sankey** (StreamNetworkFrame + `showParticles`).
|
|
88
104
|
|
|
89
105
|
Encoding: `decay`, `pulse`, `transition`, `staleness` — compose freely.
|
|
90
106
|
|
|
@@ -94,105 +110,50 @@ Most HOCs support push via `forwardRef`. **Omit** `data` — do NOT pass `data={
|
|
|
94
110
|
const ref = useRef()
|
|
95
111
|
ref.current.push({ id: "p1", x: 1, y: 2 })
|
|
96
112
|
ref.current.pushMany([...points])
|
|
97
|
-
ref.current.replace([...points]) // ordinal only —
|
|
98
|
-
ref.current.remove("p1")
|
|
99
|
-
ref.current.
|
|
100
|
-
ref.current.update("p1", d => ({ ...d, y: 99 })) // in-place update — requires pointIdAccessor
|
|
113
|
+
ref.current.replace([...points]) // ordinal only — bounded-ingest, preserves category order + transitions
|
|
114
|
+
ref.current.remove("p1" | ["p1","p2"]) // requires ID accessor
|
|
115
|
+
ref.current.update("p1", d => ({ ...d, y: 99 })) // requires ID accessor
|
|
101
116
|
ref.current.clear()
|
|
102
117
|
ref.current.getData()
|
|
103
|
-
ref.current.getScales() //
|
|
118
|
+
ref.current.getScales() // {o, r, projection} (ordinal) | {x, y} (XY) — null if unmounted
|
|
104
119
|
<Scatterplot ref={ref} xAccessor="x" yAccessor="y" pointIdAccessor="id" />
|
|
105
120
|
```
|
|
106
|
-
|
|
121
|
+
ID accessor: `pointIdAccessor` (XY/realtime), `dataIdAccessor` (ordinal), `nodeIDAccessor`/`edgeIdAccessor` (network). `replace()` is ordinal-only — used by aggregator HOCs like LikertChart. Network HOC refs operate on nodes; for edges use `StreamNetworkFrameHandle` directly: `removeNode(id)`, `removeEdge(sourceId, targetId)` or `removeEdge(edgeId)`, `updateNode(id, updater)`, `updateEdge(sourceId, targetId, updater)`.
|
|
107
122
|
Not supported: Tree, Treemap, CirclePack, Orbit, ChoroplethMap, FlowMap, ScatterplotMatrix.
|
|
108
123
|
|
|
109
124
|
## Custom Charts (escape hatch)
|
|
110
125
|
|
|
111
|
-
When the catalog doesn't fit, three HOCs
|
|
112
|
-
|
|
113
|
-
- **`XYCustomChart`** (`semiotic/xy`) — XY layouts: waffle, calendar heatmap, custom point/line/area arrangements
|
|
114
|
-
- **`OrdinalCustomChart`** (`semiotic/ordinal`) — category × value layouts: marimekko, parallel coordinates, bullet, fan chart, slope graph
|
|
115
|
-
- **`NetworkCustomChart`** (`semiotic/network`) — graph layouts: flextree, dagre, custom force/radial
|
|
116
|
-
|
|
117
|
-
All three accept `layout` and `layoutConfig` (your own typed config), but the layout context and return shape differ by chart family:
|
|
118
|
-
|
|
119
|
-
- **`XYCustomChart` / `OrdinalCustomChart`** — `layout: (ctx) => { nodes, overlays? }`. Context exposes `data`, `scales`, `dimensions` (with plot rect — center-anchored for radial ordinal, top-left otherwise), `theme` (semantic + categorical), `resolveColor(key)`, and `config`. XY scales: `{ x, y }` (linear). Ordinal scales: `{ o, r, projection }` (band + linear).
|
|
120
|
-
- **`NetworkCustomChart`** — `layout: (ctx) => { sceneNodes?, sceneEdges?, labels?, overlays? }`. Context exposes `nodes`, `edges`, `dimensions`, `theme`, `resolveColor(key)`, and `config` — graph data, no `data`/`scales`. Network layouts often run an external positioner (`d3-flextree`, `dagre`) on nodes/edges, then emit network scene primitives (`circle`, `rect`, `arc` for nodes; `line`, `bezier`, `curved` for edges).
|
|
121
|
-
|
|
122
|
-
XY/ordinal frames render whatever you put in `nodes` (rect, point, area, line, wedge, connector, etc.). Network frames split node-shaped scenes from edge-shaped scenes — give them `sceneNodes` for the round/rect/arc visuals and `sceneEdges` for the connecting paths. All three frames handle painting, hit testing, accessibility, transitions, decay, and SSR for you.
|
|
123
|
-
|
|
124
|
-
```tsx
|
|
125
|
-
import { XYCustomChart } from "semiotic/xy"
|
|
126
|
-
import { OrdinalCustomChart } from "semiotic/ordinal"
|
|
127
|
-
import { NetworkCustomChart } from "semiotic/network"
|
|
128
|
-
import {
|
|
129
|
-
waffleLayout, calendarLayout, // XY recipes
|
|
130
|
-
marimekkoLayout, bulletLayout, parallelCoordinatesLayout, // ordinal
|
|
131
|
-
flextreeLayout, dagreLayout, // network
|
|
132
|
-
} from "semiotic/recipes"
|
|
133
|
-
|
|
134
|
-
<XYCustomChart data={cells} layout={waffleLayout} layoutConfig={{ rows: 10, columns: 10, ... }} />
|
|
135
|
-
<OrdinalCustomChart data={revenue} layout={marimekkoLayout} layoutConfig={{ ... }} />
|
|
136
|
-
<NetworkCustomChart nodes={nodes} edges={edges} layout={flextreeLayout} layoutConfig={{ ... }} />
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
**Recipes subpath** (`semiotic/recipes`) ships pure layout functions. They emit standard SceneNodes — no chart code. BYO heavy deps (`d3-flextree`, `dagre`) live in user code.
|
|
140
|
-
|
|
141
|
-
### Chrome (labels, axes, legends): the recipe owns it
|
|
142
|
-
|
|
143
|
-
Custom layouts often need chrome the standard chart axes can't render — variable-width bars, per-row independent value scales, parallel axes, asymmetric tree branches. The unified pattern: **the recipe emits its own labels/axes/ticks via the `overlays` return field** (a ReactNode painted on top of the canvas). Built-in axes (via `showAxes` on the HOC) work for layouts that respect the standard scale; everything else, the recipe handles itself.
|
|
144
|
-
|
|
145
|
-
Convention across shipped recipes — every recipe takes a consistent set of label/axis toggles in `layoutConfig`:
|
|
146
|
-
|
|
147
|
-
| Recipe | Toggle | Default | What it draws |
|
|
148
|
-
|---|---|---|---|
|
|
149
|
-
| `marimekkoLayout` | `showCategoryLabels` | `true` | Category names under each variable-width bar |
|
|
150
|
-
| `bulletLayout` | `showLabels` | `true` | Metric name to the left of each row |
|
|
151
|
-
| `bulletLayout` | `showTicks` | `true` | Per-row value-axis ticks (each row independently scaled) |
|
|
152
|
-
| `parallelCoordinatesLayout` | `showAxes` | `true` | Vertical axis line + field name + 5 ticks per axis |
|
|
153
|
-
| `flextreeLayout` / `dagreLayout` | `showLabels` | `true` | Node text rendered inside each rect |
|
|
154
|
-
| `waffleLayout` / `calendarLayout` | — | — | No chrome needed (uniform grid + cell color tells the story) |
|
|
126
|
+
When the catalog doesn't fit, three HOCs take a layout function emitting scene primitives. Frame still owns hit testing, transitions, decay, theme, SSR.
|
|
155
127
|
|
|
156
|
-
|
|
128
|
+
- **`XYCustomChart`** (`semiotic/xy`) — waffle, calendar heatmap, custom point/line/area
|
|
129
|
+
- **`OrdinalCustomChart`** (`semiotic/ordinal`) — marimekko, parallel coords, bullet, fan, slope
|
|
130
|
+
- **`NetworkCustomChart`** (`semiotic/network`) — flextree, dagre, custom force/radial
|
|
157
131
|
|
|
158
|
-
|
|
132
|
+
Layout signature differs by family:
|
|
133
|
+
- **XY/Ordinal**: `layout: (ctx) => { nodes, overlays? }`. `ctx`: `data`, `scales` ({x,y} XY | {o,r,projection} ordinal), `dimensions` (plot rect — center-anchored for radial ordinal, top-left otherwise), `theme`, `resolveColor(key)`, `config`.
|
|
134
|
+
- **Network**: `layout: (ctx) => { sceneNodes?, sceneEdges?, labels?, overlays? }`. `ctx`: `nodes`, `edges`, `dimensions`, `theme`, `resolveColor(key)`, `config`. Run external positioners (`d3-flextree`, `dagre`) then emit network scene primitives (circle/rect/arc nodes; line/bezier/curved edges).
|
|
159
135
|
|
|
160
|
-
|
|
136
|
+
`semiotic/recipes` ships pure layout functions (`waffleLayout`, `calendarLayout`, `marimekkoLayout`, `bulletLayout`, `parallelCoordinatesLayout`, `flextreeLayout`, `dagreLayout`). BYO heavy deps (`d3-flextree`, `dagre`) in user code.
|
|
161
137
|
|
|
162
|
-
|
|
163
|
-
const [hovered, setHovered] = useState(null)
|
|
164
|
-
<OrdinalCustomChart
|
|
165
|
-
data={rows}
|
|
166
|
-
layout={parallelCoordinatesLayout}
|
|
167
|
-
layoutConfig={{
|
|
168
|
-
fields: ["mpg", "hp", "weight"],
|
|
169
|
-
highlightFn: hovered ? (d) => d.name === hovered : undefined,
|
|
170
|
-
}}
|
|
171
|
-
onObservation={(obs) => {
|
|
172
|
-
if (obs.type === "hover") setHovered(obs.datum?.data?.name ?? obs.datum?.name)
|
|
173
|
-
else if (obs.type === "hover-end") setHovered(null)
|
|
174
|
-
}}
|
|
175
|
-
/>
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Built-in chrome works when the layout uses standard scales
|
|
138
|
+
**Chrome (labels/axes/legends): the recipe owns it.** Recipes emit own chrome via `overlays` return field (ReactNode painted on top). Built-in axes via `showAxes` on the HOC work for layouts respecting the standard scale. Recipe convention: `showXxx` boolean toggles, `xxxFormat` callbacks. Shipped recipes' toggles: marimekko `showCategoryLabels`, bullet `showLabels`+`showTicks`, parallelCoordinates `showAxes`, flextree/dagre `showLabels`, waffle/calendar none.
|
|
179
139
|
|
|
180
|
-
|
|
140
|
+
**Interaction (hover/brush/selection): the parent owns it.** Recipes are pure — they take predicate props (e.g. `parallelCoordinatesLayout`'s `highlightFn?`) and the parent manages state via `onObservation` (`{type: "hover" | "hover-end" | ...}`), feeding a derived predicate back into `layoutConfig`. Matching rows render at full opacity; non-matching dim; highlighted z-order on top.
|
|
181
141
|
|
|
182
142
|
**Notes:**
|
|
183
|
-
- Coords are plot-relative (
|
|
184
|
-
- Layouts
|
|
185
|
-
- Streaming layouts: ingest
|
|
186
|
-
- Custom layouts own their colors
|
|
187
|
-
- Tooltips: emit datum keys
|
|
143
|
+
- Coords are plot-relative (frame translates by `margin`). Read `ctx.dimensions.plot`. Radial ordinal: `plot.x = -width/2`, `plot.y = -height/2` (center-translated).
|
|
144
|
+
- Layouts needing axis domains: pass `xExtent`/`yExtent` (XY) or `oExtent`/`rExtent` (ordinal) — those flow through scale construction *before* the layout runs.
|
|
145
|
+
- Streaming layouts: ingest via ref (`push`/`pushMany`); layout re-runs on each ingest. Overlays update on data-change paths, NOT per-frame.
|
|
146
|
+
- Custom layouts own their colors — always prefer `ctx.resolveColor(key)` over hardcoded literals. `CategoryColorProvider` integration is XY-only; for cross-chart sync on network/ordinal customLayouts, pass matching `colorScheme` to each.
|
|
147
|
+
- Tooltips: emit datum keys matching user-visible accessor names. Avoid underscored synthetic keys (default tooltip filters those out).
|
|
188
148
|
|
|
189
149
|
## Coordinated Views
|
|
190
150
|
|
|
191
|
-
**LinkedCharts** — `selections
|
|
192
|
-
Chart props: `selection`, `linkedHover`, `linkedBrush`. Hooks: `useSelection`, `useLinkedHover`, `useBrushSelection
|
|
193
|
-
**Shared categories inside LinkedCharts → wrap in `CategoryColorProvider`.**
|
|
194
|
-
**Linked crosshair**: `linkedHover={{ name: "sync", mode: "x-position", xField: "time" }}`. Click
|
|
195
|
-
**
|
|
151
|
+
**LinkedCharts** — `selections`. **CategoryColorProvider** — `colors`|`categories` + `colorScheme`.
|
|
152
|
+
Chart props: `selection`, `linkedHover`, `linkedBrush`. Hooks: `useSelection`, `useLinkedHover`, `useBrushSelection`.
|
|
153
|
+
**Shared categories inside LinkedCharts → wrap in `CategoryColorProvider`.** Gives identical per-category colors AND makes LinkedCharts render one unified legend (suppressing individual chart legends). Without it, mismatched colors and duplicate legends.
|
|
154
|
+
**Linked crosshair**: `linkedHover={{ name: "sync", mode: "x-position", xField: "time" }}`. Click locks crosshair (dashed white); click/Escape unlocks.
|
|
155
|
+
**Linked series highlight** (series↔bar cross-highlight): `linkedHover={{ name: "sync", mode: "series" }}` auto-resolves the chart's series-identity field (colorBy/lineBy/areaBy/stackBy/groupBy) and keys the linked selection off it — no hand-wired `fields`. Add `seriesField: "region"` to override (align charts whose series live under different prop names). Modes are exclusive: `mode` is `"field"` (default) | `"x-position"` (crosshair) | `"series"`.
|
|
156
|
+
Also: **ScatterplotMatrix**, **ChartContainer** (`title`, `subtitle`, `actions`), **ChartGrid** (`columns`, `gap`), **ContextLayout**.
|
|
196
157
|
|
|
197
158
|
## Server-Side Rendering (`semiotic/server`)
|
|
198
159
|
|
|
@@ -201,130 +162,126 @@ HOC charts render SVG automatically in server environments. For standalone gener
|
|
|
201
162
|
```ts
|
|
202
163
|
import { renderChart, renderToImage, renderToAnimatedGif, renderDashboard } from "semiotic/server"
|
|
203
164
|
|
|
204
|
-
const svg = renderChart("BarChart", { data, categoryAccessor
|
|
205
|
-
const png = await renderToImage("LineChart", {
|
|
206
|
-
const gif = await renderToAnimatedGif("line", data, { xAccessor
|
|
207
|
-
const dashboard = renderDashboard([{ component: "BarChart", props
|
|
165
|
+
const svg = renderChart("BarChart", { data, categoryAccessor, valueAccessor, theme: "tufte", showLegend, showGrid, annotations })
|
|
166
|
+
const png = await renderToImage("LineChart", { ... }, { format: "png", scale: 2 }) // requires sharp
|
|
167
|
+
const gif = await renderToAnimatedGif("line", data, { xAccessor, yAccessor, theme: "dark" }, { fps: 12, transitionFrames: 4, decay: { type: "linear" } }) // requires sharp + gifenc
|
|
168
|
+
const dashboard = renderDashboard([{ component: "BarChart", props }, { component: "PieChart", colSpan: 2, props }], { title, theme, layout: { columns: 2 } })
|
|
208
169
|
```
|
|
209
170
|
|
|
210
|
-
All
|
|
171
|
+
All accept `theme` (preset name or object); theme categorical colors flow to data marks. `generateFrameSVGs()` returns frame SVGs without sharp/gifenc.
|
|
211
172
|
AnimatedGifOptions: `fps`, `stepSize`, `windowSize`, `frameCount`, `xExtent`/`yExtent` (lock axes), `transitionFrames`, `easing`, `decay`, `loop`, `scale`.
|
|
212
|
-
Server SVGs include `role="img"`, `<title>`, `<desc>`, grid, legend, annotations
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
- **StackedAreaChart** — `data`, `xAccessor`, `yAccessor`, `areaBy` (required).
|
|
218
|
-
- **Scatterplot/BubbleChart** — `data`, `xAccessor`, `yAccessor`. BubbleChart requires `sizeBy`.
|
|
219
|
-
- **Heatmap** — `data`, `xAccessor`, `yAccessor`, `valueAccessor`.
|
|
220
|
-
- **BarChart** — `data`, `categoryAccessor`, `valueAccessor`.
|
|
221
|
-
- **StackedBarChart** — `data`, `categoryAccessor`, `valueAccessor`, `stackBy` (required).
|
|
222
|
-
- **GroupedBarChart** — `data`, `categoryAccessor`, `valueAccessor`, `groupBy` (required).
|
|
223
|
-
- **PieChart/DonutChart** — `data`, `categoryAccessor`, `valueAccessor`.
|
|
224
|
-
- **FunnelChart** — `data`, `stepAccessor` ("step"), `valueAccessor` ("value"). Renders with trapezoid connectors, no axes.
|
|
225
|
-
- **GaugeChart** — `value`. Optional: `thresholds` (array of `{value, color, label}`), `min`, `max`, `sweep`, `arcWidth`, `cornerRadius`.
|
|
226
|
-
- **SwimlaneChart** — `data`, `categoryAccessor`, `subcategoryAccessor` (required), `valueAccessor`.
|
|
227
|
-
- **ForceDirectedGraph** — `nodes`, `edges` (both required). If deriving nodes from edge endpoints, materialize `nodes` before returning JSX/renderChart props.
|
|
228
|
-
- **SankeyDiagram** — `edges` (required), `valueAccessor`.
|
|
229
|
-
- **ChoroplethMap** — `areas` (GeoJSON features, pre-resolved).
|
|
230
|
-
|
|
231
|
-
All components accept: `width`, `height`, `theme`, `title`, `description`, `showLegend`, `showGrid`, `background`, `annotations`, `margin`, `colorScheme`, `colorBy`, `legendPosition`. Pass additional frame-level props via `frameProps`.
|
|
173
|
+
Server SVGs include `role="img"`, `<title>`, `<desc>`, grid, legend, annotations. SVG groups have stable `id` attrs for Figma layer naming: `data-area`, `axes`, `grid`, `annotations`, `legend`, `chart-title`.
|
|
174
|
+
|
|
175
|
+
`renderChart` props match the same accessors documented per-chart above. Sparkline has no axes/grid/legend/title by default (margin 2px). ForceDirectedGraph: materialize `nodes` before passing (don't infer from edge endpoints).
|
|
176
|
+
|
|
177
|
+
All components also accept: `width`, `height`, `theme`, `title`, `description`, `showLegend`, `showGrid`, `background`, `annotations`, `margin`, `colorScheme`, `colorBy`, `legendPosition`. Pass frame-level props via `frameProps`.
|
|
232
178
|
|
|
233
179
|
## Annotations
|
|
234
180
|
|
|
235
|
-
All HOCs accept `annotations
|
|
181
|
+
All HOCs accept `annotations`. Coordinates use data field names.
|
|
236
182
|
|
|
237
|
-
**Positioning**: `widget`, `label`, `callout`, `text`, `bracket`
|
|
183
|
+
**Positioning**: `widget`, `label`, `callout`, `callout-circle`, `callout-rect`, `text`, `bracket`
|
|
238
184
|
**Reference lines**: `y-threshold` (`value`, `label`, `color`, `labelPosition`), `x-threshold`, `band` (`y0`, `y1`)
|
|
239
185
|
**Ordinal**: `category-highlight`
|
|
240
186
|
**Enclosures**: `enclose`, `rect-enclose`, `highlight`
|
|
241
187
|
**Statistical**: `trend`, `envelope`, `anomaly-band`, `forecast`
|
|
242
|
-
**Streaming anchors**: `"fixed"
|
|
188
|
+
**Streaming anchors**: `"fixed" | "latest" | "sticky" | "semantic"` — also exposed as `lifecycle.anchor` on the `semiotic/ai` annotation lifecycle. `"semantic"` is typed but currently falls back to fixed positioning; stableId-based re-resolution remains open.
|
|
189
|
+
**Hierarchy**: any annotation accepts `emphasis: "primary" | "secondary"` across XY, ordinal, network, geo, and static SVG rendering. `secondary` dims (opacity 0.6) and yields z-order; `primary` paints at full weight and on top. Type-agnostic, no-op when unset; class hooks `annotation-emphasis--{primary|secondary}` for further styling.
|
|
190
|
+
**Connectors**: `connector: { end?: "arrow"; type?: "line" | "curve"; curve? }`. `type: "curve"` draws a swoopy quadratic-bezier connector (bend = `curve` × connector length, default 0.25; negate to bow the other way); the arrowhead aligns to the curve's tangent. Default is a straight line.
|
|
191
|
+
**Auto-placement** (opt-in): `autoPlaceAnnotations` (boolean | config) on any HOC runs the `annotationLayout` recipe — collision-avoiding offsets for notes without manual `dx`/`dy`, curved connector routing when placement must go far. Config: `defaultOffset`, `notePadding`, `markPadding`, `edgePadding`, `preserveManualOffsets` (true), `routeLongConnectors` (true), `connectorThreshold`.
|
|
192
|
+
**Density** (opt-in, within `autoPlaceAnnotations`): `density` (true | `{ maxAnnotations?, areaPerAnnotation? (20000), minVisible? (1) }`) sheds lowest-priority note annotations when the plot is over-crowded — priority = `emphasis` (`primary` never shed) → `provenance.confidence` → `lifecycle.freshness` (`expired` first); reference lines/bands/overlays never count. `progressiveDisclosure: true` keeps shed notes tagged `_annotationDeferred` (`.annotation-deferred`, hidden until chart `:hover`/`:focus-within`) instead of dropping them; the persistent set is always shown. Pure forms in `semiotic/recipes`: `annotationDensity({ annotations, width, height, ... }) → { visible, deferred, budget }` and `annotationBudget(w, h)`. `diagnoseConfig` flags `ANNOTATION_DENSITY` when notes exceed the budget.
|
|
193
|
+
**Association/redundant cues** (opt-in, within `autoPlaceAnnotations`): `redundantCues: true` gives a colored `text` note offset from its anchor (the one note type that draws no connector) a faint leader line back to the anchor — a spatial, CVD-safe cue instead of color-alone matching. `auditAccessibility` flags color-only association as `perceivable.annotation-association` (warn) and treats `redundantCues` as satisfying it; `diagnoseConfig` flags `ANNOTATION_FAR_NO_CONNECTOR` / `ANNOTATION_LONG_CONNECTOR` for connector-necessity.
|
|
194
|
+
**Responsive/cohesion** (opt-in, within `autoPlaceAnnotations`): `responsive: true` (or `{ minWidth }`, default 480) sheds `secondary`-emphasis notes once the plot narrows past the breakpoint (keeps `primary`/unmarked); composes with `density`, and with `progressiveDisclosure` defers instead of drops. `cohesion: "blended" | "layer"` (also a per-annotation field; per-annotation wins) — `blended` adopts mark colors/typography (default look), `layer` renders a distinct editorial layer (`--semiotic-annotation-color`, italic) via the `annotation-cohesion--*` class.
|
|
195
|
+
**Audience/defensive** (M6): per-annotation `defensive: true` is never shed by density/responsive (joins the floor) so it survives into every export; with `provenance`, the layout pass bakes `source`+`confidence` visibly into the label (`"… (AI · 70%)"`). `autoPlaceAnnotations: { density: true, audience }` accepts an `AudienceProfile` (anything with a `familiarity` map) and scales the density budget by aggregate familiarity — low-familiarity keeps more notes (×1.5), expert fewer (×0.6).
|
|
196
|
+
**Editorial visibility**: `filterAnnotationsByStatus(annotations, { showRetractedAnnotations?, showSupersededAnnotations? })` returns the current note set without applying styles. `applyAnnotationStatus` uses the same visibility rule; `describeChart` and `buildNavigationTree` skip retracted and superseded notes by default.
|
|
243
197
|
|
|
244
198
|
## Theming
|
|
245
199
|
|
|
246
|
-
CSS custom properties: `--semiotic-bg
|
|
200
|
+
CSS custom properties: `--semiotic-{bg, text, text-secondary, border, grid, primary, secondary, surface, success, danger, warning, error, info, focus, font-family, annotation-color, legend-font-size, title-font-size, tick-font-family, tick-font-size (12px), axis-label-font-size (12px), tooltip-{bg, text, radius, font-size, shadow}}`.
|
|
247
201
|
|
|
248
202
|
```jsx
|
|
249
|
-
<ThemeProvider theme="tufte">
|
|
250
|
-
<ThemeProvider theme={{ mode: "dark", colors: { categorical: [...] } }}> {/*
|
|
203
|
+
<ThemeProvider theme="tufte"> {/* named preset */}
|
|
204
|
+
<ThemeProvider theme={{ mode: "dark", colors: { categorical: [...] } }}> {/* merge onto dark base */}
|
|
251
205
|
```
|
|
252
206
|
|
|
253
|
-
**Color priority** (with `colorBy`): CategoryColorProvider/LinkedCharts
|
|
207
|
+
**Color priority** (with `colorBy`): CategoryColorProvider/LinkedCharts map > `colorScheme` > ThemeProvider `colors.categorical` > `"category10"`.
|
|
254
208
|
Presets: `light`, `dark`, `high-contrast`, `pastels`(-dark), `bi-tool`(-dark), `italian`(-dark), `tufte`(-dark), `journalist`(-dark), `playful`(-dark), `carbon`(-dark).
|
|
255
209
|
Serialization: `themeToCSS(theme, selector)`, `themeToTokens(theme)`, `resolveThemePreset(name)`.
|
|
256
210
|
|
|
257
|
-
**Semantic status roles** (
|
|
211
|
+
**Semantic status roles** (every preset): `colors.success/danger/warning/error/info` + `secondary`/`surface`. Each emits as `--semiotic-{role}`. Use for status-driven charts: `<Waterfall positiveColor="var(--semiotic-success)" negativeColor="var(--semiotic-danger)" />`, `<Swimlane color="var(--semiotic-warning)" />`, status annotations.
|
|
258
212
|
|
|
259
|
-
**Scoped CSS cascade override** (per-subtree, no ThemeProvider needed):
|
|
260
|
-
```jsx
|
|
261
|
-
<div style={{ "--semiotic-danger": "#4b0082" }}>
|
|
262
|
-
{/* every chart below inherits this danger color via canvas CSS-var lookup */}
|
|
263
|
-
</div>
|
|
264
|
-
```
|
|
265
|
-
Canvas scene builders read CSS variables via `getComputedStyle` on the canvas DOM ancestor, so standard CSS cascade rules apply even though rendering is canvas-based. Use CSS vars for **single-role** overrides; use a nested `ThemeProvider` for **array/scale** overrides (categorical palette, sequential/diverging scheme name).
|
|
213
|
+
**Scoped CSS cascade override** (per-subtree, no ThemeProvider needed): wrap a subtree in `<div style={{ "--semiotic-danger": "#4b0082" }}>` — canvas scene builders read CSS vars via `getComputedStyle` on the canvas DOM ancestor, so cascade rules apply even though rendering is canvas. CSS vars for single-role overrides; nested `ThemeProvider` for array/scale overrides (categorical palette, sequential/diverging scheme).
|
|
266
214
|
|
|
267
215
|
## AI Features
|
|
268
|
-
|
|
216
|
+
|
|
217
|
+
Surface APIs: `onObservation`/`useChartObserver`, `toConfig`/`fromConfig`/`toURL`/`fromURL`/`copyConfig`/`configToJSX`, `validateProps`, `diagnoseConfig`, `auditAccessibility`/`accessibilityCaveats` + `describeChart` + `buildNavigationTree`/`AccessibleNavTree`/`useNavigationSync` + `buildReaderGrounding` (a11y audit + descriptions + structured navigation + bidirectional sync + agent-reader grounding — see Accessibility), `exportChart(div, { format })`, `npx semiotic-ai --doctor`/`--audit-a11y`.
|
|
269
218
|
|
|
270
219
|
### Conversational Interrogation (`semiotic/ai`)
|
|
271
|
-
Headless
|
|
220
|
+
Headless "chat with the chart" hook. Library ships no UI — BYO chat surface.
|
|
272
221
|
- **`useChartInterrogation({ data, onQuery, componentName?, props?, initialAnnotations? })`** → `{ ask(query), history, summary, annotations, loading, error, reset }`
|
|
273
|
-
- **`onQuery: (query, context) => Promise<{ answer, annotations? }>`** — call your LLM
|
|
274
|
-
- **`summary`**:
|
|
275
|
-
- **`annotations`**:
|
|
276
|
-
- **`summarizeData(data, options?)`**:
|
|
277
|
-
- **MCP
|
|
278
|
-
|
|
279
|
-
```jsx
|
|
280
|
-
import { LineChart, useChartInterrogation } from "semiotic/ai"
|
|
281
|
-
|
|
282
|
-
function InterrogatableChart({ data }) {
|
|
283
|
-
const { ask, history, annotations, loading } = useChartInterrogation({
|
|
284
|
-
data,
|
|
285
|
-
componentName: "LineChart",
|
|
286
|
-
props: { xAccessor: "month", yAccessor: "revenue" },
|
|
287
|
-
onQuery: async (query, { summary }) => {
|
|
288
|
-
const res = await myLLMCall(query, summary)
|
|
289
|
-
return { answer: res.text, annotations: res.highlights }
|
|
290
|
-
},
|
|
291
|
-
})
|
|
292
|
-
return (
|
|
293
|
-
<>
|
|
294
|
-
<LineChart data={data} xAccessor="month" yAccessor="revenue" annotations={annotations} />
|
|
295
|
-
<YourChatUI history={history} loading={loading} onAsk={ask} />
|
|
296
|
-
</>
|
|
297
|
-
)
|
|
298
|
-
}
|
|
299
|
-
```
|
|
222
|
+
- **`onQuery: (query, context) => Promise<{ answer, annotations? }>`** — call your LLM. `context`: `{ data, summary, componentName?, props? }`.
|
|
223
|
+
- **`summary`**: stat summary (`rowCount`, per-field `{min, max, mean, median}` for numerics, top-k for categoricals, ISO range for dates). Available before any `ask()`.
|
|
224
|
+
- **`annotations`**: merged `initialAnnotations` + latest AI response. Wire to chart's `annotations` prop.
|
|
225
|
+
- **`summarizeData(data, options?)`**: standalone for server prompting or batch.
|
|
226
|
+
- **MCP tool**: `interrogateChart(component, props, query)` returns same summary + AI-facing instructions.
|
|
300
227
|
|
|
301
228
|
### Chart Capability Layer (`semiotic/ai`)
|
|
302
|
-
Heuristic chart-suggestion engine. Charts ship capability descriptors next to
|
|
303
|
-
|
|
304
|
-
- **`
|
|
305
|
-
- **`
|
|
306
|
-
- **`scoreChart(component, data, { intent?, variantKey? })`** → evaluate a specific chart for a dataset (does it fit, how well, why/why not).
|
|
229
|
+
Heuristic chart-suggestion engine — no LLM required. Charts ship capability descriptors next to TSX files; engine ranks against a profiled dataset by intent.
|
|
230
|
+
- **`profileData(data, { rawInput?, seriesField? })`** → `ChartDataProfile`: per-role candidate fields (x/y/series/category/size/time), distinct counts, monotonicity, structure detection.
|
|
231
|
+
- **`suggestCharts(data, { intent?, allow?, deny?, maxResults?, includeVariants?, minScore?, audience? })`** → ranked `Suggestion[]` with `{ component, family, importPath, variant?, score, intentScores, rubric, reasons, caveats, props }`. `props` is spreadable directly. **Receivability**: set `audience.receptionModality` (`visual` default | `screen-reader` | `sonified` | `agent`); a non-visual channel audits each candidate and down-ranks charts the audience can't receive there (8-slice pie for a screen reader), adding the audit's findings to `caveats[]` (familiarity and receivability are separate axes). `accessibilityCaveats(auditResult)` distils any audit into the same caveat strings.
|
|
232
|
+
- **`scoreChart(component, data, { intent?, variantKey? })`** → evaluate a specific chart for a dataset.
|
|
307
233
|
- **`useChartSuggestions(data, options)`** → memoized React hook returning `{ suggestions, profile }`.
|
|
308
234
|
- **`registerChartCapability(capability)`** / **`unregisterChartCapability(name)`** — runtime registration for custom charts.
|
|
309
|
-
- **Intent taxonomy
|
|
310
|
-
- **Capability authoring**:
|
|
311
|
-
- **Variants encode that settings change what a chart is good for
|
|
312
|
-
- **Interrogation tie-in**: pass `includeSuggestions: true` to `useChartInterrogation` and the
|
|
313
|
-
- **MCP tool**: `suggestCharts(data, intent?)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
235
|
+
- **Intent taxonomy** (13 built-in): `trend`, `compare-series`, `compare-categories`, `rank`, `part-to-whole`, `distribution`, `correlation`, `flow`, `hierarchy`, `geo`, `outlier-detection`, `composition-over-time`, `change-detection`. Extend via `registerIntent`.
|
|
236
|
+
- **Capability authoring**: `Foo.capability.ts` next to `Foo.tsx`, append to registry in `src/components/ai/chartCapabilities.ts`. Declares `family`, `rubric` (familiarity/accuracy/precision 1-5), `fits(profile)` gate, `intentScores`, optional `variants` with `intentDeltas`, `buildProps(profile, variant)`.
|
|
237
|
+
- **Variants** encode that settings change what a chart is good for (e.g. StackedAreaChart's `streamgraph` variant boosts trend, penalizes part-to-whole).
|
|
238
|
+
- **Interrogation tie-in**: pass `includeSuggestions: true` to `useChartInterrogation` and the ranked list lands in `context.suggestions` for the LLM.
|
|
239
|
+
- **MCP tool**: `suggestCharts(data, intent?)`.
|
|
240
|
+
|
|
241
|
+
### Conversation-arc telemetry (`semiotic/ai`)
|
|
242
|
+
Opt-in event store recording the AI session arc: `suggestion-shown → suggestion-chosen → audience-set → chart-rendered → chart-edited → chart-replaced → chart-exported | chart-abandoned`, plus `interrogation-asked`/`interrogation-answered`, reader-navigation events, and `annotation-status-changed`. Module-scoped, no provider needed. Default surface is no-op — call `enableConversationArc()` to start.
|
|
243
|
+
- **`enableConversationArc({ capacity?, sessionId? })`** / **`disableConversationArc()`**. Bounded ring buffer (default 1000 events).
|
|
244
|
+
- **`getConversationArcStore()`** → `{ enabled, sessionId, capacity, record, flush, getEvents, subscribe, clear, reset }`. `getEvents()` returns referentially stable snapshot.
|
|
245
|
+
- **`useConversationArc({ enableOnMount?, disableOnUnmount?, capacity?, sessionId? })`** → `{ history, summary, enabled, sessionId, record, clear }`. Uses `useSyncExternalStore`.
|
|
246
|
+
- **`summarizeArc(events)`** → pure reducer. Server/replay safe.
|
|
247
|
+
- **Persistence / replay**: `registerConversationArcSink(sink)` attaches an opt-in durable sink. Built-ins: `createLocalStorageConversationArcSink({ key?, storage?, maxEvents? })`, `createIndexedDBConversationArcSink({ dbName?, storeName?, indexedDB?, maxEvents? })`, and `createWebhookConversationArcSink({ url, method?, headers?, fetch?, mapEvent? })`. Sinks receive accepted events only; disabled telemetry is still zero-overhead. `loadConversationArc(events, { enabled?, capacity?, sessionId?, append? })` / `replayConversationArc(...)` hydrate the visible store snapshot without re-emitting events to listeners or sinks; default `enabled: false` makes replay safe for analytics.
|
|
248
|
+
- **`recordAudienceChange(audience, previous?, { arcId?, meta? }?)`** — sugar for `audience-set`. Call from audience-picker `onChange`.
|
|
249
|
+
- **Events**: `ConversationArcEvent` discriminated union. Each variant carries its own payload.
|
|
250
|
+
- **Auto-instrumented**: `useChartSuggestions` emits `suggestion-shown` (dedup by component-list + intent); `useChartInterrogation` emits `interrogation-asked` + `interrogation-answered` (with `latencyMs`); `AccessibleNavTree` emits the reception pair `nav-node-focused`/`nav-branch-expanded` on reader traversal (keyboard/click), correlated by its `chartId` prop. Zero-overhead when disabled.
|
|
251
|
+
|
|
252
|
+
### Annotation provenance + lifecycle (`semiotic/ai`, types re-exported from `semiotic`)
|
|
253
|
+
Two optional blocks attach to any annotation — existing arrays keep working unchanged.
|
|
254
|
+
- **`provenance`**: `{ author?, authorKind?, source?, basis?, confidence?, createdAt?, dataVersion?, stableId? }` (union of the shipped fields + IDID §8 `ChartAnnotationProvenance`). `authorKind` = actor (`"human"|"agent"|"watcher"|"system"|(string & {})`); `basis` = evidence type (`"human-note"|"statistical-test"|"rule"|"llm-inference"|"external-source"|"computed"|(string & {})`), distinct from the actor; `source` open union (`"user"|"ai"|"agent"|"import"|"computed"|"system"|(string & {})`); `dataVersion` = data snapshot the note was made against.
|
|
255
|
+
- **`lifecycle`**: `{ freshness?, status?, supersedes?, ttlHint?, anchor? }`. Two orthogonal axes — **temporal** `freshness` (`"fresh"|"aging"|"stale"|"expired"`, derived from `createdAt`+`ttlHint`) and **editorial** `status` (`"proposed"|"accepted"|"disputed"|"retracted"`); a note can be fresh-but-disputed. `supersedes` = `stableId` of the note this replaces. `anchor`: `"fixed"|"latest"|"sticky"|"semantic"`. `ttlHint`: ISO 8601 duration (`"P30D"`) or ms.
|
|
256
|
+
- **`withProvenance(annotation, { provenance?, lifecycle? })`** → pure, SSR-safe.
|
|
257
|
+
- **`Annotated<T>`** type: `T & { provenance?, lifecycle? }`.
|
|
258
|
+
- **`computeAnnotationFreshness(annotations, { now?, dataExtent?, thresholds? })`** → populates `lifecycle.freshness`. `now` defaults to `dataExtent` max, then `Date.now()`. Default thresholds 1×/1.5×/3× TTL.
|
|
259
|
+
- **`annotationFreshnessFor(annotation, nowMs, thresholds?)`** → classifies a single annotation. Explicit `lifecycle.freshness` wins.
|
|
260
|
+
- **`applyAnnotationLifecycle(annotations, { now?, dataExtent?, opacity?, strokeDasharray?, labelSuffix?, showExpiredAnnotations?, thresholds? })`** → freshness + default visuals (aging dims opacity 0.55, stale dims + dashes `"4 4"`, expired filtered; set `showExpiredAnnotations: true` to keep). Per-band overrides; pass `null` to disable a band default. Annotation-level `opacity`/`strokeDasharray` win.
|
|
261
|
+
- **`applyAnnotationStatus(annotations, { opacity?, strokeDasharray?, labelSuffix?, showRetractedAnnotations?, showSupersededAnnotations? })`** (M7) → editorial-status treatment, orthogonal to freshness: `disputed`→`(?)` + dim, `proposed`→provisional dim+dash, `retracted`→filtered (like expired), `accepted`→full. Opacity **multiplies** into existing, so it composes with `applyAnnotationLifecycle` (run freshness first). Also resolves `supersedes` — a note superseded by a present, non-retracted note is hidden. Use **`filterAnnotationsByStatus`** when a non-visual or custom surface needs the same visibility contract without styling. Emit transitions via **`recordAnnotationStatusChange(toStatus, { annotationId?, fromStatus?, chartId? })`** → conversation-arc `annotation-status-changed` event.
|
|
262
|
+
- **Semantic anchors**: `anchor: "semantic"` / `lifecycle.anchor: "semantic"` re-resolves through `provenance.stableId` after data refresh, using point scene nodes or matching data rows and falling back to the recorded coordinate when the target is gone.
|
|
263
|
+
|
|
264
|
+
### Temporal lifecycle (shared `semiotic/realtime` + `semiotic/ai`)
|
|
265
|
+
Three systems answer "how does this look as it ages?" on three different time axes — not interchangeable.
|
|
266
|
+
|
|
267
|
+
| Policy | Lives in | Time axis | Output | Scope |
|
|
268
|
+
|---|---|---|---|---|
|
|
269
|
+
| `DecayConfig` | `semiotic/realtime` | buffer position | continuous opacity ramp | per-datum |
|
|
270
|
+
| `StalenessConfig` | `semiotic/realtime` | wall-clock idle | binary live/stale (+ optional badge) | chart-wide |
|
|
271
|
+
| Annotation freshness | `semiotic/ai` | `createdAt` + `ttlHint` | 4 named bands (opacity + dashing + expired filter) | per-annotation |
|
|
272
|
+
|
|
273
|
+
Shared primitive: **`bandFromAge(ageMs, ttlMs, thresholds?)`** → `"fresh"|"aging"|"stale"|"expired"`. Exported from both. `DEFAULT_LIFECYCLE_THRESHOLDS = { fresh: 1.0, aging: 1.5, stale: 3.0 }`. Shared **anchor mode** (`AnnotationAnchor` from `semiotic/realtime`, re-exported from `semiotic/ai` as `lifecycle.anchor`).
|
|
274
|
+
|
|
275
|
+
**Streaming chart-time aging**: pass chart's `dataExtent` to `applyAnnotationLifecycle` — latest data point becomes "now". Pair with `withCurrentProvenance(annotation, { author?, source? })` / `currentTimestamp()` to auto-stamp `createdAt`. Full survey: `/intelligence/temporal-lifecycle`.
|
|
276
|
+
|
|
277
|
+
### Variant discovery (`semiotic/ai`)
|
|
278
|
+
Interface for proposing/scoring variants beyond `capability.variants`. The built-in proposer emits registered variants, conservative heuristic transforms, and same-intent cross-family alternatives; external recommenders can register model/agent proposers.
|
|
279
|
+
- **`VariantProposal`**: `{ id, baseComponent, label?, intentDeltas?, rubricDeltas?, buildProps?, rationale?, source: "manual"|"heuristic"|"model", variantKey?, tags? }`.
|
|
280
|
+
- **`VariantScore`**: `{ proposalId, fit (0–5), novelty (0–1), risk (0–1), reasons }`. Mixes with `suggestCharts` composite scores.
|
|
281
|
+
- **`proposeVariant(component, capability, context)`** → `VariantProposal[]`. Context accepts `{ profile, audience?, intent?, existingVariants? }`.
|
|
282
|
+
- **`evaluateVariantProposal(proposal, profile, audience?, { intent?, baselineComponent? }?)`** → `VariantScore`.
|
|
283
|
+
- **MCP tool**: `proposeChartVariants(component, props?, data?, intent?, audience?)` ranks proposals and returns ready-to-use props.
|
|
284
|
+
- **`registerVariantDiscovery(fn)`** → registers proposer. `proposeVariant` dispatches through all and dedupes by `proposal.id`. Returns unregister. Inspect: `getRegisteredVariantDiscovery()` / `clearVariantDiscovery()`.
|
|
328
285
|
|
|
329
286
|
## AI Behavior Contracts
|
|
330
287
|
|
|
@@ -349,32 +306,38 @@ These rules are generated from `ai/behaviorContracts.cjs` and are consumed by `s
|
|
|
349
306
|
<!-- semiotic-behavior-contracts:end -->
|
|
350
307
|
|
|
351
308
|
## Accessibility
|
|
309
|
+
`role="group"` (outer) + `role="img"` (inner canvas). Keyboard: arrows navigate points, Enter cycles neighbors, Home/End/PageUp/PageDown. Shape-adaptive focus ring (`--semiotic-focus`). `accessibleTable` (default true) for sr-only data summary (live aria-live region sits OUTSIDE `role="img"` so AT announces hovered/focused data). Auto-detects `prefers-reduced-motion`, `forced-colors`. Hooks: `useReducedMotion()`, `useHighContrast()`.
|
|
352
310
|
|
|
353
|
-
`
|
|
311
|
+
**Chartability audit**: `auditAccessibility(component, props, { inChartContainer?, describe?, navigable? })` (from `semiotic/ai` or `semiotic/utils`) grades a config against Chartability (POUR-CAF) — credits built-ins, flags author-actionable gaps, marks un-checkable items `manual` (not a false pass). `formatAccessibilityAudit(result)` renders the report. Surfaced as `npx semiotic-ai --audit-a11y` (non-zero exit on critical fail → CI gate) + the `auditAccessibility` MCP tool. Returns `{ ok, summary, findings[] }`; each finding has `{ id, principle, heuristic, critical, status: "pass"|"fail"|"warn"|"manual"|"not-applicable", message, fix }`. NOT a pass/fail cert — pair with manual NVDA/JAWS/VoiceOver testing.
|
|
312
|
+
|
|
313
|
+
**Chart descriptions**: `describeChart(component, props, { levels?, locale?, capability?, audience? })` (from `semiotic/ai` or `semiotic/utils`) generates a layered natural-language description (Lundgard L1 encoding / L2 statistics / L3 trend) → `{ text, levels: {l1?,l2?,l3?,l4?}, annotations? }`. Richest for XY/bar/part-to-whole/distribution; degrades to L1 for network/hierarchy/geo/value. **Annotations**: when `props.annotations` is present, the result carries an `annotations` sentence ("The author has marked 2 features…") and it *leads* `text` ahead of L1–L3 — an author-placed note is intent in its purest form. Provenance-aware: an `authorKind`/`source` of `agent`/`ai`/`watcher` qualifies it ("an AI-suggested callout"). Absent when no annotations, so un-annotated callers are unchanged. **L4 (intent)**: pass a `capability` (a chart's descriptor or a resolved `{ family, intentScores }`) and `describeChart` emits the illocutionary *communicative-act* sentence ("This is an alerting chart; the peak at March is the point to investigate") — opt-in, default output stays L1–L3. Helpers: `resolveCommunicativeAct(component, capability)`, `communicativeActForIntent(intent)`, type `CommunicativeAct`. **Full-accessibility chrome (title, caption, description, navigation, data download) is the opt-in `ChartContainer` layer — not baked into the bare chart.** `<ChartContainer describe chartConfig={{component, props}}>` renders an sr-only L1–L3 description (`describe={{ visible:true }}` to show it, `{ levels }` for verbosity). The audit's `assistive.features-described` passes when `describe` is on.
|
|
314
|
+
|
|
315
|
+
**Agent-reader grounding**: `buildReaderGrounding(component, props, { capability?, audience?, includeStructure? })` (from `semiotic/ai` or `semiotic/utils`) → `{ description (L1–L3), intent (act + L4 sentence), structure (nav tree), text }` — the single payload an LLM reads to interpret a chart faithfully (the reader-side complement to a capability descriptor). MCP: `groundChart` tool.
|
|
316
|
+
|
|
317
|
+
**Structured navigation** (Olli/Data-Navigator model): `buildNavigationTree(component, props, { maxLeaves?, locale? })` (from `semiotic/ai` or `semiotic/utils`) → a `NavTreeNode` tree (chart → axis/series → datum), labels composed via describeChart. When `props.annotations` is present it appends an **Annotations** branch (`role: "annotation"`) so a reader encounters author notes during traversal — provenance + M7 editorial `status` surfaced inline, retracted and superseded notes skipped, added on every family (incl. root-only network/geo/hierarchy). `describeChart` also leads its text with an annotation sentence when annotations are present. `AccessibleNavTree` (from `semiotic` or `semiotic/ai`) renders it as a WAI-ARIA `tree` widget (arrows/Enter/Home/End, roving tabindex, `onActiveChange`, controlled `activeId` auto-expands to the node, `chartId` for telemetry), sr-only by default. With the conversation-arc store enabled it emits `nav-node-focused`/`nav-branch-expanded` reception events on genuine traversal (not on canvas-driven `activeId` changes). `<ChartContainer navigable chartConfig={...}>` mounts it (`navigable={{ visible?, maxLeaves? }}`). Audit's `compromising.navigable-structure` passes when `navigable` is on (incl. hierarchy charts). Audit options: `auditAccessibility(component, props, { inChartContainer?, describe?, navigable? })`.
|
|
318
|
+
|
|
319
|
+
**Bidirectional sync**: `useNavigationSync({ tree, chartId?, matchFields?, selectionName?, annotations? })` (from `semiotic` or `semiotic/ai`) → `{ activeId, onActiveChange, selection, annotatedIds, focusAnnotation }`. Tree→canvas highlights the matching mark (field-value selection); canvas→tree maps the hovered/clicked datum back to its leaf. Rides the module-global selection + observation stores — **no provider needed**: give the chart `chartId` + `selection={sync.selection}`, give `AccessibleNavTree` `activeId`/`onActiveChange`. **Annotation anchors**: pass the chart's `annotations` and an anchored annotation (carrying the datum's `matchFields`) resolves to a nav leaf — `annotatedIds` are the leaf ids with a note; `focusAnnotation(annotation | index)` jumps the tree + canvas to the anchored point so a non-visual reader can reach an AI's anchored note.
|
|
354
320
|
|
|
355
321
|
## Usage Notes
|
|
356
322
|
|
|
323
|
+
- **Push API**: Omit `data`. `data={[]}` clears on every render.
|
|
357
324
|
- **Tooltip datum shape**: HOC tooltips get raw data. Frame `tooltipContent` gets wrapped — use `d.data`.
|
|
325
|
+
- **Tooltip format cascade**: `valueFormat`/`xFormat`/`yFormat` flow to default tooltip (axis + tooltip read identically). Custom `tooltip` fully overrides — re-pass via `Tooltip({format})` / `MultiLineTooltip({fields:[{format}]})`. Bespoke-tooltip charts (Histogram, FunnelChart, LikertChart, GaugeChart) don't participate; customize via `tooltip`.
|
|
326
|
+
- **`tooltip="multi"`**: shows all series at hovered X for LineChart, AreaChart, StackedAreaChart. Custom fn receives `datum.allSeries`.
|
|
358
327
|
- **Legend**: "bottom" expands margin ~80px. MultiAxisLineChart: use `legendPosition="bottom"`.
|
|
359
|
-
- **
|
|
360
|
-
- **
|
|
361
|
-
-
|
|
362
|
-
-
|
|
363
|
-
-
|
|
364
|
-
-
|
|
365
|
-
-
|
|
366
|
-
- **Geo imports**:
|
|
367
|
-
- **
|
|
368
|
-
- **
|
|
369
|
-
-
|
|
370
|
-
-
|
|
371
|
-
- **
|
|
372
|
-
- **xScaleType: "time"**: Creates `scaleTime`. Required for landmark ticks with timestamps.
|
|
373
|
-
- **scalePadding**: Pixel inset on scale ranges. Pass via `frameProps={{ scalePadding: 12 }}`.
|
|
374
|
-
- **categoryFormat/xFormat/yFormat**: Can return ReactNode (renders in `<foreignObject>`).
|
|
375
|
-
- **Tick deduplication**: Adjacent identical labels auto-removed.
|
|
376
|
-
- **Composing overlays**: XY/Ordinal charts paint `--semiotic-bg` across the canvas; stack with `frameProps={{ background: "transparent" }}` on the overlay. Network/Geo don't paint bg by default.
|
|
328
|
+
- **Horizontal bars**: need wider left margin (`margin={{ left: 120 }}`).
|
|
329
|
+
- **Log scale**: domain min clamped to 1e-6. **`xScaleType: "time"`**: creates `scaleTime`; required for landmark ticks with timestamps.
|
|
330
|
+
- **`barPadding`**: pixel value (40/60 default). Reduce for small charts.
|
|
331
|
+
- **`sort`** (BarChart/StackedBarChart/GroupedBarChart/DotPlot): `false` preserves insertion order; `"auto"` = insertion while streaming, value-desc on static (DotPlot default). StackedBar/GroupedBar default to `false`; the underlying frame value-sorts when `oSort` is undefined, so always pass `sort` explicitly if order matters.
|
|
332
|
+
- **`fillArea`**: `fillArea={["seriesA"]}` fills named series only — names must match `lineBy`/`colorBy` keys.
|
|
333
|
+
- **`hoverHighlight`**: requires `colorBy` as a string field.
|
|
334
|
+
- **`frameProps` style functions**: bypass HOC color resolution — use `colorBy` instead.
|
|
335
|
+
- **Geo imports**: always `semiotic/geo`, never `semiotic`.
|
|
336
|
+
- **Axis config**: `frameProps.axes: [{ orient, includeMax, autoRotate, gridStyle, landmarkTicks, tickAnchor }]`. `tickAnchor: "edges"` flips first tick's `text-anchor` to `start`, last to `end` (and `dominant-baseline` on vertical axes) so edge labels don't overflow. Pairs with `axisExtent: "exact"`.
|
|
337
|
+
- **Per-axis CSS**: every axis renders as `<g class="semiotic-axis semiotic-axis-{bottom|left|right|top}" data-orient="…">`. Style via `[data-orient="left"] text { font-size: 14px }` — CSS-var defaults are set inline via `var(--semiotic-tick-font-size, …)`, so cascade overrides win cleanly. Tick text: `class="semiotic-axis-tick"`; labels: `class="semiotic-axis-label"`; titles: `class="semiotic-chart-title"`.
|
|
338
|
+
- **`scalePadding`**: pixel inset on scale ranges (via `frameProps={{ scalePadding: 12 }}`).
|
|
339
|
+
- **`categoryFormat`/`xFormat`/`yFormat`**: can return ReactNode (renders in `<foreignObject>`). Tick deduplication: adjacent identical labels auto-removed.
|
|
340
|
+
- **Composing overlays**: XY/Ordinal paint `--semiotic-bg` across the canvas; stack with `frameProps={{ background: "transparent" }}` on the overlay. Network/Geo don't paint bg by default.
|
|
377
341
|
|
|
378
342
|
## Performance
|
|
379
|
-
|
|
380
343
|
Prefer string accessors (`xAccessor="value"`) — always referentially stable. Memoize function accessors with `useCallback`.
|