Skip to content

Latest commit

 

History

History
380 lines (301 loc) · 39.6 KB

File metadata and controls

380 lines (301 loc) · 39.6 KB

Semiotic — AI Assistant Guide

Quick Start

  • Install: npm install semiotic
  • Use sub-path importssemiotic/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). Full semiotic is 188KB gz.
  • CLI: npx semiotic-ai [--schema|--compact|--examples|--doctor]
  • MCP: npx semiotic-mcp

Architecture

  • 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/RealtimeEdge wrappers in callbacks, not your data.
  • Every HOC accepts frameProps for pass-through. TypeScript strict: true. Every HOC has error boundary + dev-mode validation.

Common Props (all HOCs)

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 }.

XY Charts (semiotic/xy)

LineChartdata, 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". baselinenormalize. No lineBy/lineDataAccessor. tooltip="multi" lists every series at the hovered x (values interpolated between samples). Scatterplotdata, 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. Heatmapdata, xAccessor, yAccessor, valueAccessor, colorScheme, showValues, cellBorderColor ScatterplotMatrixdata, fields (array of numeric field names for grid) MinimapChart — Overview + detail with linked zoom. Wraps an XY chart. CandlestickChartdata, 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).

Ordinal Charts (semiotic/ordinal)

BarChartdata, 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) SwarmPlotcolorBy, 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 PieChartcategoryAccessor, valueAccessor, colorBy, startAngle DonutChart — PieChart + innerRadius (60), centerContent FunnelChartstepAccessor, valueAccessor, categoryAccessor (optional), connectorOpacity, orientation SwimlaneChartcategoryAccessor, 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.) LikertChartcategoryAccessor, valueAccessor|levelAccessor+countAccessor, levels (required), orientation, colorScheme GaugeChartvalue (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).

Network Charts (semiotic/network)

ForceDirectedGraphnodes, edges, nodeIDAccessor, sourceAccessor, targetAccessor, colorBy, nodeSize, nodeSizeRange, edgeWidth, iterations (300), forceStrength (0.1), showLabels, nodeLabel SankeyDiagramedges, 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. ChordDiagramedges, nodes, valueAccessor, edgeColorBy, padAngle, showLabels TreeDiagramdata (root), layout, orientation, childrenAccessor, colorBy, colorByDepth Treemapdata (root), childrenAccessor, valueAccessor, colorBy, colorByDepth, showLabels CirclePackdata (root), childrenAccessor, valueAccessor, colorBy, colorByDepth OrbitDiagramdata (root), childrenAccessor, orbitMode, speed, animated (true), colorBy

Geo Charts (semiotic/geo)

Import from semiotic/geo — NOT semiotic — to avoid pulling d3-geo into non-geo bundles.

ChoroplethMapareas (GeoJSON Feature[] or "world-110m"), valueAccessor, colorScheme, projection ("equalEarth"), graticule, tooltip, showLegend ProportionalSymbolMappoints, xAccessor ("lon"), yAccessor ("lat"), sizeBy, sizeRange, colorBy, areas (optional background) FlowMapflows, nodes, valueAccessor, edgeColorBy, lineType, showParticles DistanceCartogrampoints, 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 })

Realtime Charts (semiotic/realtime)

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.

Push API on HOC charts

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.

Custom Charts (escape hatch)

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 arrangements
  • OrdinalCustomChart (semiotic/ordinal) — category × value layouts: marimekko, parallel coordinates, bullet, fan chart, slope graph
  • NetworkCustomChart (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 / OrdinalCustomChartlayout: (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).
  • NetworkCustomChartlayout: (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).

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.

Chrome (labels, axes, legends): the recipe owns it

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.

Interaction (hover, brush, selection): the parent component owns it

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)
  }}
/>

Built-in chrome works when the layout uses standard scales

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). Read ctx.dimensions.plot for the drawing rect. Radial ordinal projection is the one exception: plot.x = -width/2, plot.y = -height/2 because the canvas ctx is center-translated.
  • Layouts that need axis domains: pass xExtent/yExtent (XY) or oExtent/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 so ThemeProvider / colorScheme flow through. CategoryColorProvider integration is XY-only; for cross-chart category sync on network/ordinal customLayouts, pass a matching colorScheme to each chart.
  • Tooltips: emit datum keys that match the user-visible accessor names (e.g. when categoryAccessor: "region" and valueAccessor: "revenue" are passed, put region and revenue on each rect's datum). The default tooltip looks them up by accessor name and the user's custom tooltip will read whatever fields they expect. Avoid underscored synthetic keys — the default tooltip filters those out.

Coordinated Views

LinkedChartsselections, CategoryColorProvidercolors|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

Server-Side Rendering (semiotic/server)

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:

  • Sparklinedata, xAccessor, yAccessor. No axes/grid/legend/title by default. Margin defaults to 2px.
  • LineChart/AreaChartdata, xAccessor, yAccessor. Optional: lineBy/areaBy, colorBy, colorScheme.
  • StackedAreaChartdata, xAccessor, yAccessor, areaBy (required).
  • Scatterplot/BubbleChartdata, xAccessor, yAccessor. BubbleChart requires sizeBy.
  • Heatmapdata, xAccessor, yAccessor, valueAccessor.
  • BarChartdata, categoryAccessor, valueAccessor.
  • StackedBarChartdata, categoryAccessor, valueAccessor, stackBy (required).
  • GroupedBarChartdata, categoryAccessor, valueAccessor, groupBy (required).
  • PieChart/DonutChartdata, categoryAccessor, valueAccessor.
  • FunnelChartdata, stepAccessor ("step"), valueAccessor ("value"). Renders with trapezoid connectors, no axes.
  • GaugeChartvalue. Optional: thresholds (array of {value, color, label}), min, max, sweep, arcWidth, cornerRadius.
  • SwimlaneChartdata, categoryAccessor, subcategoryAccessor (required), valueAccessor.
  • ForceDirectedGraphnodes, edges (both required). If deriving nodes from edge endpoints, materialize nodes before returning JSX/renderChart props.
  • SankeyDiagramedges (required), valueAccessor.
  • ChoroplethMapareas (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.

Annotations

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"

Theming

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).

AI Features

onObservation/useChartObserver, toConfig/fromConfig/toURL/fromURL/copyConfig/configToJSX, validateProps(component, props), diagnoseConfig(component, props), exportChart(div, { format }), npx semiotic-ai --doctor

Conversational Interrogation (semiotic/ai)

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. context is { 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: Merged initialAnnotations + latest AI response. Wire to the chart's annotations prop 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} />
    </>
  )
}

Chart Capability Layer (semiotic/ai)

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 (extends DataSummary): 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? }) → ranked Suggestion[] with { component, family, importPath, variant?, score, intentScores, rubric, reasons, caveats, props }. props is 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 via registerIntent(descriptor).
  • Capability authoring: create Foo.capability.ts next to Foo.tsx, then append to the registry in src/components/ai/chartCapabilities.ts. Each capability declares family, rubric (familiarity/accuracy/precision 1-5), fits(profile) gate, intentScores, optional variants with intentDeltas, and buildProps(profile, variant).
  • Variants encode that settings change what a chart is good for: e.g. StackedAreaChart's streamgraph variant boosts trend but penalizes part-to-whole.
  • Interrogation tie-in: pass includeSuggestions: true to useChartInterrogation and the same ranked list lands in context.suggestions for 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} />
}

AI Behavior Contracts

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" to semiotic-ai --doctor when validating ref-based JSX with no data prop. Keep usageMode="static" or omit it for renderChart/MCP/static configs where data must be present.
  • 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.

Accessibility

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().

Usage Notes

  • Tooltip datum shape: HOC tooltips get raw data. Frame tooltipContent gets wrapped — use d.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): false preserves insertion order; "auto" = insertion-order while streaming, value-desc on static (DotPlot default — opt-in on others, avoids category shuffling under the push API). StackedBar/GroupedBar default to false; the underlying frame value-sorts when oSort is undefined, so always pass sort explicitly if order matters.
  • Tooltip format cascade: valueFormat/xFormat/yFormat flow to the default tooltip automatically, so axis and tooltip read identically. A custom tooltip prop fully overrides — re-pass via Tooltip({format}) / MultiLineTooltip({fields:[{format}]}). Bespoke-tooltip charts (Histogram, FunnelChart, LikertChart, GaugeChart) don't participate; customize via tooltip.
  • Horizontal bars: Need wider left margin: margin={{ left: 120 }}.
  • Push API: Omit data entirely. data={[]} clears on every render.
  • frameProps style functions: Bypass HOC color resolution — use colorBy prop instead.
  • Geo imports: Always semiotic/geo, never semiotic, to avoid d3-geo in non-geo bundles.
  • fillArea: fillArea={["seriesA"]} fills named series only. Names must match lineBy/colorBy keys.
  • hoverHighlight: Requires colorBy as 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's text-anchor to start and the last to end on horizontal axes (and dominant-baseline to hanging/auto on vertical axes) so edge labels don't overflow the plot. Pairs naturally with axisExtent: "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 !important needed because the CSS-var defaults are set inline via var(--semiotic-tick-font-size, …), so cascade overrides win cleanly. Tick text carries class="semiotic-axis-tick"; labels carry class="semiotic-axis-label"; titles carry class="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-bg across the canvas; stack with frameProps={{ background: "transparent" }} on the overlay. Network/Geo don't paint bg by default.

Performance

Prefer string accessors (xAccessor="value") — always referentially stable. Memoize function accessors with useCallback.