- Install:
npm install semiotic
- Use sub-path imports —
semiotic/xy(85KB gz),semiotic/ordinal(69KB gz),semiotic/network(63KB gz),semiotic/geo(52KB gz),semiotic/realtime(90KB gz),semiotic/server(122KB gz),semiotic/utils(22KB gz),semiotic/recipes(5KB gz),semiotic/themes(4KB gz),semiotic/data(3KB gz),semiotic/ai(189KB gz). Fullsemioticis 188KB gz.
- CLI:
npx semiotic-ai [--schema|--compact|--examples|--doctor] - MCP:
npx semiotic-mcp
- HOC Charts: Simple props, sensible defaults. Stream Frames: Full control.
- Always use HOC charts unless you need control they don't expose. Stream Frames pass
RealtimeNode/RealtimeEdgewrappers in callbacks, not your data. - Every HOC accepts
framePropsfor pass-through. TypeScriptstrict: true. Every HOC has error boundary + dev-mode validation.
title, description (aria-label), summary (sr-only), width (600), height (400), responsiveWidth, responsiveHeight, margin, className, color (uniform fill), stroke (uniform stroke color — CSS var OK), strokeWidth (uniform stroke width in px), opacity (uniform 0–1 opacity), enableHover (true), tooltip (boolean | "multi" | function | config object), showLegend, showGrid (false), frameProps, onObservation, onClick, chartId, loading (false), loadingContent (ReactNode — replaces the default skeleton when loading is true; pass false to suppress the loading UI entirely), emptyContent, legendInteraction ("none"|"highlight"|"isolate"), legendPosition ("right"|"left"|"top"|"bottom"), emphasis ("primary"|"secondary"), annotations (array), accessibleTable (true), hoverHighlight (boolean — dims non-hovered series, requires colorBy), hoverRadius (30), animate (boolean | { duration?, easing?, intro? } — animated intro on first render + smooth transitions on data change; intro defaults to true when animate is enabled), axisExtent ("nice" default | "exact" — pins first/last tick to actual data min/max with equidistant intermediates. Applies to XY x/y axes and ordinal value axis only; no-op on network/geo/hierarchy. Explicit tickValues still wins.)
Primitive styling props (color, stroke, strokeWidth, opacity) apply to any shape the chart draws (bars, circles, lines, wedges, rects). Precedence: top-level prop > frameProps.*Style function return > HOC base > theme fallback. Use CSS variables (stroke="var(--semiotic-border)") for theme-aware, cascade-overridable styling. For per-datum customization, keep using the function-form frameProps.pieceStyle / pointStyle / lineStyle etc. — the top-level prop overlays on top of whatever the function returns.
onClick receives (datum, { x, y }). onObservation receives { type, datum?, x?, y?, timestamp, chartType, chartId }.
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 (asymmetric min/max envelope: { y0Accessor, y1Accessor, style?, perSeries?, interactive? } or array for fan charts; participates in yExtent; non-interactive by default; hovered datum is enriched with band: {y0,y1} and bands: [...]), directLabel, gapStrategy, xScaleType/yScaleType ("linear"|"log"|"time"), tooltip="multi" for hover-anywhere series comparison
AreaChart — LineChart props + areaBy, y0Accessor, gradientFill, areaOpacity (0.7), showLine (true), band (same shape as LineChart — decorative envelope under the area), tooltip="multi" for hover-anywhere area comparison
DifferenceChart — Two-series A/B comparison. Fills the area between with seriesAColor where A > B and seriesBColor where B > A; crossovers interpolated. Props: data, xAccessor ("x"), seriesAAccessor ("a"), seriesBAccessor ("b"), seriesALabel/seriesBLabel, seriesAColor (var(--semiotic-danger))/seriesBColor (var(--semiotic-info)), showLines (true), lineWidth (1.5), showPoints (false), pointRadius (3), curve ("linear"), areaOpacity (0.6), gradientFill, xExtent/yExtent, pointIdAccessor, windowSize (max raw rows in push buffer; FIFO eviction). Push API via ref.current.push({x, a, b}). Accessor outputs coerce through toNumber so Date + numeric strings are accepted.
StackedAreaChart — flat array + areaBy (required), colorBy, normalize, baseline ("zero" default | "wiggle" streamgraph | "silhouette" centered), stackOrder ("key" alpha | "insideOut" largest-in-middle | "asc"/"desc"). Streamgraph: baseline="wiggle" + stackOrder="insideOut". baseline ⊥ normalize. No lineBy/lineDataAccessor. tooltip="multi" lists every series at the hovered x (values interpolated between samples).
Scatterplot — data, xAccessor, yAccessor, colorBy, sizeBy, sizeRange, pointRadius (5), pointOpacity (0.8), marginalGraphics, regression (boolean | "linear" | "polynomial" | "loess" | RegressionConfig — sugar for a trend-annotation overlay; sits underneath user annotations)
BubbleChart — Scatterplot + sizeBy (required), sizeRange ([5,40]), regression
ConnectedScatterplot — + orderAccessor, regression
QuadrantChart — Scatterplot + quadrants (required), xCenter, yCenter
MultiAxisLineChart — Dual Y-axis. series (required: [{ yAccessor, label?, color?, format?, extent? }]). Falls back to multi-line if not 2 series.
Heatmap — data, xAccessor, yAccessor, valueAccessor, colorScheme, showValues, cellBorderColor
ScatterplotMatrix — data, fields (array of numeric field names for grid)
MinimapChart — Overview + detail with linked zoom. Wraps an XY chart.
CandlestickChart — data, xAccessor, highAccessor (req), lowAccessor (req), openAccessor + closeAccessor (optional). With all four: OHLC bars. With only high/low: degrades to a range chart. candlestickStyle ({ upColor, downColor, wickColor, rangeColor, bodyWidth, wickWidth }). Honors mode (primary/context/sparkline).
BarChart — data, categoryAccessor, valueAccessor, orientation, colorBy, sort, barPadding (40), roundedTop, gradientFill (true | {topOpacity, bottomOpacity} | {colorStops} — same API as AreaChart; runs tip→base), regression (boolean | "linear" | "polynomial" | "loess" | RegressionConfig — sugar for a trend-annotation overlay; categories regressed as category-index, line projects through band scale)
StackedBarChart — + stackBy (required), normalize, sort (default false — insertion order)
GroupedBarChart — + groupBy (required), barPadding (60), sort (default false — insertion order)
SwarmPlot — colorBy, sizeBy, pointRadius, pointOpacity
BoxPlot — + showOutliers, outlierRadius
Histogram — + bins (25), relative. Always horizontal.
ViolinPlot — + bins, curve, showIQR
RidgelinePlot — + bins, amplitude (1.5)
DotPlot — + sort ("auto" — insertion order when streaming, value-desc on static), dotRadius, showGrid default true, regression
PieChart — categoryAccessor, valueAccessor, colorBy, startAngle
DonutChart — PieChart + innerRadius (60), centerContent
FunnelChart — stepAccessor, valueAccessor, categoryAccessor (optional), connectorOpacity, orientation
SwimlaneChart — categoryAccessor, subcategoryAccessor (required), valueAccessor, colorBy (defaults to subcategoryAccessor), orientation, roundedTop (pixel radius applied to both outer ends of each lane — left+right for horizontal, top+bottom for vertical. Middle segments stay square so adjacent pieces butt against each other; single-segment lanes round all four corners.)
LikertChart — categoryAccessor, valueAccessor|levelAccessor+countAccessor, levels (required), orientation, colorScheme
GaugeChart — value (required), min, max, thresholds, arcWidth, cornerRadius (pixel radius for rounded segment ends — same semantics as DonutChart), sweep, fillZones, showNeedle, centerContent
All ordinal: colorBy, colorScheme, categoryFormat (string|ReactNode), showCategoryTicks (true).
ForceDirectedGraph — nodes, edges, nodeIDAccessor, sourceAccessor, targetAccessor, colorBy, nodeSize, nodeSizeRange, edgeWidth, iterations (300), forceStrength (0.1), showLabels, nodeLabel
SankeyDiagram — edges, nodes, valueAccessor, nodeIdAccessor, colorBy, edgeColorBy, orientation, nodeAlign, nodeWidth, nodePaddingRatio, showLabels
ProcessSankey — temporal sankey with a real time x-axis. nodes, edges (each with startTime/endTime), domain (required [t0, t1]), axisTicks?, xExtentAccessor (optional [start, end] lifetime per node — lane spans min(xExtent[0], earliestEdge) to max(xExtent[1], latestEdge)), colorBy/colorScheme/showLegend/legendPosition, pairing ("value"|"temporal"), packing ("off"|"reuse"), laneOrder ("crossing-min"|"inside-out"|"crossing-min+inside-out"|"insertion"), lifetimeMode ("full"|"half"), ribbonLane ("source"|"target"|"both"), showLaneRails, showLabels (default true), showQualityReadout, showParticles + particleStyle (same shape as SankeyDiagram, canvas + ParticlePool), timeFormat/valueFormat, push API via ref. Static-graph cycles are valid as long as edges move forward in time. Use for time-stamped flow events; use SankeyDiagram for static total-flow snapshots.
ChordDiagram — edges, nodes, valueAccessor, edgeColorBy, padAngle, showLabels
TreeDiagram — data (root), layout, orientation, childrenAccessor, colorBy, colorByDepth
Treemap — data (root), childrenAccessor, valueAccessor, colorBy, colorByDepth, showLabels
CirclePack — data (root), childrenAccessor, valueAccessor, colorBy, colorByDepth
OrbitDiagram — data (root), childrenAccessor, orbitMode, speed, animated (true), colorBy
Import from semiotic/geo — NOT semiotic — to avoid pulling d3-geo into non-geo bundles.
ChoroplethMap — areas (GeoJSON Feature[] or "world-110m"), valueAccessor, colorScheme, projection ("equalEarth"), graticule, tooltip, showLegend
ProportionalSymbolMap — points, xAccessor ("lon"), yAccessor ("lat"), sizeBy, sizeRange, colorBy, areas (optional background)
FlowMap — flows, nodes, valueAccessor, edgeColorBy, lineType, showParticles
DistanceCartogram — points, center, costAccessor, strength, showRings
All geo: fitPadding, zoomable, zoomExtent, onZoom, dragRotate, graticule, tileURL, tileAttribution
Helpers: resolveReferenceGeography("world-110m"|"world-50m"), mergeData(features, data, { featureKey, dataKey })
Push API: ref.current.push({ time, value }). All pushed data must include a time field.
RealtimeLineChart, RealtimeHistogram (+ brush, onBrush, linkedBrush, direction), TemporalHistogram (static-data sibling of RealtimeHistogram — same props minus windowSize/windowMode; takes a bounded data array), RealtimeSwarmChart, RealtimeWaterfallChart, RealtimeHeatmap, Streaming Sankey (StreamNetworkFrame + showParticles)
Encoding: decay, pulse, transition, staleness — compose freely.
Most HOCs support push via forwardRef. Omit data — do NOT pass data={[]}.
const ref = useRef()
ref.current.push({ id: "p1", x: 1, y: 2 })
ref.current.pushMany([...points])
ref.current.replace([...points]) // ordinal only — full dataset replacement, preserves category order + transitions (progressively chunks large datasets)
ref.current.remove("p1") // by ID — requires pointIdAccessor
ref.current.remove(["p1", "p2"]) // batch remove
ref.current.update("p1", d => ({ ...d, y: 99 })) // in-place update — requires pointIdAccessor
ref.current.clear()
ref.current.getData()
ref.current.getScales() // returns {o, r, projection} (ordinal) / {x, y} (XY) — null if not yet computed
<Scatterplot ref={ref} xAccessor="x" yAccessor="y" pointIdAccessor="id" />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).
Not supported: Tree, Treemap, CirclePack, Orbit, ChoroplethMap, FlowMap, ScatterplotMatrix.
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.
XYCustomChart(semiotic/xy) — XY layouts: waffle, calendar heatmap, custom point/line/area arrangementsOrdinalCustomChart(semiotic/ordinal) — category × value layouts: marimekko, parallel coordinates, bullet, fan chart, slope graphNetworkCustomChart(semiotic/network) — graph layouts: flextree, dagre, custom force/radial
All three accept layout and layoutConfig (your own typed config), but the layout context and return shape differ by chart family:
XYCustomChart/OrdinalCustomChart—layout: (ctx) => { nodes, overlays? }. Context exposesdata,scales,dimensions(with plot rect — center-anchored for radial ordinal, top-left otherwise),theme(semantic + categorical),resolveColor(key), andconfig. XY scales:{ x, y }(linear). Ordinal scales:{ o, r, projection }(band + linear).NetworkCustomChart—layout: (ctx) => { sceneNodes?, sceneEdges?, labels?, overlays? }. Context exposesnodes,edges,dimensions,theme,resolveColor(key), andconfig— graph data, nodata/scales. Network layouts often run an external positioner (d3-flextree,dagre) on nodes/edges, then emit network scene primitives (circle,rect,arcfor nodes;line,bezier,curvedfor edges).
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.
import { XYCustomChart } from "semiotic/xy"
import { OrdinalCustomChart } from "semiotic/ordinal"
import { NetworkCustomChart } from "semiotic/network"
import {
waffleLayout, calendarLayout, // XY recipes
marimekkoLayout, bulletLayout, parallelCoordinatesLayout, // ordinal
flextreeLayout, dagreLayout, // network
} from "semiotic/recipes"
<XYCustomChart data={cells} layout={waffleLayout} layoutConfig={{ rows: 10, columns: 10, ... }} />
<OrdinalCustomChart data={revenue} layout={marimekkoLayout} layoutConfig={{ ... }} />
<NetworkCustomChart nodes={nodes} edges={edges} layout={flextreeLayout} layoutConfig={{ ... }} />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.
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.
Convention across shipped recipes — every recipe takes a consistent set of label/axis toggles in layoutConfig:
| Recipe | Toggle | Default | What it draws |
|---|---|---|---|
marimekkoLayout |
showCategoryLabels |
true |
Category names under each variable-width bar |
bulletLayout |
showLabels |
true |
Metric name to the left of each row |
bulletLayout |
showTicks |
true |
Per-row value-axis ticks (each row independently scaled) |
parallelCoordinatesLayout |
showAxes |
true |
Vertical axis line + field name + 5 ticks per axis |
flextreeLayout / dagreLayout |
showLabels |
true |
Node text rendered inside each rect |
waffleLayout / calendarLayout |
— | — | No chrome needed (uniform grid + cell color tells the story) |
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.
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.
const [hovered, setHovered] = useState(null)
<OrdinalCustomChart
data={rows}
layout={parallelCoordinatesLayout}
layoutConfig={{
fields: ["mpg", "hp", "weight"],
highlightFn: hovered ? (d) => d.name === hovered : undefined,
}}
onObservation={(obs) => {
if (obs.type === "hover") setHovered(obs.datum?.data?.name ?? obs.datum?.name)
else if (obs.type === "hover-end") setHovered(null)
}}
/>Pass showAxes on XYCustomChart / OrdinalCustomChart to get the same x/y or o/r axes the built-in HOCs render — useful for custom layouts that overlay points/lines on regular scales. Recipe-managed chrome is for cases where standard axes don't fit (variable-width bars under a band scale, per-row independent scales, etc.).
Notes:
- Coords are plot-relative (the frame translates the canvas/SVG group by
margin). Readctx.dimensions.plotfor the drawing rect. Radial ordinal projection is the one exception:plot.x = -width/2,plot.y = -height/2because the canvas ctx is center-translated. - Layouts that need axis domains: pass
xExtent/yExtent(XY) oroExtent/rExtent(ordinal) — those flow through scale construction before the layout runs. - 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). - Custom layouts own their colors. Always prefer
ctx.resolveColor(key)over hardcoded literals soThemeProvider/colorSchemeflow through.CategoryColorProviderintegration is XY-only; for cross-chart category sync on network/ordinal customLayouts, pass a matchingcolorSchemeto each chart. - Tooltips: emit datum keys that match the user-visible accessor names (e.g. when
categoryAccessor: "region"andvalueAccessor: "revenue"are passed, putregionandrevenueon each rect'sdatum). 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.
LinkedCharts — selections, CategoryColorProvider — colors|categories + colorScheme
Chart props: selection, linkedHover, linkedBrush. Hooks: useSelection, useLinkedHover, useBrushSelection
Shared categories inside LinkedCharts → wrap in CategoryColorProvider. When two or more charts encode the same categorical field (e.g. both colorBy="region"), wrapping in CategoryColorProvider gives every chart identical colors per category AND makes LinkedCharts render one unified legend (and suppress individual chart legends). Without it, each chart renders its own legend independently — often with mismatched colors.
Linked crosshair: linkedHover={{ name: "sync", mode: "x-position", xField: "time" }}. Click-to-lock: click locks crosshair (dashed white), click/Escape unlocks.
ScatterplotMatrix, ChartContainer (title, subtitle, actions), ChartGrid (columns, gap), ContextLayout
HOC charts render SVG automatically in server environments. For standalone generation:
import { renderChart, renderToImage, renderToAnimatedGif, renderDashboard } from "semiotic/server"
const svg = renderChart("BarChart", { data, categoryAccessor: "region", valueAccessor: "revenue", theme: "tufte", showLegend: true, showGrid: true, annotations: [...] })
const png = await renderToImage("LineChart", { data, ... }, { format: "png", scale: 2 }) // requires sharp
const gif = await renderToAnimatedGif("line", data, { xAccessor: "x", yAccessor: "y", theme: "dark" }, { fps: 12, transitionFrames: 4, decay: { type: "linear" } }) // requires sharp + gifenc
const dashboard = renderDashboard([{ component: "BarChart", props: {...} }, { component: "PieChart", colSpan: 2, props: {...} }], { title: "Q1", theme: "dark", layout: { columns: 2 } })All render functions accept theme (preset name or object). Theme categorical colors flow to data marks automatically. generateFrameSVGs() returns frame SVGs without sharp/gifenc (sync, for client preview).
AnimatedGifOptions: fps, stepSize, windowSize, frameCount, xExtent/yExtent (lock axes), transitionFrames, easing, decay, loop, scale.
Server SVGs include role="img", <title>, <desc>, grid, legend, annotations (y-threshold, x-threshold, band, label, text, category-highlight). SVG groups have id attributes for Figma layer naming: data-area, axes, grid, annotations, legend, chart-title.
renderChart required props by component:
- Sparkline —
data,xAccessor,yAccessor. No axes/grid/legend/title by default. Margin defaults to 2px. - LineChart/AreaChart —
data,xAccessor,yAccessor. Optional:lineBy/areaBy,colorBy,colorScheme. - StackedAreaChart —
data,xAccessor,yAccessor,areaBy(required). - Scatterplot/BubbleChart —
data,xAccessor,yAccessor. BubbleChart requiressizeBy. - Heatmap —
data,xAccessor,yAccessor,valueAccessor. - BarChart —
data,categoryAccessor,valueAccessor. - StackedBarChart —
data,categoryAccessor,valueAccessor,stackBy(required). - GroupedBarChart —
data,categoryAccessor,valueAccessor,groupBy(required). - PieChart/DonutChart —
data,categoryAccessor,valueAccessor. - FunnelChart —
data,stepAccessor("step"),valueAccessor("value"). Renders with trapezoid connectors, no axes. - GaugeChart —
value. Optional:thresholds(array of{value, color, label}),min,max,sweep,arcWidth,cornerRadius. - SwimlaneChart —
data,categoryAccessor,subcategoryAccessor(required),valueAccessor. - ForceDirectedGraph —
nodes,edges(both required). If deriving nodes from edge endpoints, materializenodesbefore returning JSX/renderChart props. - SankeyDiagram —
edges(required),valueAccessor. - ChoroplethMap —
areas(GeoJSON features, pre-resolved).
All components accept: width, height, theme, title, description, showLegend, showGrid, background, annotations, margin, colorScheme, colorBy, legendPosition. Pass additional frame-level props via frameProps.
All HOCs accept annotations (array). Coordinates use data field names.
Positioning: widget, label, callout, text, bracket
Reference lines: y-threshold (value, label, color, labelPosition), x-threshold, band (y0, y1)
Ordinal: category-highlight
Enclosures: enclose, rect-enclose, highlight
Statistical: trend, envelope, anomaly-band, forecast
Streaming anchors: "fixed" | "latest" | "sticky"
CSS custom properties: --semiotic-bg, --semiotic-text, --semiotic-text-secondary, --semiotic-border, --semiotic-grid, --semiotic-primary, --semiotic-secondary, --semiotic-surface, --semiotic-success, --semiotic-danger, --semiotic-warning, --semiotic-error, --semiotic-info, --semiotic-focus, --semiotic-font-family, --semiotic-annotation-color, --semiotic-legend-font-size, --semiotic-title-font-size, --semiotic-tick-font-family, --semiotic-tick-font-size (10px default — drives axis tick text), --semiotic-axis-label-font-size (12px default — drives axis-label text + foreignObject ticks), --semiotic-tooltip-bg/text/radius/font-size/shadow.
<ThemeProvider theme="tufte"> {/* Named preset */}
<ThemeProvider theme={{ mode: "dark", colors: { categorical: [...] } }}> {/* Merge onto dark base */}Color priority (with colorBy): CategoryColorProvider/LinkedCharts category map > explicit colorScheme fallback > ThemeProvider colors.categorical > "category10".
Presets: light, dark, high-contrast, pastels(-dark), bi-tool(-dark), italian(-dark), tufte(-dark), journalist(-dark), playful(-dark), carbon(-dark).
Serialization: themeToCSS(theme, selector), themeToTokens(theme), resolveThemePreset(name).
Semantic status roles (on every preset): colors.success, colors.danger, colors.warning, colors.error, colors.info, plus colors.secondary and colors.surface. Each emits as a --semiotic-{role} CSS custom property. Use for status-driven charts: <Waterfall positiveColor="var(--semiotic-success)" negativeColor="var(--semiotic-danger)" />, <Swimlane color="var(--semiotic-warning)" />, bar stroke delineation <RealtimeHistogram stroke="var(--semiotic-border)" />, status annotations.
Scoped CSS cascade override (per-subtree, no ThemeProvider needed):
<div style={{ "--semiotic-danger": "#4b0082" }}>
{/* every chart below inherits this danger color via canvas CSS-var lookup */}
</div>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).
onObservation/useChartObserver, toConfig/fromConfig/toURL/fromURL/copyConfig/configToJSX, validateProps(component, props), diagnoseConfig(component, props), exportChart(div, { format }), npx semiotic-ai --doctor
Headless hook for "chat with the chart" interactions. The library ships no UI — bring your own chat surface.
useChartInterrogation({ data, onQuery, componentName?, props?, initialAnnotations? })→{ ask(query), history, summary, annotations, loading, error, reset }onQuery: (query, context) => Promise<{ answer, annotations? }>— call your LLM here.contextis{ data, summary, componentName?, props? }.summary: LLM-friendly statistical summary (rowCount, per-field{ min, max, mean, median }for numerics, top-k for categoricals, ISO range for dates). Available before any ask().annotations: MergedinitialAnnotations+ latest AI response. Wire to the chart'sannotationsprop for visual highlighting.summarizeData(data, options?): Standalone for server-side prompting or batch jobs.- MCP Tool:
interrogateChart(component, props, query)returns the same statistical summary and AI-facing instructions.
import { LineChart, useChartInterrogation } from "semiotic/ai"
function InterrogatableChart({ data }) {
const { ask, history, annotations, loading } = useChartInterrogation({
data,
componentName: "LineChart",
props: { xAccessor: "month", yAccessor: "revenue" },
onQuery: async (query, { summary }) => {
const res = await myLLMCall(query, summary)
return { answer: res.text, annotations: res.highlights }
},
})
return (
<>
<LineChart data={data} xAccessor="month" yAccessor="revenue" annotations={annotations} />
<YourChatUI history={history} loading={loading} onAsk={ask} />
</>
)
}Heuristic chart-suggestion engine. Charts ship capability descriptors next to their TSX files; the engine ranks them against a profiled dataset by intent. No LLM call required.
profileData(data, { rawInput?, seriesField? })→ChartDataProfile(extendsDataSummary): candidate fields per role (x/y/series/category/size/time), distinct counts, monotonicity, structure detection (hierarchy/network/geo).suggestCharts(data, { intent?, allow?, deny?, maxResults?, includeVariants?, minScore? })→ rankedSuggestion[]with{ component, family, importPath, variant?, score, intentScores, rubric, reasons, caveats, props }.propsis spreadable directly into the matching chart.scoreChart(component, data, { intent?, variantKey? })→ evaluate a specific chart for a dataset (does it fit, how well, why/why not).useChartSuggestions(data, options)→ memoized React hook returning{ suggestions, profile }.registerChartCapability(capability)/unregisterChartCapability(name)— runtime registration for custom charts.- Intent taxonomy: 13 built-in intents (
trend,compare-series,compare-categories,rank,part-to-whole,distribution,correlation,flow,hierarchy,geo,outlier-detection,composition-over-time,change-detection). Extend viaregisterIntent(descriptor). - Capability authoring: create
Foo.capability.tsnext toFoo.tsx, then append to the registry insrc/components/ai/chartCapabilities.ts. Each capability declaresfamily,rubric(familiarity/accuracy/precision 1-5),fits(profile)gate,intentScores, optionalvariantswithintentDeltas, andbuildProps(profile, variant). - Variants encode that settings change what a chart is good for: e.g.
StackedAreaChart'sstreamgraphvariant boosts trend but penalizes part-to-whole. - Interrogation tie-in: pass
includeSuggestions: truetouseChartInterrogationand the same ranked list lands incontext.suggestionsfor the LLM. - MCP tool:
suggestCharts(data, intent?)returns the ranked list as structured content.
import { useChartSuggestions, LineChart, BarChart, /* ... */ } from "semiotic/ai"
const COMPONENT_MAP = { LineChart, BarChart, /* ... */ }
function SuggestedChart({ data, intent }) {
const { suggestions } = useChartSuggestions(data, { intent })
const top = suggestions[0]
if (!top) return null
const Component = COMPONENT_MAP[top.component]
return <Component {...top.props} />
}These rules are generated from ai/behaviorContracts.cjs and are consumed by semiotic-ai --doctor, MCP resources, and docs checks.
- 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. Agent action: Pass usageMode="push" tosemiotic-ai --doctorwhen validating ref-based JSX with no data prop. Keep usageMode="static" or omit it for renderChart/MCP/static configs where data must be present. - 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. 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. - 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. 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. 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. - 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. 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. - 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. Agent action: When generating code that calls remove() or update(), include the matching ID accessor and make sure pushed rows carry that ID field. - 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. 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.
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().
- Tooltip datum shape: HOC tooltips get raw data. Frame
tooltipContentgets wrapped — used.data. - Legend: "bottom" expands margin ~80px. MultiAxisLineChart: use
legendPosition="bottom". - Log scale: Domain min clamped to 1e-6.
- barPadding: Pixel value (40/60 default). Reduce for small charts.
- sort (BarChart/StackedBarChart/GroupedBarChart/DotPlot):
falsepreserves insertion order;"auto"= insertion-order while streaming, value-desc on static (DotPlot default — opt-in on others, avoids category shuffling under the push API). StackedBar/GroupedBar default tofalse; the underlying frame value-sorts whenoSortis undefined, so always passsortexplicitly if order matters. - Tooltip format cascade:
valueFormat/xFormat/yFormatflow to the default tooltip automatically, so axis and tooltip read identically. A customtooltipprop fully overrides — re-pass viaTooltip({format})/MultiLineTooltip({fields:[{format}]}). Bespoke-tooltip charts (Histogram, FunnelChart, LikertChart, GaugeChart) don't participate; customize viatooltip. - Horizontal bars: Need wider left margin:
margin={{ left: 120 }}. - Push API: Omit
dataentirely.data={[]}clears on every render. - frameProps style functions: Bypass HOC color resolution — use
colorByprop instead. - Geo imports: Always
semiotic/geo, neversemiotic, to avoid d3-geo in non-geo bundles. - fillArea:
fillArea={["seriesA"]}fills named series only. Names must matchlineBy/colorBykeys. - hoverHighlight: Requires
colorByas a string field. - tooltip="multi": Shows all series at hovered X for LineChart, AreaChart, and StackedAreaChart. Custom fn receives
datum.allSeries. - Axis config:
frameProps.axes: [{ orient, includeMax, autoRotate, gridStyle, landmarkTicks, tickAnchor }].tickAnchor: "edges"flips the first tick'stext-anchortostartand the last toendon horizontal axes (anddominant-baselinetohanging/autoon vertical axes) so edge labels don't overflow the plot. Pairs naturally withaxisExtent: "exact". - Targeting individual axes from CSS: every axis renders as its own
<g class="semiotic-axis semiotic-axis-{bottom|left|right|top}" data-orient="…">. Style with[data-orient="left"] text { font-size: 14px }or via the class names — no!importantneeded because the CSS-var defaults are set inline viavar(--semiotic-tick-font-size, …), so cascade overrides win cleanly. Tick text carriesclass="semiotic-axis-tick"; labels carryclass="semiotic-axis-label"; titles carryclass="semiotic-chart-title". - xScaleType: "time": Creates
scaleTime. Required for landmark ticks with timestamps. - scalePadding: Pixel inset on scale ranges. Pass via
frameProps={{ scalePadding: 12 }}. - categoryFormat/xFormat/yFormat: Can return ReactNode (renders in
<foreignObject>). - Tick deduplication: Adjacent identical labels auto-removed.
- Composing overlays: XY/Ordinal charts paint
--semiotic-bgacross the canvas; stack withframeProps={{ background: "transparent" }}on the overlay. Network/Geo don't paint bg by default.
Prefer string accessors (xAccessor="value") — always referentially stable. Memoize function accessors with useCallback.