- Install:
npm install semiotic - Import:
semiotic,semiotic/xy,semiotic/ordinal,semiotic/network,semiotic/geo,semiotic/realtime,semiotic/ai,semiotic/data,semiotic/server,semiotic/themes,semiotic/utils - CLI:
npx semiotic-ai [--schema|--compact|--examples|--doctor] - MCP:
npx semiotic-mcp - Every HOC has a built-in error boundary and dev-mode validation warnings
- HOC Charts: Simple props, sensible defaults. Stream Frames: Full control.
- Always use HOC charts unless you need control they don't expose. Stream Frames (
StreamNetworkFrame,StreamXYFrame,StreamOrdinalFrame,StreamGeoFrame) are low-level — they passRealtimeNode/RealtimeEdgewrappers in callbacks, not your data. - Every HOC accepts
framePropsto pass through. TypeScriptstrict: true.
title, description (overrides aria-label), summary (sr-only note), width (600), height (400), responsiveWidth, responsiveHeight, margin, className, color (uniform fill — overrides theme/colorScheme), enableHover (true), tooltip (boolean | (datum) => ReactNode | { fields?, title?, format?, style? }), showLegend, showGrid (false), frameProps, onObservation, chartId, loading (false), emptyContent, legendInteraction ("none"|"highlight"|"isolate"), legendPosition ("right"|"left"|"top"|"bottom"), emphasis ("primary"|"secondary"), annotations (array), accessibleTable (true)
onObservation receives { type: "hover"|"hover-end"|"click"|"brush"|"selection", datum?, x?, y?, timestamp, chartType, chartId }. The datum is your original data object.
LineChart — data, xAccessor ("x"), yAccessor ("y"), lineBy, lineDataAccessor ("coordinates"), colorBy, colorScheme, curve, lineWidth (2), showPoints, pointRadius (3), fillArea, areaOpacity (0.3), anomaly, forecast, directLabel, gapStrategy ("break"|"interpolate"|"zero"), xScaleType/yScaleType ("linear"|"log")
AreaChart — LineChart props + areaBy, y0Accessor (band/ribbon), gradientFill (boolean|{topOpacity,bottomOpacity}), areaOpacity (0.7), showLine (true)
StackedAreaChart — flat array + areaBy (required), colorBy, normalize. Do NOT use lineBy or lineDataAccessor.
Scatterplot — data, xAccessor, yAccessor, colorBy, sizeBy, sizeRange, pointRadius (5), pointOpacity (0.8), marginalGraphics
BubbleChart — Scatterplot + sizeBy (required), sizeRange ([5,40]), bubbleOpacity (0.6)
ConnectedScatterplot — data, xAccessor, yAccessor, orderAccessor (sequencing field), pointRadius (4)
QuadrantChart — Scatterplot + quadrants (required: { topRight, topLeft, bottomRight, bottomLeft } each { label, color, opacity? }), xCenter, yCenter, centerlineStyle, showQuadrantLabels (true). Supports push API.
MultiAxisLineChart — Dual Y-axis. data, xAccessor ("x"), series (required: array of { yAccessor, label?, color?, format?, extent? }), colorScheme, curve ("monotoneX"), lineWidth (2). Data unitized to [0,1] internally; left axis=series[0], right axis=series[1] in original units. For push API, provide series[].extent for stable unitization. Falls back to standard multi-line if not exactly 2 series.
Heatmap — data, xAccessor, yAccessor, valueAccessor, colorScheme ("blues"|"reds"|"greens"|"viridis"), showValues, cellBorderColor. Supports string/categorical axes.
BarChart — data, categoryAccessor, valueAccessor, orientation, colorBy, sort, barPadding (40)
StackedBarChart — + stackBy (required), normalize, barPadding (40)
GroupedBarChart — + groupBy (required), barPadding (60)
SwarmPlot — data, categoryAccessor, valueAccessor, colorBy, sizeBy, pointRadius, pointOpacity
BoxPlot — + showOutliers, outlierRadius
Histogram — + bins (25), relative. Always horizontal. categoryAccessor optional (defaults to "category").
ViolinPlot — + bins, curve, showIQR
RidgelinePlot — + bins, amplitude (1.5, unitless multiplier of lane width)
DotPlot — + sort (true), dotRadius, showGrid default true
PieChart — data, categoryAccessor, valueAccessor, colorBy, startAngle, slicePadding
DonutChart — PieChart + innerRadius (60), centerContent (ReactNode)
FunnelChart — data, stepAccessor ("step"), valueAccessor ("value"), categoryAccessor (optional), colorBy, connectorOpacity (0.3), orientation ("horizontal"|"vertical"). Horizontal: centered bars with trapezoid connectors. Vertical: bars with diagonal hatch for dropoff. Multi-category: categoryAccessor="channel" mirrors (horizontal) or groups (vertical).
SwimlaneChart — data, categoryAccessor ("category"), subcategoryAccessor (required), valueAccessor ("value"), colorBy (defaults to subcategoryAccessor), colorScheme, orientation ("horizontal"|"vertical"), barPadding (40). Renders categorical lanes with items stacked sequentially — unlike StackedBarChart, the same subcategory can appear multiple times in the same lane. Items stack left-to-right (horizontal) or bottom-to-top (vertical) in data order. Wraps StreamOrdinalFrame with chartType="swimlane". Supports push API for streaming.
All ordinal HOCs support colorBy and colorScheme. showCategoryTicks (default true) hides per-tick labels when false — margins auto-adjust. For distribution charts with colorBy, set showCategoryTicks={false} since the legend identifies categories.
ForceDirectedGraph — nodes, edges, nodeIDAccessor, sourceAccessor, targetAccessor, colorBy, colorScheme, nodeSize (number|string|fn), nodeSizeRange, edgeWidth, edgeColor, edgeOpacity, iterations (300), forceStrength (0.1), showLabels, nodeLabel, legendInteraction
SankeyDiagram — edges, nodes, valueAccessor, nodeIdAccessor ("id"), sourceAccessor ("source"), targetAccessor ("target"), colorBy, edgeColorBy ("source"|"target"|"gradient"|fn), orientation, nodeAlign, nodeWidth, nodePaddingRatio, nodeLabel, showLabels, edgeOpacity
ChordDiagram — edges, nodes, valueAccessor, edgeColorBy, padAngle, groupWidth, showLabels
TreeDiagram — data (root), layout, orientation, childrenAccessor, colorBy, colorByDepth, edgeStyle
Treemap — data (root), childrenAccessor, valueAccessor, colorBy, colorByDepth, showLabels, labelMode
CirclePack — data (root), childrenAccessor, valueAccessor, colorBy, colorByDepth, circleOpacity
OrbitDiagram — animated radial/orbital hierarchy. data (root), childrenAccessor, nodeIdAccessor, orbitMode ("flat"|"solar"|"atomic"|number[]), speed (0.25), revolution, eccentricity, orbitSize, nodeRadius, showRings, showLabels, animated (true), colorBy, colorByDepth. For static radial trees, use TreeDiagram layout="radial".
Import from semiotic/geo — NOT semiotic — to avoid pulling d3-geo into non-geo bundles.
ChoroplethMap — areas (GeoJSON Feature[] or "world-110m"), valueAccessor, colorScheme, areaOpacity (1), projection ("equalEarth"), graticule, tooltip, showLegend
ProportionalSymbolMap — points, xAccessor ("lon"), yAccessor ("lat"), sizeBy, sizeRange ([3,30]), colorBy, areas (optional background), projection
FlowMap — flows, nodes, xAccessor, yAccessor, nodeIdAccessor, valueAccessor, edgeColorBy, edgeOpacity (0.6), edgeWidthRange ([1,8]), lineType ("geo"|"line"), showParticles, particleStyle
DistanceCartogram — points, center (id), costAccessor, strength (0-1), lineMode, showRings (true|false|number[]), ringStyle, showNorth, costLabel, transition, pointRadius
All geo HOCs: fitPadding (0–1), zoomable (defaults true with tileURL), zoomExtent ([1,8]), onZoom, dragRotate (true for orthographic), graticule, tileURL, tileAttribution, tileCacheSize, selection, linkedHover, onObservation
Tiles: tileURL accepts string template ({z}/{x}/{y}) or function. Mercator only. OSM tiles are dev-only — use commercial provider with env var key in production.
Zoom: Imperative: ref.current.getZoom(), ref.current.resetZoom().
Reference geography: resolveReferenceGeography("world-110m"|"world-50m"|"land-110m"|"land-50m") returns GeoJSON features.
mergeData(features, data, { featureKey, dataKey }) — join data into GeoJSON by key. World-atlas uses ISO numeric codes as id.
import { ChoroplethMap, resolveReferenceGeography, mergeData } from "semiotic/geo"
const world = await resolveReferenceGeography("world-110m")
const areas = mergeData(world, gdpData, { featureKey: "id", dataKey: "id" })
<ChoroplethMap areas={areas} valueAccessor="gdpPerCapita" colorScheme="viridis"
projection="equalEarth" zoomable tooltip />StreamGeoFrame — low-level frame. Push API: ref.current.push(datum), .pushMany(), .clear(). Props: projection, areas, points, lines, xAccessor, yAccessor, areaStyle, pointStyle, lineStyle, graticule, zoomable, decay, pulse, transition.
Push API: chartRef.current.push({ time, value })
IMPORTANT: All pushed data must include a time field (default: "time"). Set timeAccessor if your field differs. Without valid time field, charts render blank.
RealtimeLineChart — timeAccessor ("time"), valueAccessor ("value"), windowSize (200), windowMode, stroke, strokeWidth
RealtimeHistogram — binSize (required), timeAccessor, valueAccessor, categoryAccessor, colors, brush (boolean|"x"|object, defaults to { dimension: "x", snap: "bin" } when true), onBrush, linkedBrush (cross-chart coordination)
RealtimeSwarmChart — timeAccessor, valueAccessor, categoryAccessor, radius, opacity
RealtimeWaterfallChart — timeAccessor, valueAccessor, positiveColor, negativeColor
RealtimeHeatmap — timeAccessor, valueAccessor, heatmapXBins, heatmapYBins, aggregation
Streaming Sankey — StreamNetworkFrame with chartType="sankey", showParticles, particleStyle. Push individual edges: ref.current.push({ source, target, value }).
Encoding: decay, pulse, transition, staleness — compose freely on all streaming charts.
All Realtime* charts accept data props for static mode (no push API needed). RealtimeHistogram brush supports bin-snapping (snap: "bin") and streaming tracking — the brush shrinks as selected bins scroll off and auto-clears when fully evicted.
Most HOC charts support push via forwardRef. Omit data/nodes/edges — do NOT pass data={[]}.
const ref = useRef()
ref.current.push({ x: 1, y: 2 }) // single
ref.current.pushMany([...points]) // batch
ref.current.clear() // reset
ref.current.getData() // read
<Scatterplot ref={ref} xAccessor="x" yAccessor="y" />Supported: all XY, ordinal, network (Force, Sankey, Chord), geo point charts. Not supported: hierarchy charts (Tree, Treemap, CirclePack, Orbit), ChoroplethMap, FlowMap, ScatterplotMatrix.
Frame callbacks (nodeStyle, edgeStyle, nodeSize as fn) receive RealtimeNode/RealtimeEdge wrappers. Access original data via .data:
// WRONG: nodeSize={(d) => d.weight} — d.weight is undefined
// RIGHT: nodeSize={(d) => d.data?.weight} — or use string: nodeSize="weight"Same applies to frameProps style functions on HOCs. customHoverBehavior/customClickBehavior receive { type, data, x, y } | null. tooltipContent receives { type, data }.
The hover dot automatically matches the hovered element's color (line stroke, point fill, etc.). Override via frameProps:
<LineChart frameProps={{ hoverAnnotation: { pointColor: "#ff0000" } }} />Fallback chain: pointColor → element color → --semiotic-primary CSS var → #007bff.
LinkedCharts — selections (resolution: "union"|"intersect"|"crossfilter"), showLegend, legendPosition, legendInteraction, legendSelectionName, legendField
CategoryColorProvider — colors (map) or categories + colorScheme
Chart props: selection, linkedHover, linkedBrush. Hooks: useSelection, useLinkedHover, useBrushSelection, useFilteredData
ScatterplotMatrix — data, fields, colorBy, cellSize, hoverMode, brushMode
ChartContainer — title, subtitle, height (400), width ("100%"), status, loading, error, errorBoundary, actions ({ export, fullscreen, copyConfig, dataSummary }), controls
ChartGrid — columns (number|"auto"), minCellWidth (300), gap (16). emphasis="primary" spans two columns.
ContextLayout — context (ReactNode), position, contextSize (250)
// Cross-highlighting dashboard
<CategoryColorProvider categories={["North", "South", "East"]}>
<LinkedCharts>
<ChartGrid columns={2}>
<LineChart data={d} colorBy="region" linkedHover={{ name: "hl", fields: ["region"] }} selection={{ name: "hl" }} emphasis="primary" responsiveWidth />
<BarChart data={d} colorBy="region" linkedHover={{ name: "hl", fields: ["region"] }} selection={{ name: "hl" }} responsiveWidth />
</ChartGrid>
</LinkedCharts>
</CategoryColorProvider>
// Forecast + anomaly
<LineChart data={ts} xAccessor="time" yAccessor="value"
forecast={{ trainEnd: 60, steps: 15, confidence: 0.95 }}
anomaly={{ threshold: 2 }} />
// Pre-computed forecast bounds
<LineChart data={ml} xAccessor="time" yAccessor="value"
forecast={{ isTraining: "isTraining", isForecast: "isForecast", isAnomaly: "isAnomaly", upperBounds: "upper", lowerBounds: "lower" }} />
// Percentile band — layer AreaChart + LineChart
<>
<AreaChart data={d} xAccessor="x" yAccessor="p95" y0Accessor="p5"
showLine={false} areaOpacity={0.3} gradientFill />
<LineChart data={d} xAccessor="x" yAccessor="p50" lineWidth={2} />
</>
// Streaming sankey with particles
const sankeyRef = useRef()
sankeyRef.current.push({ source: "Web", target: "API", value: 1 })
<StreamNetworkFrame ref={sankeyRef} chartType="sankey"
showParticles particleStyle={{ radius: 2, colorBy: "source" }}
width={600} height={400} />
// SSR
import { renderOrdinalToStaticSVG } from "semiotic/server"
const svg = renderOrdinalToStaticSVG({ data, categoryAccessor: "cat", valueAccessor: "val", width: 600, height: 400 })All HOCs accept annotations (array). Coordinates use your data field names. Network/orbit use nodeId.
Positioning: widget (React content at data coords — v3 replacement for v2 htmlAnnotationRules; props: content, dx, dy, width, height, anchor), label (callout with connector), callout (circle + label), text (plain text), bracket
Reference lines: y-threshold (value, label, color), x-threshold, band (y0, y1, label, fill)
Enclosures: enclose (circle around coordinates), rect-enclose, highlight (filter fn or field+value)
Statistical (XY): trend (method: linear/polynomial/loess), envelope, anomaly-band, forecast
Streaming anchors: "fixed" (default), "latest" (tracks newest datum), "sticky" (freezes when evicted)
Custom rendering: frameProps.svgAnnotationRules = (annotation, index, context) => ReactNode | null. Context has scales, width, height, data. Colors inherit from theme (--semiotic-primary, --semiotic-text-secondary).
<LineChart data={data} xAccessor="time" yAccessor="latency"
annotations={[
{ type: "y-threshold", value: 200, label: "SLA limit", color: "#e45050" },
{ type: "widget", time: 42, latency: 850, dy: -30, content: <span>Incident</span> },
]} />Charts are themeable via CSS custom properties on any ancestor element. Key vars: --semiotic-bg, --semiotic-text, --semiotic-text-secondary, --semiotic-border, --semiotic-grid, --semiotic-primary, --semiotic-focus, --semiotic-font-family, --semiotic-border-radius, --semiotic-tooltip-bg/text/radius/font-size/shadow, --semiotic-selection-color/opacity, --semiotic-diverging.
import { ThemeProvider } from "semiotic"
<ThemeProvider theme="tufte"> {/* Named preset */}
<ThemeProvider theme={{ colors: { primary: "#ff6b6b", categorical: [...] } }}> {/* Custom */}Presets: light, dark, high-contrast, pastels, pastels-dark, bi-tool, bi-tool-dark, italian, italian-dark, tufte, tufte-dark, journalist, journalist-dark, playful, playful-dark.
Serialization (semiotic/themes): themeToCSS(theme, selector), themeToTokens(theme), resolveThemePreset(name).
Color-blind palette: import { COLOR_BLIND_SAFE_CATEGORICAL } from "semiotic" (8-color Wong 2011).
semiotic/utils (~137KB, ~10% of full bundle) — Lightweight entry point for utilities without any chart components:
- Theme:
ThemeProvider,useTheme,LIGHT_THEME,DARK_THEME,HIGH_CONTRAST_THEME,COLOR_BLIND_SAFE_CATEGORICAL,themeToCSS,themeToTokens,resolveThemePreset,THEME_PRESETS - Format:
adaptiveTimeTicks,smartTickFormat - Color:
darkenColor,lightenColor - Patterns:
createHatchPattern - Validation:
validateProps,diagnoseConfig - Serialization:
toConfig,fromConfig,toURL,fromURL,copyConfig,configToJSX,serializeSelections,deserializeSelections,exportChart - Vega-Lite:
fromVegaLite— convert Vega-Lite specs to Semiotic configs - Data structures:
RingBuffer,IncrementalExtent - Tooltip:
normalizeTooltip
Key: ThemeProvider sets CSS vars on a wrapper div (no React context). Canvas charts read vars via getComputedStyle. exportChart inlines computed styles.
Dark/light mode merge rules: String preset (e.g. "dark") → full replacement with that preset's theme. Object with mode (e.g. { mode: "dark", colors: { categorical: [...] } }) → merges onto the matching base theme (DARK_THEME or LIGHT_THEME), so background/text/grid adapt while your overrides are preserved. Object without mode → shallow-merges onto the current theme (partial override). ThemeProvider is reactive — changing the theme prop re-applies immediately.
CSS interop: Host app --semiotic-* vars on :root are overridden by ThemeProvider's closer wrapper div. To let app tokens flow through, either skip ThemeProvider and set --semiotic-* vars in CSS, or use the hybrid approach (ThemeProvider for palette only, CSS vars for chrome).
- HOC charts and Frames render SVG automatically in server environments
renderXYToStaticSVG(props),renderOrdinalToStaticSVG(props),renderNetworkToStaticSVG(props),renderGeoToStaticSVG(props)fromsemiotic/serverframeTypeis"xy"|"ordinal"|"network"|"geo"(NOT component names)- Geo SSR requires pre-resolved features (synchronous — call
resolveReferenceGeographyfirst) - Works with Next.js App Router, Remix, Astro
onObservation/useChartObserver— structured events across chartstoConfig/fromConfig/toURL/fromURL/copyConfig/configToJSX— serializationDetailsPanel— click-driven detail panel inChartContainervalidateProps(componentName, props)— prop validation with typo suggestionsdiagnoseConfig(componentName, props)— anti-pattern detector (13+ checks)exportChart(containerDiv, { format: "png"|"svg" })— pass wrapper div, composites canvas+SVGnpx semiotic-ai --doctor— CLI validation
createHatchPattern({ background, stroke, lineWidth, spacing, angle }) from semiotic — returns CanvasPattern | null for use as fill in style functions. Used by FunnelChart vertical mode for dropoff bars.
Charts render with role="group" (outer interactive wrapper, keyboard/focus) and role="img" (inner canvas, read by assistive tech). SVG overlays include <title> and <desc>.
Keyboard navigation: Arrow keys navigate data points. In XY/ordinal charts, ArrowRight/Left moves within a series, ArrowUp/Down switches series. In network charts, arrows move to the spatially nearest node in the pressed direction; Enter cycles edge-connected neighbors. Home/End jump to first/last. PageUp/PageDown skip 10%. Escape clears focus.
Focus ring: Shape-adaptive dashed ring (circle for points, rect for bars, arc for wedges). Color: --semiotic-focus CSS var.
Data summary: accessibleTable (default true) renders a sr-only summary. Activate via keyboard focus or actions.dataSummary in ChartContainer. JIT-computed — no render cost until activated.
Reduced motion: prefers-reduced-motion auto-detected. Transitions skip to end state, orbit stops, pulse/decay disabled.
High contrast: forced-colors / prefers-contrast: more auto-detected. ThemeProvider applies HIGH_CONTRAST_THEME automatically.
Hooks (from semiotic): useReducedMotion(), useHighContrast() — SSR-safe, return false on server.
- Tooltip datum shape: HOC tooltip functions get raw data. Frame
tooltipContentgets wrapped data — used.data. - Legend positioning: "bottom" auto-expands margin ~80px. For narrow charts (<400px), prefer "bottom" or "top".
- MultiAxisLineChart legend: Always use
legendPosition="bottom"(or"top") — the right-hand axis occupies the space where a right-side legend would go. - Log scale: Clamps domain min to 1e-6 (log(0) undefined).
- barPadding: Pixel value, defaults 40/60. Reduce for small charts.
- Horizontal bars: Need wider left margin with long labels:
margin={{ left: 120 }}. - LinkedCharts legends:
CategoryColorProvidersuppresses child legends. Force withshowLegend={true}. - Push API: Omit
dataprop entirely.data={[]}clears pushed data every render. - frameProps style functions: Bypass HOC color resolution — use
colorByprop instead. Frame style functions receive(datum, categoryName), not(datum, index). - v2 migration:
htmlAnnotationRules→widgetannotations +svgAnnotationRules. v2summaryStyleindex-based coloring → v3 category-string-based. - accessibleTable: Direct prop on HOCs. Set
accessibleTable={false}to disable the sr-only data summary.
Prefer string accessors (xAccessor="value") over function accessors — always referentially stable. If you must use functions, memoize with useCallback or define outside the component. The pipeline uses .toString() comparison for inline arrows but this fails for closures capturing changing variables.