@nteract/semiotic
Semiotic — AI Assistant Guide
Install
agr install @nteract/semiotic --target copilotWrites 1 file into .github/copilot-instructions.md, pinned to git-7e049550.
- .github/copilot-instructions.md
Document
Semiotic — AI Assistant Guide
Quick Start
- Install:
npm install semiotic
- Use sub-path imports —
semiotic/xy(145KB gz),semiotic/ordinal(121KB gz),semiotic/network(145KB gz),semiotic/geo(104KB gz),semiotic/realtime(154KB gz),semiotic/realtime/core(144KB gz),semiotic/realtime/react(1KB gz),semiotic/server(224KB gz),semiotic/server/node(224KB gz),semiotic/server/edge(240KB gz),semiotic/utils(85KB gz),semiotic/utils/core(84KB gz),semiotic/utils/react(6KB gz),semiotic/recipes(89KB gz),semiotic/recipes/core(89KB gz),semiotic/recipes/react(2KB gz),semiotic/themes(7KB gz),semiotic/themes/core(7KB gz),semiotic/themes/react(6KB gz),semiotic/data(4KB gz),semiotic/value(8KB gz),semiotic/physics(161KB gz),semiotic/physics/matter(1KB gz),semiotic/physics/rapier(1KB gz),semiotic/ai(505KB gz),semiotic/ai/core(81KB gz),semiotic/controls(12KB gz),semiotic/rough(4KB gz). Fullsemioticis 407KB gz.
- CLI:
npx semiotic-ai [--schema|--compact|--examples|--doctor|--audit-a11y]· MCP:npx semiotic-mcp
Architecture
HOC Charts (simple, default) → Stream Frames (full control). Use HOCs unless you need control they don't expose. Stream Frames pass RealtimeNode/RealtimeEdge wrappers in callbacks, not your data. Every HOC accepts frameProps, has an error boundary + dev-mode validation. TypeScript strict: true.
Common Props (chart HOCs only)
title, description (aria-label), summary (sr-only), width (600), height (400), responsiveWidth, responsiveHeight, margin, className, color (uniform fill), stroke, strokeWidth, opacity, enableHover (true), tooltip (boolean | "multi" | function | config), showLegend, showGrid (false), frameProps, onObservation, onClick, chartId, loading, loadingContent (ReactNode; false suppresses), emptyContent, legendInteraction ("none"|"highlight"|"isolate"), legendPosition ("right"|"left"|"top"|"bottom"), emphasis ("primary"|"secondary"), annotations, accessibleTable (true), hoverHighlight (requires colorBy), hoverRadius (30), animate (boolean | {duration?, easing?, intro?}), axisExtent ("nice"|"exact" — pins first/last tick to data min/max; XY x/y + ordinal value axis only).
This list is not global. Plain value components do not inherit chart-HOC props and must be
validated against their own schema. In particular, BigNumber uses label, description, and
summary; it does not accept title or accessibleTable.
Primitive styling (color/stroke/strokeWidth/opacity): apply to any shape the chart draws. Precedence: top-level prop > frameProps.*Style fn return > HOC base > theme. Use CSS vars (stroke="var(--semiotic-border)") for cascade-overridable theming. Per-datum: use frameProps.pieceStyle/pointStyle/lineStyle fn form.
onClick receives (datum, { x, y }). onObservation receives { type, datum?, x?, y?, timestamp, chartType, chartId }.
XY Charts (semiotic/xy)
LineChart — data, xAccessor ("x"), yAccessor ("y"), lineBy, lineDataAccessor, colorBy, colorScheme, curve, lineWidth (2), showPoints, pointRadius (3), fillArea (boolean|string[]), areaOpacity (0.3), lineGradient, anomaly, forecast, band ({y0Accessor, y1Accessor, style?, perSeries?, interactive?} or array for fan charts; participates in yExtent; non-interactive by default), directLabel, gapStrategy, xScaleType/yScaleType ("linear"|"log"|"time"), styleRules (per-series), tooltip="multi" for hover-anywhere
AreaChart — LineChart props + areaBy, y0Accessor, gradientFill, semanticGradient ({stops: [{offset: 0–1, color?, opacity?}]}; offset follows the resolved y-domain), semanticLine (true with semanticGradient; false keeps the normal stroke/lineGradient), areaOpacity (0.7), showLine (true), band, tooltip="multi"
DifferenceChart — Two-series A/B. Fills between with seriesAColor where A>B, seriesBColor where B>A; crossovers interpolated. data, xAccessor, seriesAAccessor ("a"), seriesBAccessor ("b"), seriesALabel/seriesBLabel, seriesAColor (var(--semiotic-danger))/seriesBColor (var(--semiotic-info)), showLines (true), lineWidth (1.5), showPoints (false), pointRadius (3), curve ("linear"), areaOpacity (0.6), gradientFill, xExtent/yExtent, pointIdAccessor, windowSize. Push via ref.push({x,a,b}). Accessor outputs coerce through toNumber.
BumpChart — Ranking-over-x. Each x-column ranks series by yAccessor; rank is vertical position. data, xAccessor ("x"), yAccessor ("y"), lineBy ("series"), rankDirection ("descending"|"ascending"), ribbon (false — encode magnitude as true perpendicular-offset ribbon width instead of fixed-width lines; line↔ribbon share centerline geometry so animate tweens width only), curve ("smooth"|"linear"), ribbonSizeRange ([4,28]), samplesPerSegment (12), lineWidth (3), highlightTop (color only the N best series by mean rank; the rest share neutralColor), neutralColor, styleRules (per-series; resolve against each series' first observation), showPoints, showLabels (true|"start"|"end"|"both"), hoverHighlight, annotations (x may use original x values incl. Date). Wraps XYCustomChart.
StackedAreaChart — flat array + areaBy (required), colorBy, normalize, baseline ("zero"|"wiggle" streamgraph|"silhouette" centered), stackOrder ("key"|"insideOut"|"asc"|"desc"). Streamgraph: baseline="wiggle" + stackOrder="insideOut". baseline ⊥ normalize. No lineBy. tooltip="multi" interpolates between samples.
Scatterplot — xAccessor, yAccessor, colorBy, sizeBy, sizeRange, symbolBy (categorical field → glyph shape: each mark becomes a d3-shape glyph; size still tracks sizeBy/pointRadius), symbolMap ({category → shape}; unmapped auto-assign — pass it for legend-matchable shapes), pointRadius (5), pointOpacity (0.8), marginalGraphics, styleRules (per-point; axis:"x"/"y" thresholds), regression (boolean | "linear"|"polynomial"|"loess" | RegressionConfig — sugar for trend overlay)
BubbleChart — Scatterplot + sizeBy (required), sizeRange ([5,40]), regression
ConnectedScatterplot — + orderAccessor, regression
QuadrantChart — Scatterplot + quadrants, xCenter, yCenter
MultiAxisLineChart — Dual Y-axis. series ([{yAccessor, label?, color?, format?, extent?}]). Falls back to multi-line if ≠ 2 series.
Heatmap — xAccessor, yAccessor, valueAccessor, colorScheme, showValues, cellBorderColor
ScatterplotMatrix — fields (numeric field names)
MinimapChart — Overview + detail with linked zoom. Wraps an XY chart.
CandlestickChart — xAccessor, highAccessor (req), lowAccessor (req), openAccessor+closeAccessor (optional → OHLC; high/low only → range). candlestickStyle ({upColor, downColor, wickColor, rangeColor, bodyWidth, wickWidth}). Honors mode.
Ordinal Charts (semiotic/ordinal)
BarChart — categoryAccessor, valueAccessor, orientation, colorBy, sort, barPadding (40), roundedTop, gradientFill ({stops: [{offset: 0–1, color?, opacity?}]}; tip→base), styleRules, regression
StackedBarChart — + stackBy (required), normalize, sort (false default — insertion order), styleRules
GroupedBarChart — + groupBy (required), barPadding (60), sort (false default), styleRules
SwarmPlot — colorBy, sizeBy, symbolBy (categorical field → glyph shape, like Scatterplot), symbolMap, pointRadius, pointOpacity
BoxPlot — + showOutliers, outlierRadius
Histogram — + bins (25), relative. Always horizontal.
ViolinPlot — + bins, curve, showIQR
RidgelinePlot — + bins, amplitude (1.5)
DotPlot — + sort ("auto"), dotRadius, showGrid (true default), regression
PieChart — categoryAccessor, valueAccessor, colorBy, startAngle
DonutChart — PieChart + innerRadius (60), centerContent
FunnelChart — stepAccessor, valueAccessor, categoryAccessor?, connectorOpacity, orientation
SwimlaneChart — categoryAccessor, subcategoryAccessor (req), valueAccessor, colorBy (defaults to subcategoryAccessor), orientation, roundedTop (pixel radius on outer ends of each lane; middles stay square; single-segment lanes round all four)
LikertChart — categoryAccessor, valueAccessor|levelAccessor+countAccessor, levels?, orientation, colorScheme
GaugeChart — value (req), min, max, thresholds, arcWidth, cornerRadius (rounded segment ends), sweep, fillZones, showNeedle, centerContent
All ordinal: colorBy, colorScheme, categoryFormat (string|ReactNode), showCategoryTicks (true).
styleRules (declarative threshold-aware styling — every chart family) — an ordered StyleRule[] where each { when, style } matching a mark contributes its style, merged in list order so the last applicable rule wins per property (CSS-cascade model). Wired on: ordinal BarChart/StackedBarChart/GroupedBarChart (bars/segments); XY LineChart/AreaChart/StackedAreaChart (per-series) + Scatterplot/BubbleChart/QuadrantChart/ConnectedScatterplot (per-point); network ForceDirectedGraph/SankeyDiagram/ChordDiagram (nodes); geo ChoroplethMap (features) + ProportionalSymbolMap/DistanceCartogram (symbols); physics GaltonBoardChart/UnitPileChart/CollisionSwarmChart/EventDropChart (particles). when = predicate (datum, ctx) => boolean, a declarative threshold ({ axis?: "x"|"y"|"value", field?, gt, gte, lt, lte, eq, ne, within:[min,max], outside, in:[...] }), or true/omitted (always). ctx channels by family: bars { value, category }; XY { value(=y), x, y } (use axis:"x"/"y" to target either axis regardless of accessor field name); network/geo/physics { value, category } (category = the colorBy/group). A rule's style.fill may be a color string or a HatchFill descriptor ({ type:"hatch", background?, stroke?, spacing?, angle?, lineWidth?, lineOpacity? }) resolving to a CanvasPattern on canvas and an SVG <pattern> in SSR — one declaration, both backends. Precedence: top-level primitives > per-mark style fn (pieceStyle/pointStyle/nodeStyle) > styleRules > base colorBy/color/theme. Works through renderChart/MCP for all wired families (use the declarative-threshold form, not predicates, when the config must serialize). HatchFill is also a band/x-band annotation fill and any scene style.fill. Caveats: line/area rules resolve per-series (sample datum, not per-vertex); network hierarchy charts (Tree/Treemap/CirclePack/Orbit) are NOT wired (their colorByDepth owns fill); MultiAxisLineChart/MinimapChart/CandlestickChart not wired. Exported from semiotic + each family entry + semiotic/utils: StyleRule, resolveStyleRules, matchesThreshold, makeRuleValueResolver, makeXYRuleContext, makeNodeRuleContext, composeStyleRules, HatchFill, isHatchFill. See /features/style-rules.
Network Charts (semiotic/network)
ForceDirectedGraph — nodes, edges, nodeIDAccessor, sourceAccessor, targetAccessor, colorBy, nodeSize, nodeSizeRange, nodeStroke/nodeStrokeWidth (node-only outline), edgeWidth, edgeColor/edgeOpacity (edge-only stroke), iterations (300), forceStrength (0.1 — link-attraction multiplier), layoutExecution ("auto" default | "worker" | "sync" — auto runs big layouts in a Web Worker by estimated cost, sync fallback everywhere), layoutLoadingContent (ReactNode while worker layout pends; false suppresses), onLayoutStateChange ("pending"|"ready"|"error"), showLabels, nodeLabel, styleRules (style groups of nodes — rules see the raw node; ctx.category = colorBy group). Node vs edge stroking: the generic stroke/strokeWidth/opacity style all marks uniformly; to stroke nodes and edges separately use node-only nodeStroke/nodeStrokeWidth (e.g. nodeStroke="none" drops the node ring) and edge-only edgeColor/edgeWidth/edgeOpacity. Precedence per property: specific > generic > built-in default.
SankeyDiagram — edges, nodes, valueAccessor, nodeIdAccessor, colorBy, edgeColorBy, orientation, nodeAlign, nodeWidth, nodePaddingRatio, showLabels
ProcessSankey — temporal sankey with real time x-axis. nodes, edges (each with startTime/endTime; zero-duration OK), domain (req [t0, t1]), axisTicks?, xExtentAccessor (optional [start, end] lifetime per node), nodeLabel (visible lane-label accessor), colorBy/colorScheme/showLegend/legendPosition, styleRules (band fills; fill may be a color or HatchFill — canvas + SSR), selection/linkedHover, selectionDatum ("raw" default | "scene" — what selection predicates see), pairing ("value"|"temporal", default temporal), packing ("off"|"reuse"), laneOrder ("crossing-min"|"inside-out"|"crossing-min+inside-out"|"insertion"), maxValueScale (pixels-per-unit cap), lanePlacement ("stack"|"hug" — hug/binding maxValueScale also post-scale geometry-refines order), lifetimeMode ("full"|"half", default half), ribbonLane ("source"|"target"|"both"), ribbonMinRun (0 exact | pixel minimum | "auto" — source-only feeder runway + source-band handoff; not a general min-edge length), showLaneRails, showLabels (true|false|"auto" density budget; auto sheds keep deferred text for selection reveal), labelPriorityAccessor / maxLabels (auto priority + hard cap), showQualityReadout (crossings/pixel + non-fatal validation warnings), layoutExecution ("auto"|"worker"|"sync" — auto offloads packing/ordering to a module worker by cost; SSR always sync), layoutWorkerThreshold, layoutLoadingContent, onLayoutStateChange ("pending"|"ready"|"error"), showParticles + particleStyle, timeFormat/valueFormat, push API via ref (getScales → time + centerlines; getCustomLayout → { layout, bands, ribbons, warnings, … } with layout.layoutQuality). Pure quality helpers from semiotic/network: diagnoseProcessSankeyLayout / diagnoseProcessSankeyProps / explainProcessSankeyLayout (also fed into diagnoseConfig("ProcessSankey", props)). Validation policy: static/MCP hard-fail duplicate ids; React push warns on duplicates and strips non-finite systemIn/Out times. Static-graph cycles OK as long as edges move forward in time. Use ProcessSankey for time-stamped events; SankeyDiagram for static 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
Geo Charts (semiotic/geo)
Import from semiotic/geo only — avoids d3-geo in non-geo bundles.
ChoroplethMap — areas (GeoJSON Feature[] or "world-110m"), valueAccessor, colorScheme, projection ("equalEarth"), graticule, tooltip, showLegend, styleRules (flag features — rules see the feature with properties flattened; fill may be a HatchFill)
ProportionalSymbolMap — points, xAccessor ("lon"), yAccessor ("lat"), sizeBy, sizeRange, colorBy, areas?
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}).
Physics Charts (semiotic/physics)
Import from semiotic/physics only — not re-exported from the root semiotic entry (keeps the physics kernel out of default dashboards).
Process/arrival/distribution charts backed by StreamPhysicsFrame. The settled projection is the chart; motion is explanatory context. Use when the movement has data semantics, not as decoration.
GaltonBoardChart — data, valueAccessor ("value"), bins (21), mode ("sample"|"mechanical"), pegRows, mechanicalCount, branchProbability, ballRadius, colorBy, styleRules (per-particle; ctx.category = colorBy group), seed, size/width/height, paused, frameProps. Renders a deterministic Galton/Plinko-style distribution that settles into bins; mechanical mode can generate a seeded no-data demonstration.
EventDropChart — data, timeAccessor ("time"), arrivalAccessor ("arrivalTime"), windows ({size}), watermark ({delay}|fn), timeScale, ballRadius, colorBy, seed, size, paused, frameProps. Use for event-time arrival, lateness, and watermark-window stories.
UnitPileChart (was PhysicsPileChart; alias kept) — data, categoryAccessor ("category"), valueAccessor ("value"), mode ("sample"|"mechanical"), mechanicalCount, mechanicalCategories, unitValue (1), ballRadius, colorBy, seed, showProjection, size, paused, frameProps. Unitizes values into repeated bodies that settle into category piles; mechanical mode can generate a seeded no-data capacity sketch; the default projection overlay keeps exact totals readable.
CollisionSwarmChart — data, xAccessor ("x"), optional groupAccessor, radiusAccessor, pointRadius, xExtent, collisionIterations, settle, showProjection, colorBy, seed, size, paused, frameProps. Uses springs plus collisions to separate overlapping dots while preserving the quantitative axis and optional group lanes.
PacketFlowChart (was PhysicalFlowChart; alias kept) — nodes, links/edges/data, nodeIdAccessor ("id"), nodeXAccessor ("x"), nodeYAccessor ("y"), sourceAccessor ("source"), targetAccessor ("target"), throughputAccessor ("value"), pathAccessor ("path"), coordinateMode ("auto"|"normalized"|"pixels"), particleRate, maxParticles, particleRadius, flowSpeed, pathConstraint ("path"|"none"), reducedMotion, showStaticFlow, showNodeLabels, showSensors, paused, seed, size. Experimental physics-backed flow chart where packets move along authored node coordinates or link paths while a static throughput layer keeps route quantities readable.
ProcessFlowChart — multi-body workflow lane. data, stages (required: [{id, label?, force?, damping?, capacity?, pressure?, portal?, absorb?, share?}]), stageAccessor ("stage"), idAccessor, groupBy (optional feature key; completion when all members hit an absorb stage), groupLabelAccessor, workAccessor, radiusAccessor, ballRadius (6), colorBy, groupCompletion ("allAbsorbed"|"none"), groupAnchorAlong (0.55), showProjection (true), showChrome (true — processChrome kit), liveCapacity (true — FIFO queues at unitsPerSecond), onCapacityChange (queue depth / processed), bodyLimit (soft stream budget + oldest eviction), bodyMark ("circle"|"halo"|"faceted"|"pill"|"diamond"|"square" or per-row datum.__physicsMark), selection, settle, seed, size, paused, frameProps. Settled projection is stage occupancy + capacity badges; use for review queues / triage / merge pipelines. Prefer GauntletChart for one compound plan with timed gate effects.
Stage geography kit (semiotic/physics + semiotic/recipes) — physicsStageGeography({size, flow ("down"|"right"), destinations (count | [{id,label}]), padding, chargeExtent, destinationExtent, projectionExtent, channelRatio}) → {charge, apparatus, destinations: [{id,label,order,centerX,centerY,…}], projection}. The shared charge → apparatus → destinations vocabulary every physics chart re-invented locally (Gauntlet startX/socketX/graveyardX, Crucible chamber/mouth/outlets, Galton bins and Pile tubes writing the same lane formula). Companions: physicsStageColliders (floor + one divider per interior boundary, so bodies land in the bin the data says), physicsChargePoint (spread a burst across the entry zone instead of co-locating it), physicsDestination(geo, id), describePhysicsStageGeography (one-sentence reading protocol for a11y/agent grounding). Authoring vocabulary for a new chart or PhysicsCustomChart layout — shipped charts keep their own layouts; a test pins the builder to their lane math.
Terminal state (event-tape charts) — a tape-driven physics chart's end state must be computable from authored inputs with no simulation, because that pure result is what reduced motion, SSR, snapshot export, and describeChart receive. CrucibleChart: compileCruciblePlan(...) → plan.initialState/plan.terminalState/plan.terminalSpawns. ChainReactionChart: initialRuntime(machine, mode, currentTime, true). GauntletChart: resolveGauntletTerminalStates({projects, events, layout, positiveProperties, negativeProperties, viability?, outcome?}) (also resolveGauntletTerminalState for one project) — folds the authored tape, mirroring the live tick's effect order. Caveat: with Gauntlet's crashDetection armed (default), physics can still override the outcome to bad_design_crash, which no pure fold can predict; the pure result is "what the plan earns on paper". Pass crashDetection={false} for a fully authored reading. Design test for a new physics metaphor: can you state its ledger in one sentence (else it's an animation), does its terminal state exist without simulating (else it's a movie), would a naive reader guess the reading protocol from the apparatus (else the name is wrong)?
Physics family contracts (every physics HOC) — seed (deterministic; an explicit frameProps.config.kernel.seed still wins), rerunMS (settle → delay → deterministic replay; null/omitted = single run), showProjection, paused, and selection + linkedHover. selection accepts either a { name } SelectionConfig — joining the shared selection store so physics bodies cross-highlight inside LinkedCharts like every other family — or a resolved { isActive, predicate } PhysicsBodySelection body predicate as the escape hatch. The store's datum predicate is lifted over body.datum, so chrome bodies (walls, pegs, tubes) never match a data selection. Under prefers-reduced-motion the frame settles to the true end state in one pass: paced arrivals (initialSpawnPacing, per-datum spawnAt) are all admitted and elapsed-time event tapes run to completion, so the settled projection reports the outcome rather than the authored start.
Physics controllers (createCapacityQueueController, createPortalController, composePhysicsControllers) — process plugins via controllers. Capacity getSnapshot() → queueDepth/processedCount; emit physics-capacity-processed. processChrome (semiotic/physics / semiotic/recipes) — stage bays, capacity badges, feature sockets (theme: --semiotic-process-*). PhysicsCustomChart: layout() may return regionEffects, controllers, bodyForces; layoutConfig hot path without re-enqueue. Guide: /features/physics-process-guide. Contracts: PhysicsContracts.test.tsx.
GauntletChart — compound project core + tethered positive/negative property bodies + timed gate events. positiveProperties/negativeProperties (req), gates, events, showChrome (true), showProjection (true — viability/outcome strip), showTethers (true), onStateChange, frameProps. Bodies clamp inside walls (clampGauntletPoint). Not for multi-item factory floors (use ProcessFlowChart).
CrucibleChart — bounded peer components undergo authored phases and events, form declared products, and settle into reason-labelled outlets with source lineage. The ledger/projection is authoritative; motion never infers classification, timing, membership, loss, or routing. buildCrucibleProductEvents({productId, form, contributions?, complete}) is pure authoring sugar for combine → contribute* → complete-product; every source/relation id, event position, reason, and outlet remains caller-supplied. The ref handle's replay() atomically restarts the deterministic tape even mid-run (reset() restores-and-pauses; rerunMS repeats after settlement). playbackRate changes presentation only. No push/live-event API.
ChainReactionChart — dependency chain reaction. data, taskIDAccessor, labelAccessor, laneAccessor, dependencyAccessor (all required — edges are never inferred), plus optional startAccessor/endAccessor/progressAccessor/statusAccessor/completionTimeAccessor/blockerAccessor/milestoneAccessor. Tasks place by workstream lane × dependency depth; a completed task releases one delivery ball per outgoing edge and a downstream task arms only once every prerequisite ball arrives, so a blocked task's reach is the reading. mode ("snapshot" derives the settled state at currentTime with no simulation | "replay" animates deliveries over that same derived state | "mechanical"), insight ("blocker-amplification" reports unfinished downstream tasks + affected lanes), controls (play/pause/step/reset/settle), selectedTaskIDs/onSelectionChange, reducedMotion: "settle", seed. Ref handle: play/pause/step/reset/settle, previewResolve(taskID)/clearPreview (ask "what would resolving this blocker unlock?" without editing data), completeTask/blockTask/unblockTask, getAmplification, getMachineState. Task completion is always an explicit data event — the simulation delivers prerequisites, it never decides done. Documented serverChartConfigs exclusion: the settled reading is an authored overlay over zero bodies, so the HOC SSR path is the supported static snapshot.
Pop (body-removal burst) — every physics HOC ref is a PhysicsFrameHandle (extends the shared push handle) exposing popBodies(ids, options?) (StreamPhysicsPopOptions = { color?, durationMs?, radius? }): removes the bodies and plays a burst — expanding ring + inner glow + radial sparks fading over durationMs (drawPopAnimations) — returning the removed ids. It reads as a departure, the physics/exit-emphasis counterpart to realtime pulse's data-arrival glow (the same transient-emphasis metaphor on opposite ends of a datum's life). GauntletChart also fires it internally on gate-driven property removal; /examples/nimby (civic-value balloons) and /examples/merge-pressure (merge-risk traits) drive it that way.
Value Charts (semiotic/value)
Single-focal-value displays — when one number is the answer, a chart is the wrong abstraction. Plain React (no Stream Frame); SSR-clean; ~7KB gz. Ships no chart-family dependency — embed your own Semiotic chart via two slots picked by aspect ratio.
BigNumber — value (required), label, caption, format ("number"|"currency"|"percent"|"compact"|"duration"|fn), locale, currency, precision, prefix/suffix/unit, comparison ({value, label?, format?, direction?}), target ({value, label?, format?, direction?}), delta (explicit override), deltaFormat, showDeltaPercent (true), direction ("higher-is-better" default | "lower-is-better" | "neutral"), sentiment ("auto" default | "positive" | "negative" | "neutral"), thresholds ([{at, level: "success"|"warning"|"danger"|"info"|"neutral", color?, label?}] — resolved by highest at ≤ value, painted via --semiotic-{level}), windowSize (60 — caps push buffer surfaced via getData() / slotCtx.pushBuffer), mode ("tile" default | "presentation" | "inline" | "thumbnail"), align, padding, emphasis, color/background/borderColor/borderRadius, animate (boolean | {duration?, easing?, intro?} — tweens between value changes), stalenessThreshold (ms; dims after no-push interval), staleLabel, slot overrides (headerSlot/valueSlot/deltaSlot/footerSlot/trendSlot/chartSlot — ReactNode or (ctx) => ReactNode), chartSize (px reserved for chartSlot; defaults to inner card height).
Two chart slot positions, picked by chart aspect:
trendSlot— wide / rectangular charts beneath the value (LineChart, AreaChart, DifferenceChart inmode="sparkline"). Renders at full card width.chartSlot— square charts beside the value (DonutChart, PieChart, Scatterplot, Treemap, CirclePack). Splits the card horizontally: text-on-left, chart-on-right.- Pair both: square chart anchors top-right, wide trend stretches across the bottom.
- Slot context
(ctx) => ReactNodeexposes{ value, formattedValue, level, color, delta, deltaFormatted, deltaPercent, sentiment, isStale, pushBuffer }— embedded charts readctx.colorto theme-link to the resolved threshold.
Push API via forwardRef: ref.current.push(value | {value, time?, comparison?}), pushMany, clear, getValue(), getData(). Stable across renders (refs back the imperative handle).
ARIA: auto sentence-form label combining {label}: {formatted} {unit}, {up|down} {delta} ({percent}) from {comparison.label}, {target%} of {target.label}[, stale]. Override via description; supplement via summary (sr-only).
Semantic classes: semiotic-bignumber root + --mode-{...} / --level-{...} / --sentiment-{...} / --stale modifiers; __text-region, __value, __delta, __delta-row--{up|down|flat}, __arrow--{up|down|flat}, __trend (wide slot wrapper), __chart (square slot wrapper), etc.
Helpers exported: buildFormatter, formatSignedDelta, formatDeltaPercent, formatDuration, resolveThreshold, colorForLevel, buildSparklinePath (for custom-slot rendering).
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; stacked via categoryAccessor + colors — bars sum by category within each bin; mirrored/diverging via direction="down" flipping the value domain — pair two halves with a shared timeExtent/valueExtent for an up/down detail view, and overlay extra instances on the same extent for layered envelopes), TemporalHistogram (static sibling — same props minus windowSize/windowMode), 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 — bounded-ingest, preserves category order + transitions
ref.current.remove("p1" | ["p1","p2"]) // requires ID accessor
ref.current.update("p1", d => ({ ...d, y: 99 })) // requires ID accessor
ref.current.clear()
ref.current.getData()
ref.current.getScales() // {o, r, projection} (ordinal) | {x, y} (XY) — null if unmounted
ref.current.getCustomLayout() // custom charts: the most recent layout(ctx) result (readback — don't re-run the layout host-side); null before first layout / on built-ins
<Scatterplot ref={ref} xAccessor="x" yAccessor="y" pointIdAccessor="id" />
ID accessor: pointIdAccessor (XY/realtime), dataIdAccessor (ordinal), nodeIDAccessor/edgeIdAccessor (network). replace() is ordinal-only — used by aggregator HOCs like LikertChart. Network HOC refs operate on nodes; for edges use StreamNetworkFrameHandle directly: removeNode(id), removeEdge(sourceId, targetId) or removeEdge(edgeId), updateNode(id, updater), updateEdge(sourceId, targetId, updater).
Controlled→push bridge: useSyncedPushData(ref, rows, { id, resetKey }) (from semiotic / semiotic/realtime) reconciles a controlled React array into the push buffer — diffs by id, issues the minimal push/update/remove, and clears + rebuilds on resetKey change. Reach for it instead of hand-rolling the mirror when rows live in React state; pass rows to the hook, not data. Pairs with useStreamStatus (live/stale badge). Pure core syncPushBuffer is exported for testing.
Not supported: Tree, Treemap, CirclePack, Orbit, ChoroplethMap, FlowMap, ScatterplotMatrix.
Custom Charts (escape hatch)
When the catalog doesn't fit, four HOCs take a layout function emitting scene primitives. Frame still owns hit testing, transitions, decay, theme, SSR.
XYCustomChart(semiotic/xy) — waffle, calendar heatmap, custom point/line/areaOrdinalCustomChart(semiotic/ordinal) — marimekko, parallel coords, bullet, fan, slopeNetworkCustomChart(semiotic/network) — flextree, dagre, custom force/radial, packed-cluster beeswarm matrixGeoCustomChart(semiotic/geo) — isometric landmark boards, custom geographic tessellations
Layout signature differs by family:
- XY/Ordinal:
layout: (ctx) => { nodes, overlays? }.ctx:data,scales({x,y} XY | {o,r,projection} ordinal),dimensions(plot rect — center-anchored for radial ordinal, top-left otherwise),theme,resolveColor(key),config. - Network:
layout: (ctx) => { sceneNodes?, sceneEdges?, labels?, overlays?, htmlMarks? }.ctx:nodes,edges,dimensions,theme,resolveColor(key),config,selection(shared-selection predicate{ isActive, predicate(datum) }fromLinkedCharts,nullwhen unwired — dim/highlight by it). Run external positioners (d3-flextree,dagre) then emit network scene primitives (circle/rect/arc/symbol/glyph nodes; line/bezier/curved edges). Thesymbolnode is the per-datum shape channel — ad3-shapeglyph (circle/square/triangle/diamond/star/cross/wye/chevron, or a custompath) sized bysize(area), rendered on canvas + SVG/SSR and hit-tested + keyboard-navigated as a unit. Thesymbolmark is cross-pipeline: XY and ordinal custom layouts emit it too ({type:"symbol", x, y, size, symbolType}— notex/y, vs the network variant'scx/cy), and Scatterplot/SwarmPlot expose it as thesymbolByencoding. One sharedsymbolPathimplementation backs canvas/SVG/hit-test across all three families. - The
glyphnode — composite pictograms (ALL FOUR families incl. geo): wheresymbolis one path,glyphstamps a multi-part vector pictogram — aGlyphDef({viewBox?, anchor?, parts: [{d, fill?, stroke?, strokeWidth?, opacity?}]}) whose parts declare role paints ("color"/"accent"/literal) resolved per node (color/accentprops), so one definition recolors per category like a pictogram plate reused in many inks.{type:"glyph", x, y, size, glyph, color?, accent?, fraction?, fractionStart?, fractionDirection?, ghostColor?, rotation?, style, datum, pointId?}(network variant usescx/cy+id/label).size= rendered height px (width follows viewBox aspect);anchor: [0.5, 1]stands a sign's feet on a baseline/terrain. Partial fills:fraction/fractionStartclip a[start, end]window (horizontal or bottom-up vertical) with an optional full-extentghostColorsilhouette — the ISOTYPE partial-symbol convention, fed directly byunitize. Full pipeline citizen: canvas + SVG/SSR, hit-test + keyboard nav over the drawn bounds,pointId/idannotation anchoring, and enter/move/exit transition identity (whichsymbollacks). Datum-less glyph nodes (datum: null) paint but don't hit-test/navigate — use onehitTargetper logical mark under a multi-sign tally.<Glyph def size color …/>(fromsemiotic/recipes) renders the same definition as React SVG for overlays/legends/decoration;glyphPlacement/glyphExtentexpose its geometry for layout math. - Geo:
layout: (ctx) => { nodes?, overlays? }.ctx:areas,points,lines, fittedGeoScales,dimensions,theme,resolveColor(key),config,selection. Emitgeoarea/point/line/glyphnodes; use overlays for labels and sprites.htmlMarks(network only):NetworkHtmlMark[]={ id, x, y, width, height, content: ReactNode }, positioned in the same plot space assceneNodesand rendered into one real-DOM layer the framework places above the canvas and SVGoverlays(stack: canvas →overlays→htmlMarks). Reach for it over an SVG<foreignObject>when a mark is text-heavy/rich and dims or animates on hover — a real<div>compositesopacity/transform/visibilitychanges instead of re-rasterizing text (theforeignObjectstall on large graphs). Framework owns the margin (and future zoom/pan) transform so marks stay pixel-aligned; each mark is its own element, keyed byid(position-only re-runs reposition without remounting).pointer-events: noneby default — keep a transparent hit-rectsceneNodeper mark so canvas hit-testing/onObservationstays authoritative. Markcontentcan readuseCustomLayoutSelection()to dim on shared selection without a relayout. Additive: omit it and no extra DOM renders. Class hooks:.semiotic-network-html-marks(layer) /.semiotic-network-html-mark(each).
Custom-chart authoring kit (semiotic/recipes; hitTarget* also from semiotic/xy/ordinal/network). The shape every hand-built custom chart converges on — draw real marks in overlays, emit a transparent scene node per mark for interaction — is first-class:
hitTargetPoint/hitTargetRect(XY/ordinal) +networkHitTarget(circle or rect) +geoHitTarget(geo — same transparentPointSceneNode; project lon/lat viactx.scales.projectedPointfirst): a zero-opacity, fully-transparent, hit-tested node from{x, y, (r|width,height), datum, id}. Theidbecomes the node'spointId/id(annotation anchor + nav-tree leaf) and its transition key. This is how a custom chart inherits accessibility (keyboard nav, focus ring, data table), annotation anchoring, AI/onObservation+ shared selection, and chart-mode transitions for free — replaces thergba(0,0,0,0)+opacity:0+pointId+_transitionKeyboilerplate. The visible glyph lives inoverlays; the focus ring still draws on the invisible target. A keyboard-focused geoarea outlines its polygon (a shape focus ring) rather than a centroid dot.- Radial coordinate kit:
polarToXY/xyToAngle(0 = up, clockwise),angleScale/radiusScale,ringArcPath(annular-sector / wedge / full-ring path),TAU— angle ⟂ radius for two-continuous-channel radial charts (the radial analogue of the decoration kit). - Edge-router kit (custom network edges):
curvedEdgePath(S-curve + near-level side-bow),orthogonalEdgePath,boxEdgeAnchors(box exit/entry by direction),fanOutBend(fan parallel edges apart). Plus cubic-Bézier evaluation —cubicPoint/cubicTangent(sample a point/tangent along aCubicCurveto seat a mark on the curve — a node mid-edge, an arrowhead at the end) andcubicPath(serialize to SVG). - 2D vector kit:
addPoints/subtractPoints/scalePoint/pointMagnitude/normalizePoint— the point math any hand-built radial or network layout re-derives (an edge offset normal to its tangent, a spoke, a leader line). Operates on the sharedPoint, composes with the radial + edge kits. - Interval/timeline:
packIntervals(greedy Gantt sub-track packer),activeCountOverDomain(concurrency step series). runs/runLengthEncode: collapse a per-step categorical/boolean series into drawable runs (condition strips, status timelines, calendar ribbons).- Cyclical math (day-of-year, hour, compass bearing):
wrapValue,shortestArcDelta,cyclicRangeContains,selectCyclicRange. axisFixedForcePositions(+ sharedrectCollidepositioner): pin one axis from a data field, relax the other with edge attraction + an anchor spring + rectangular (label-box) collision — the "time is structural, the graph settles the cross-axis" family that hierarchical recipes don't cover.axisFixedForceLayoutwraps it as a readyNetworkCustomLayout.- Decoration:
linearAxis(tick axis + gridlines from any scale — the bespoke-scale escape hatchshowAxescan't cover),legendSwatches(portable SVG legend foroverlays— fill/line/shape/hatch swatches; sibling tolegendGroupsFromwhich feedsframeProps.legend),hatchFill({def, fill}SVG<pattern>for percentile/uncertainty bands — the SVG analogue ofcreateHatchPattern). unwrapDatum(semiotic/recipes+semiotic/utils): collapse the wrapped-vs-raw datum split — always the raw user object (handles both.datawrappers and.datumnesting). The unwrap path foronObservationhandlers ANDframeProps.tooltipContentrenderers: call it once on the incoming value; never pre-unwrap the argument (unwrapDatum(x?.data ?? x)double-unwraps).
Word Trails (wordTrailsLayout) is the quantitatively anchored word-cloud recipe: column = category, segment = ordered vertical position, weight = font size, with overlap-free stable placement. wordColor/wordOpacity receive WordTrailsWordInfo: canonical word/column/weight/segment plus the exact source datum, dataIndex, columnIndex, and resolvedColumnColor, so derived encodings do not need brittle compound-key lookup maps. Spread wordTrailsProgressiveReveal({currentSegment, segmentDomain, oldestOpacity?, currentOpacity?, futureOpacity?, combineWeightOpacity?}) into layoutConfig for future hiding + linearly faded history without reflow; zero-opacity rows reserve geometry but emit no glyph or hit target. Analysis-derived color/distinctiveness remains a source-data field, not something the layout infers.
semiotic/recipes ships pure layout functions (waffleLayout, calendarLayout, marimekkoLayout, bulletLayout, parallelCoordinatesLayout, intervalLanesLayout, flextreeLayout, dagreLayout, lineageDagLayout, netEnsembleLayout, axisFixedForceLayout, packedClusterMatrix, isometricLandmarkLayout, forceLayout, arcLayout, adjacencyMatrix, circularLayout). Network-analysis kit (semiotic/recipes, pure graph algorithms): buildAdjacency, bfsDistances, shortestPath, egoNetwork, degree/betweenness(Brandes)/closeness/clustering (+ normalizeScores), analyzeNetEnsemble (the headless net census — see netEnsembleLayout below), and proximityProblem — the "spatial problem" layout diagnostic that flags nodes drawn closer than their graph distance warrants. Pair with network charts to size by centrality, highlight an ego network on hover, trace a shortest path, or diagnose a misleading layout. forceLayout(nodes, edges, {seed}) is a seeded, deterministic positioner returning normalized {id:{x,y}} for NetworkCustomChart (same seed ⇒ same layout; re-seed for a "re-run the layout" interaction); forceLayoutAsync(nodes, edges, {execution?, workerThreshold?, signal?}) is its Promise sibling that runs large layouts in a short-lived module Web Worker (identical deterministic output; graceful sync fallback), and useForceLayout(nodes, edges, options) → {positions, status, error} is the React wrapper — SSR and first hydration stay synchronous for markup parity, client graph changes go async while previous positions stay visible, and settled positions are memoized by node/edge array identity + options so remounting the same module-constant graph is "ready" immediately (no loading flash). arcLayout/adjacencyMatrix/circularLayout (+ orderByGroupDegree, arcPath) are the classic physics-free network forms. allocateCells is the largest-remainder grid allocator behind waffleLayout (turn {key, weight}[] + a cell count into integer cells with no rounding drift; minPerCategory keeps small categories visible) — reusable for any feature-mix / proportional waffle. unitize(value, {unit, maxUnits?, minFraction?}) / unitizeRange(value, rangeValue, opts) is the counting sibling: the pictogram/tally allocator (value → repeated unit signs with a fractional final sign — ISOTYPE: symbols repeat, they never grow). Returns {units: [{index, fraction, start, end, value}], total, shown, overflow} — maxUnits caps with an overflow flag, minFraction drops trailing slivers while total vs shown keeps the ledger honest; unitizeRange extends the tally to a projected/scenario endpoint (rangeUnits drawn hatched), sharing a mid-sign boundary exactly via startFraction. Feeds glyph-node fractions directly (unit charts, sign stacks, arrow bundles; allocateCells divides fixed cells, unitize counts). Tokenized reasoning helpers: generateTokens(input, tokenEncoding) wraps unitize plus actual, fixed-denominator, quantile, posterior-sample/sample, and seeded random-sample strategies with explicit tokenType (dot/icon/glyph), tokenSemantics (observed-unit, unitized-measure, risk-case, possible-outcome, etc.), and countStrategy; { value, rangeValue } yields rangeTokens for scenario/projection tallies. layoutTokenGrid places the resulting tokens for icon/glyph arrays; normalizeTokenEncoding keeps legacy token/unit aliases working while canonical configs use icon/unitValue; diagnoseTokenEncoding, suggestTokenEncoding, and tokenTaskIntentToCapabilityIntents expose IDID-style warnings, task-aware defaults, and a bridge to suggestCharts intents. Built-in token glyph names include person, server, chip, bolt, and bus. intervalLanesLayout (ordinal) packs concurrent {start,end,lane} records into stacked Gantt sub-tracks per lane with period bands + lane labels + a time axis — packing runs in rendered-pixel space and honors minBarWidth (2) so zero/short-duration events stay visible without overlapping same-track neighbors; axisFixedForceLayout (network) pins one axis from a field and settles the other (rect-aware collision). BYO heavy deps (d3-flextree, dagre) in user code. packedClusterMatrix (network) bins records into a column×row matrix of densely-packed beeswarm clusters (deterministic self-contained packing, geometry cached) and emits multi-channel glyphs — hue (colorAccessor/colorMap), size (sizeAccessor, area), shade (shadeAccessor, CIELAB lightness), plus EITHER shape-encoding (symbolAccessor/symbolMap — the base mark becomes that shape) OR the composite-glyph model (iconAccessor/iconMap — base is a filled circle, only mapped values get a stroked inner icon). rowMode:"banded" (default) gives aligned global orbit-bands (row labels align, one enclosure spans the columns per band, columns vary in height) vs "stacked" (per-column cell heights ∝ count). callouts:[{field,value,label}] draws leader lines to named marks. cellSizing:"proportional" makes area ∝ count. Recipe decoration kit (exported from semiotic/recipes, for any custom-layout's overlays): roundedEnclosure/boundsOf (group/band borders), bandLabel (overflow-aware axis/band labels), markCallout (leader-line callout to a mark), readField (node.data-wrapper reader), groupBy, dimFor (the highlight/dim opacity rule — {predicate?, highlight?, baseOpacity?, dimOpacity?, brighten?}; matchesHighlight is its {field,value}[] matcher), signatureKey/LayoutCache (content-signature geometry cache so re-styling never re-runs an expensive layout — key by content, never by ctx.nodes identity), legendGroupsFrom ({colorMap|keys, symbolMap?, sizeStops?} → LegendGroup[] for frameProps.legend), shade/makeShade, symbolPathString/symbolRadius/symbolExtent/SYMBOL_SEQUENCE, and the small numeric/color one-liners every layout re-declares — clamp, mean, withAlpha, nonNegativeFinite (hex→rgba() so a hover-dim can ride a recipe's resolveColor callback). (bandLabel/dimFor are adopted by marimekko/bullet/parallelCoordinates/packedClusterMatrix.) lineageDagLayout renders a pre-positioned layered lineage/DAG (reads logical layer/row coords, no re-layout) with composite node glyphs (one hit-rect per node + icon/label/store-chip decoration in overlays), level-of-detail collapse (full→compact→icon→dot), distinct dashed back-edges, and host-driven reach-dimming (layoutConfig.reachableIds) + selection (layoutConfig.selectedId / shared ctx.selection). netEnsembleLayout (network) is the complement — for ensembles of disconnected/trivially-connected DAGs (a "bag of little graphs" force layout scatters and dagre/flextree can't place). It splits into weakly-connected components, tests each for directedness (borrowing the mathematical net/directed-set idea: for a weakly-connected DAG, a single sink ⟹ everything converges to one limit; ≥2 sinks ⟹ it branches), fingerprints components with Weisfeiler–Leman refinement so order-isomorphic motifs group together, lays each out converging toward its sink(s) at the bottom, and arranges the ensemble as small multiples in motif bands (collapsing to one census glyph per component when cells get tiny). Config: colorMode ("directedness"|"motif"|"category"), groupByMotif, sort, fingerprintRounds, minCellForFull. analyzeNetEnsemble(nodes, edges) is the pure headless census (per-component directed/sink/source counts + motif classes) with no rendering. Demo + lay explanation at /recipes/net-ensemble.
tokenLayer({input, encoding, options}) is the high-level tokenized-rendering helper for custom layouts: it runs generateTokens, applies row/column/grid/waffle/dotplot/bar-segment/quantile-strip placement (or positionToken for scale/map-driven placement), and returns ordinary dot/symbol/glyph scene nodes with pointId/transition identity. Use it for ISOTYPE, icon arrays, risk grids, quantile dotplots, strips, and hybrid token overlays; pass includeRange to render rangeTokens; drop down to generateTokens when a bespoke layout only needs records.
Decoration (labels/axes/legends): the recipe owns it. Recipes emit own decoration via overlays return field (ReactNode painted on top). Built-in axes via showAxes on the HOC work for layouts respecting the standard scale. Recipe convention: showXxx boolean toggles, xxxFormat callbacks. Shipped recipes' toggles: marimekko showCategoryLabels, bullet showLabels+showTicks, parallelCoordinates showAxes, flextree/dagre showLabels, waffle/calendar none.
Interaction (hover/brush/selection): the parent owns it. Recipes are pure — they take predicate props (e.g. parallelCoordinatesLayout's highlightFn?) and the parent manages state via onObservation ({type: "hover" | "hover-end" | ...}), feeding a derived predicate back into layoutConfig. Matching rows render at full opacity; non-matching dim; highlighted z-order on top.
Notes:
- Coords are plot-relative (frame translates by
margin). Readctx.dimensions.plot. Radial ordinal:plot.x = -width/2,plot.y = -height/2(center-translated). - Layouts needing axis domains: pass
xExtent/yExtent(XY) oroExtent/rExtent(ordinal) — those flow through scale construction before the layout runs. - Streaming layouts: ingest via ref (
push/pushMany); layout re-runs on each ingest. Overlays update on data-change paths, NOT per-frame. - On
NetworkCustomChart, alayoutConfigchange re-runs the layout (buildScene) WITHOUT re-ingesting the node/edge topology — so drive interaction state, styling, or animation progress throughlayoutConfigfor cheap per-frame updates; swappingnodes/edges(or the chartwidth/height) is the heavier path that re-ingests. Custom overlays are read straight from the store at render time (the frame's repaint re-reads them), so a recipe returning fresh JSX every layout call needs no per-framesetState. - Custom layouts own their colors — always prefer
ctx.resolveColor(key)over hardcoded literals.CategoryColorProviderintegration is XY-only; for cross-chart sync on network/ordinal customLayouts, pass matchingcolorSchemeto each. - All four custom HOCs accept
selection/linkedHover/chartIdlike the built-in HOCs: hover/click emit into the shared selection store, and the resolved predicate arrives asctx.selectionso the layout can dim/highlight by a cross-chart selection. Host-owned per-render dimming (e.g. a graph-reachability set) is orthogonal — pass it throughlayoutConfigand readctx.config. - Selection restyle without a relayout (the cheap hover path): by default a
ctx.selectionchange re-runs the layout (rebuilds sceneNodes + repaints + rebuilds the quadtree). To restyle on hover/selection without re-positioning, opt in two ways: (1) return arestyle(node, selection)(network alsorestyleEdge) from the layout result — its presence makes a selection change re-apply styles to the existing scene off each mark's base style and just repaint (no relayout, no quadtree rebuild); compute geometry once in the layout body, express dimming inrestyle. (2) For the Reactoverlays, calluseCustomLayoutSelection()(fromsemiotic/semiotic/recipesor the family entry) →{ isActive, predicate }; the frame swaps only the context value on selection change, so subscribing overlay components re-render while the canvas/quadtree stay untouched. Express selection/highlight through the selection store (notlayoutConfig) to ride this path. - Annotations: all four custom HOCs accept
annotations. XY/ordinal/network custom marks can anchor bypointId; Geo annotations use geographic coordinates, while sprite- or tile-specific callouts can be emitted directly in the layout overlay. - Tooltips: emit datum keys matching user-visible accessor names. Avoid underscored synthetic keys (default tooltip filters those out).
Coordinated Views
LinkedCharts — selections. CategoryColorProvider — colors|categories + colorScheme.
Chart props: selection, linkedHover, linkedBrush. Hooks: useSelection, useLinkedHover, useBrushSelection. Works for NetworkCustomChart too — the resolved selection predicate is threaded into the custom layout as ctx.selection.
useSelectionActions(name) — write-only access (selectPoints/clear) that does NOT subscribe to selection state, so a container can push a selection (e.g. from a hover handler) without re-rendering; only the leaf consumers reading the selection re-render. The provider-at-top / consumers-at-leaves pattern for interaction-heavy coordinated views.
Shared categories inside LinkedCharts → wrap in CategoryColorProvider. Gives identical per-category colors AND makes LinkedCharts render one unified legend (suppressing individual chart legends). Without it, mismatched colors and duplicate legends.
Linked crosshair: linkedHover={{ name: "sync", mode: "x-position", xField: "time" }}. Click locks crosshair (dashed white); click/Escape unlocks.
Linked series highlight (series↔bar cross-highlight): linkedHover={{ name: "sync", mode: "series" }} auto-resolves the chart's series-identity field (colorBy/lineBy/areaBy/stackBy/groupBy) and keys the linked selection off it — no hand-wired fields. Add seriesField: "region" to override (align charts whose series live under different prop names). Modes are exclusive: mode is "field" (default) | "x-position" (crosshair) | "series".
CircularBrush — accessible range brush over a cyclical domain (day-of-year, hour, compass): value ({start,end}) / onChange (value | updater) / period (365) / radius / step+largeStep / formatValue / arcFill/stroke. Wrap-around ranges, pointer-capture drag, and full keyboard control (each handle + the range is a role="slider", ←/→ nudges, Shift = largeStep). The radial counterpart to the linear RealtimeHistogram brush. Control-surface contract: it takes value/domain/geometry/onChange and never reaches into a chart — layer it over a chart sharing its coordinate space and feed onChange into your state (or the selection store). Built on the cyclical + radial kit.
Also: ScatterplotMatrix, ChartContainer (title, subtitle, actions, notifications — ChartNotification[] chart-level notices with no mark to anchor to, e.g. audit/data-pitfall findings or user-authored notes; { id?, level? ("info"|"success"|"warning"|"error"|"neutral" → semantic role colors), title?, message, source?, dismissible? }, collapsed into a severity-colored toolbar bell + count badge (bell adopts the most severe visible level's icon/color) that opens a popover of dismissible cards — an overlay, so notices never reflow the plot; sr-only aria-live region announces count + severity, + onNotificationDismiss), ChartGrid (columns, gap), ContextLayout.
Server-Side Rendering (semiotic/server)
HOC charts render SVG automatically in server environments. For standalone generation:
import { renderChart, renderChartWithEvidence, renderToImage, renderToAnimatedGif, renderDashboard } from "semiotic/server"
const svg = renderChart("BarChart", { data, categoryAccessor, valueAccessor, theme: "tufte", showLegend, showGrid, annotations })
const { svg: svg2, evidence } = renderChartWithEvidence("BarChart", { data, categoryAccessor, valueAccessor }) // evidence: { markCount, markCountByType, empty, xDomain?, yDomain?, categories?, nodeCount?, edgeCount?, annotationCount, ariaLabel, warnings } — ground truth from the rendered scene; check `evidence.empty`/`markCount` instead of parsing SVG. MCP renderChart returns the same block.
const png = await renderToImage("LineChart", { ... }, { format: "png", scale: 2 }) // requires sharp
const gif = await renderToAnimatedGif("line", data, { xAccessor, yAccessor, theme: "dark" }, { fps: 12, transitionFrames: 4, decay: { type: "linear" } }) // requires sharp + gifenc
const dashboard = renderDashboard([{ component: "BarChart", props }, { component: "PieChart", colSpan: 2, props }], { title, theme, layout: { columns: 2 } })
All accept theme (preset name or object); theme categorical colors flow to data marks. generateFrameSVGs() returns frame SVGs without sharp/gifenc.
AnimatedGifOptions: fps, stepSize, windowSize, frameCount, xExtent/yExtent (lock axes), transitionFrames, easing, decay, loop, scale.
Server SVGs include role="img", <title>, <desc>, grid, legend, annotations. SVG groups have stable id attrs for Figma layer naming: data-area, axes, grid, annotations, legend, chart-title.
renderChart props match the same accessors documented per-chart above. Sparkline has no axes/grid/legend/title by default (margin 2px). ForceDirectedGraph: materialize nodes before passing (don't infer from edge endpoints).
All components also accept: width, height, theme, title, description, showLegend, showGrid, background, annotations, margin, colorScheme, colorBy, legendPosition. Pass frame-level props via frameProps.
Annotations
All HOCs accept annotations. Coordinates use data field names.
Positioning: widget, label, callout, callout-circle, callout-rect, text, bracket
Reference lines: y-threshold (value, label, color, labelPosition), x-threshold, band (y0, y1 — a null/omitted bound extends to the axis min/max on that side, e.g. y1: null shades "at least y0"), x-band (x0, x1, fill, fillOpacity — full-height vertical region for eras/phases; a null/omitted bound likewise extends to the domain edge; only skipped when the axis has no scale at all)
Ordinal: category-highlight
Enclosures: enclose, rect-enclose, highlight
Label backgrounds: every region-bounding annotation (y-threshold, x-threshold, band, x-band, enclose, rect-enclose, category-highlight) accepts labelBackground for a legibility backdrop behind the label text — "halo"/true (stroke halo in the plot bg; default for threshold/band labels), "box" (semitransparent rounded panel), "none"/false (plain), or a config { type: "halo"|"box", fill?, opacity? (0.85), padding? ({x,y}), radius? (3), stroke?, haloWidth? (3) }. One shared renderer (AnnotationLabel) drives client + SSR, so it works on every frame and in server SVG. band/x-band fill also accepts a HatchFill for hatched regions.
Statistical: trend, envelope, anomaly-band, forecast
Streaming anchors: "fixed" | "latest" | "sticky" | "semantic" — also exposed as lifecycle.anchor on the semiotic/ai annotation lifecycle. "semantic" is typed but currently falls back to fixed positioning; stableId-based re-resolution remains open.
Hierarchy: any annotation accepts emphasis: "primary" | "secondary" across XY, ordinal, network, geo, and static SVG rendering. secondary dims (opacity 0.6) and yields z-order; primary paints at full weight and on top. Type-agnostic, no-op when unset; class hooks annotation-emphasis--{primary|secondary} for further styling.
Connectors: connector: { end?: "arrow"; type?: "line" | "curve"; curve? }. type: "curve" draws a swoopy quadratic-bezier connector (bend = curve × connector length, default 0.25; negate to bow the other way); the arrowhead aligns to the curve's tangent. Default is a straight line.
Auto-placement (opt-in): autoPlaceAnnotations (boolean | config) on any HOC runs the annotationLayout recipe — collision-avoiding offsets for notes without manual dx/dy, curved connector routing when placement must go far. Config: defaultOffset, notePadding, markPadding, edgePadding, preserveManualOffsets (true), routeLongConnectors (true), connectorThreshold.
Density (opt-in, within autoPlaceAnnotations): density (true | { maxAnnotations?, areaPerAnnotation? (20000), minVisible? (1) }) sheds lowest-priority note annotations when the plot is over-crowded — priority = emphasis (primary never shed) → provenance.confidence → lifecycle.freshness (expired first); reference lines/bands/overlays never count. progressiveDisclosure: true keeps shed notes tagged _annotationDeferred (.annotation-deferred, hidden until chart :hover/:focus-within) instead of dropping them; the persistent set is always shown. Pure forms in semiotic/recipes: annotationDensity({ annotations, width, height, ... }) → { visible, deferred, budget } and annotationBudget(w, h). diagnoseConfig flags ANNOTATION_DENSITY when notes exceed the budget.
Association/redundant cues (opt-in, within autoPlaceAnnotations): redundantCues: true gives a colored text note offset from its anchor (the one note type that draws no connector) a faint leader line back to the anchor — a spatial, CVD-safe cue instead of color-alone matching. auditAccessibility flags color-only association as perceivable.annotation-association (warn) and treats redundantCues as satisfying it; diagnoseConfig flags ANNOTATION_FAR_NO_CONNECTOR / ANNOTATION_LONG_CONNECTOR for connector-necessity.
Responsive/cohesion (opt-in, within autoPlaceAnnotations): responsive: true (or { minWidth }, default 480) sheds secondary-emphasis notes once the plot narrows past the breakpoint (keeps primary/unmarked); composes with density, and with progressiveDisclosure defers instead of drops. cohesion: "blended" | "layer" (also a per-annotation field; per-annotation wins) — blended adopts mark colors/typography (default look), layer renders a distinct editorial layer (--semiotic-annotation-color, italic) via the annotation-cohesion--* class.
Audience/defensive (M6): per-annotation defensive: true is never shed by density/responsive (joins the floor) so it survives into every export; with provenance, the layout pass bakes source+confidence visibly into the label ("… (AI · 70%)"). autoPlaceAnnotations: { density: true, audience } accepts an AudienceProfile (anything with a familiarity map) and scales the density budget by aggregate familiarity — low-familiarity keeps more notes (×1.5), expert fewer (×0.6).
Editorial visibility: filterAnnotationsByStatus(annotations, { showRetractedAnnotations?, showSupersededAnnotations? }) returns the current note set without applying styles. applyAnnotationStatus uses the same visibility rule; describeChart and buildNavigationTree skip retracted and superseded notes by default.
Theming
CSS custom properties: --semiotic-{bg, text, text-secondary, border, grid, primary, secondary, surface, success, danger, warning, error, info, focus, font-family, annotation-color, legend-font-size, title-font-size, tick-font-family, tick-font-size (12px), axis-label-font-size (12px), tooltip-{bg, text, radius, font-size, shadow}}.
<ThemeProvider theme="tufte"> {/* named preset */}
<ThemeProvider theme={{ mode: "dark", colors: { categorical: [...] } }}> {/* merge onto dark base */}
Color priority (with colorBy): CategoryColorProvider/LinkedCharts map > colorScheme > ThemeProvider colors.categorical > "category10". colorScheme accepts a named scheme ("tableau10"), an array, or a {category: color} object map for exact per-category colors (no array ordering to keep in sync).
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 (every preset): colors.success/danger/warning/error/info + secondary/surface. Each emits as --semiotic-{role}. Use for status-driven charts: <Waterfall positiveColor="var(--semiotic-success)" negativeColor="var(--semiotic-danger)" />, <Swimlane color="var(--semiotic-warning)" />, status annotations.
Scoped CSS cascade override (per-subtree, no ThemeProvider needed): wrap a subtree in <div style={{ "--semiotic-danger": "#4b0082" }}> — canvas scene builders read CSS vars via getComputedStyle on the canvas DOM ancestor, so cascade rules apply even though rendering is canvas. CSS vars for single-role overrides; nested ThemeProvider for array/scale overrides (categorical palette, sequential/diverging scheme).
AI Design Guidance
- ISOTYPE/icon arrays: Repeated pictograms, semantic icons, and glyph tokens should be legible in the final rendered layout. Treat 16px as the minimum intended rendered icon dimension for ISOTYPE/icon-array designs. This is design guidance, not a Semiotic-enforced minimum: low-level helpers may default smaller for dense/sparkline contexts, so set
tokenSize, glyphsize, SVG slots, or wrapping explicitly. If available width would shrink icons below that visual floor, wrap tokens into multiple rows/columns, reduce visible token count, change the unit value, or choose a non-icon encoding.
AI Features
Surface APIs: onObservation/useChartObserver, toConfig/fromConfig/toURL/fromURL/copyConfig/configToJSX, validateProps, diagnoseConfig (includes tokenEncoding warnings when present), suggestTokenEncoding/diagnoseTokenEncoding, auditAccessibility/accessibilityCaveats + describeChart + buildNavigationTree/AccessibleNavTree/useNavigationSync + buildReaderGrounding (a11y audit + descriptions + structured navigation + bidirectional sync + agent-reader grounding — see Accessibility), exportChart(div, { format }), npx semiotic-ai --doctor/--audit-a11y.
Conversational Interrogation (semiotic/ai)
Headless "chat with the chart" hook. Library ships no UI — BYO 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.context:{ data, summary, componentName?, props? }.summary: stat summary (rowCount, per-field{min, max, mean, median}for numerics, top-k for categoricals, ISO range for dates). Available before anyask().annotations: mergedinitialAnnotations+ latest AI response. Wire to chart'sannotationsprop.summarizeData(data, options?): standalone for server prompting or batch.- MCP tool:
interrogateChart(component, props, query)returns same summary + AI-facing instructions.
Chart Capability Layer (semiotic/ai)
Heuristic chart-suggestion engine — no LLM required. Charts ship capability descriptors next to TSX files; engine ranks against a profiled dataset by intent.
profileData(data, { rawInput?, seriesField? })→ChartDataProfile: per-role candidate fields (x/y/series/category/size/time), distinct counts, monotonicity, structure detection.suggestCharts(data, { intent?, allow?, deny?, maxResults?, includeVariants?, minScore?, audience? })→ rankedSuggestion[]with{ component, family, importPath, variant?, score, intentScores, rubric, reasons, caveats, props }.propsis spreadable directly. Receivability: setaudience.receptionModality(visualdefault |screen-reader|sonified|agent); a non-visual channel audits each candidate and down-ranks charts the audience can't receive there (8-slice pie for a screen reader), adding the audit's findings tocaveats[](familiarity and receivability are separate axes).accessibilityCaveats(auditResult)distils any audit into the same caveat strings.scoreChart(component, data, { intent?, variantKey? })→ evaluate a specific chart for a dataset.useChartSuggestions(data, options)→ memoized React hook returning{ suggestions, profile }.registerChartCapability(capability)/unregisterChartCapability(name)— runtime registration for custom charts.- Intent taxonomy (13 built-in):
trend,compare-series,compare-categories,rank,part-to-whole,distribution,correlation,flow,hierarchy,geo,outlier-detection,composition-over-time,change-detection. Extend viaregisterIntent. - Capability authoring:
Foo.capability.tsnext toFoo.tsx, append to registry insrc/components/ai/chartCapabilities.ts. Declaresfamily,rubric(familiarity/accuracy/precision 1-5),fits(profile)gate,intentScores, optionalvariantswithintentDeltas,buildProps(profile, variant). - Variants encode that settings change what a chart is good for (e.g. StackedAreaChart's
streamgraphvariant boosts trend, penalizes part-to-whole). - Interrogation tie-in: pass
includeSuggestions: truetouseChartInterrogationand the ranked list lands incontext.suggestionsfor the LLM. - MCP tool:
suggestCharts(data, intent?).
Conversation-arc telemetry (semiotic/ai)
Opt-in event store recording the AI session arc: suggestion-shown → suggestion-chosen → audience-set → chart-rendered → chart-edited → chart-replaced → chart-exported | chart-abandoned, plus interrogation-asked/interrogation-answered, reader-navigation events, and annotation-status-changed. Module-scoped, no provider needed. Default surface is no-op — call enableConversationArc() to start.
enableConversationArc({ capacity?, sessionId? })/disableConversationArc(). Bounded ring buffer (default 1000 events).getConversationArcStore()→{ enabled, sessionId, capacity, record, flush, getEvents, subscribe, clear, reset }.getEvents()returns referentially stable snapshot.useConversationArc({ enableOnMount?, disableOnUnmount?, capacity?, sessionId? })→{ history, summary, enabled, sessionId, record, clear }. UsesuseSyncExternalStore.summarizeArc(events)→ pure reducer. Server/replay safe.- Persistence / replay:
registerConversationArcSink(sink)attaches an opt-in durable sink. Built-ins:createLocalStorageConversationArcSink({ key?, storage?, maxEvents? }),createIndexedDBConversationArcSink({ dbName?, storeName?, indexedDB?, maxEvents? }), andcreateWebhookConversationArcSink({ url, method?, headers?, fetch?, mapEvent? }). Sinks receive accepted events only; disabled telemetry is still zero-overhead.loadConversationArc(events, { enabled?, capacity?, sessionId?, append? })/replayConversationArc(...)hydrate the visible store snapshot without re-emitting events to listeners or sinks; defaultenabled: falsemakes replay safe for analytics. recordAudienceChange(audience, previous?, { arcId?, meta? }?)— sugar foraudience-set. Call from audience-pickeronChange.- Events:
ConversationArcEventdiscriminated union. Each variant carries its own payload. - Auto-instrumented:
useChartSuggestionsemitssuggestion-shown(dedup by component-list + intent);useChartInterrogationemitsinterrogation-asked+interrogation-answered(withlatencyMs);AccessibleNavTreeemits the reception pairnav-node-focused/nav-branch-expandedon reader traversal (keyboard/click), correlated by itschartIdprop. Zero-overhead when disabled.
Annotation provenance + lifecycle (semiotic/ai, types re-exported from semiotic)
Two optional blocks attach to any annotation — existing arrays keep working unchanged.
provenance:{ author?, authorKind?, source?, basis?, confidence?, createdAt?, dataVersion?, stableId? }(union of the shipped fields + IDID §8ChartAnnotationProvenance).authorKind= actor ("human"|"agent"|"watcher"|"system"|(string & {}));basis= evidence type ("human-note"|"statistical-test"|"rule"|"llm-inference"|"external-source"|"computed"|(string & {})), distinct from the actor;sourceopen union ("user"|"ai"|"agent"|"import"|"computed"|"system"|(string & {}));dataVersion= data snapshot the note was made against.lifecycle:{ freshness?, status?, supersedes?, ttlHint?, anchor? }. Two orthogonal axes — temporalfreshness("fresh"|"aging"|"stale"|"expired", derived fromcreatedAt+ttlHint) and editorialstatus("proposed"|"accepted"|"disputed"|"retracted"); a note can be fresh-but-disputed.supersedes=stableIdof the note this replaces.anchor:"fixed"|"latest"|"sticky"|"semantic".ttlHint: ISO 8601 duration ("P30D") or ms.withProvenance(annotation, { provenance?, lifecycle? })→ pure, SSR-safe.Annotated<T>type:T & { provenance?, lifecycle? }.computeAnnotationFreshness(annotations, { now?, dataExtent?, thresholds? })→ populateslifecycle.freshness.nowdefaults todataExtentmax, thenDate.now(). Default thresholds 1×/1.5×/3× TTL.annotationFreshnessFor(annotation, nowMs, thresholds?)→ classifies a single annotation. Explicitlifecycle.freshnesswins.applyAnnotationLifecycle(annotations, { now?, dataExtent?, opacity?, strokeDasharray?, labelSuffix?, showExpiredAnnotations?, thresholds? })→ freshness + default visuals (aging dims opacity 0.55, stale dims + dashes"4 4", expired filtered; setshowExpiredAnnotations: trueto keep). Per-band overrides; passnullto disable a band default. Annotation-levelopacity/strokeDasharraywin.applyAnnotationStatus(annotations, { opacity?, strokeDasharray?, labelSuffix?, showRetractedAnnotations?, showSupersededAnnotations? })(M7) → editorial-status treatment, orthogonal to freshness:disputed→(?)+ dim,proposed→provisional dim+dash,retracted→filtered (like expired),accepted→full. Opacity multiplies into existing, so it composes withapplyAnnotationLifecycle(run freshness first). Also resolvessupersedes— a note superseded by a present, non-retracted note is hidden. UsefilterAnnotationsByStatuswhen a non-visual or custom surface needs the same visibility contract without styling. Emit transitions viarecordAnnotationStatusChange(toStatus, { annotationId?, fromStatus?, chartId? })→ conversation-arcannotation-status-changedevent.- Semantic anchors:
anchor: "semantic"/lifecycle.anchor: "semantic"re-resolves throughprovenance.stableIdafter data refresh, using point scene nodes or matching data rows and falling back to the recorded coordinate when the target is gone.
Temporal lifecycle (shared semiotic/realtime + semiotic/ai)
Three systems answer "how does this look as it ages?" on three different time axes — not interchangeable.
| Policy | Lives in | Time axis | Output | Scope |
|---|---|---|---|---|
DecayConfig | semiotic/realtime | buffer position | continuous opacity ramp | per-datum |
StalenessConfig | semiotic/realtime | wall-clock idle | binary live/stale (+ optional badge) | chart-wide |
| Annotation freshness | semiotic/ai | createdAt + ttlHint | 4 named bands (opacity + dashing + expired filter) | per-annotation |
Shared primitive: bandFromAge(ageMs, ttlMs, thresholds?) → "fresh"|"aging"|"stale"|"expired". Exported from both. DEFAULT_LIFECYCLE_THRESHOLDS = { fresh: 1.0, aging: 1.5, stale: 3.0 }. Shared anchor mode (AnnotationAnchor from semiotic/realtime, re-exported from semiotic/ai as lifecycle.anchor).
Streaming chart-time aging: pass chart's dataExtent to applyAnnotationLifecycle — latest data point becomes "now". Pair with withCurrentProvenance(annotation, { author?, source? }) / currentTimestamp() to auto-stamp createdAt. Full survey: /intelligence/temporal-lifecycle.
Variant discovery (semiotic/ai)
Interface for proposing/scoring variants beyond capability.variants. The built-in proposer emits registered variants, conservative heuristic transforms, and same-intent cross-family alternatives; external recommenders can register model/agent proposers.
VariantProposal:{ id, baseComponent, label?, intentDeltas?, rubricDeltas?, buildProps?, rationale?, source: "manual"|"heuristic"|"model", variantKey?, tags? }.VariantScore:{ proposalId, fit (0–5), novelty (0–1), risk (0–1), reasons }. Mixes withsuggestChartscomposite scores.proposeVariant(component, capability, context)→VariantProposal[]. Context accepts{ profile, audience?, intent?, existingVariants? }.evaluateVariantProposal(proposal, profile, audience?, { intent?, baselineComponent? }?)→VariantScore.- MCP tool:
proposeChartVariants(component, props?, data?, intent?, audience?)ranks proposals and returns ready-to-use props. registerVariantDiscovery(fn)→ registers proposer.proposeVariantdispatches through all and dedupes byproposal.id. Returns unregister. Inspect:getRegisteredVariantDiscovery()/clearVariantDiscovery().
AI Behavior Contracts
These rules are generated from ai/behaviorContracts.cjs and are consumed by semiotic-ai --doctor, MCP resources, and docs checks.
- Accessible chart text uses direct chart props (
accessibility.description-props): High-level charts expose title for the visible name, description for a concise accessible description, summary for a screen-reader-only takeaway and interaction guidance, and accessibleTable for the data-table fallback. Agent action: Put title, description, summary, and accessibleTable directly on the chart component when they appear in its schema. For generated L1–L3 description or a navigable chart tree, use ChartContainer with chartConfig plus describe and/or navigable; do not invent frameProps fields. - 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. Its thresholds use { value, color, label? }; BigNumber thresholds use the distinct { at, level, color?, label? } vocabulary. 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. - Serialized proposals keep the initial snapshot (
streaming.serialized-proposal-snapshot): A JSON component/props proposal for MCP, SSR, or evaluation is a static snapshot even when the request mentions future pushes. Keep the supplied initial rows in the component's real data prop and do not invent pushRows, pushRequirement, ref, or method props. Agent action: Return renderable initial props in JSON. If the requested deliverable is React push code, separately omit the controlled data prop in JSX, attach a ref, and call the documented imperative method from an effect or event handler. Example:JSON snapshot: { "component": "LineChart", "props": { "data": [{"week":1,"users":120}], "xAccessor":"week", "yAccessor":"users" } }. React push code instead omits data and calls ref.current?.push(row). - 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. - Axis formatters are React callbacks (
serialization.formatters-are-react-callbacks): xFormat, yFormat, categoryFormat, and valueFormat are callback props, not d3 format strings or axis-title strings. They are intentionally absent from JSON/MCP schemas and string values fail validation. Agent action: In serialized props, omit formatter callbacks and use xLabel, yLabel, categoryLabel, or valueLabel for axis titles. In React JSX, pass a function such as xFormat={value => formatAxis(value)}. Example:JSON: { "xLabel": "Payload (KB)", "yLabel": "Response time (ms)" }. React-only: xFormat={value => formatNumber(value, { maximumFractionDigits: 0 })}. - Value components do not inherit chart-HOC props (
value.bignumber-wire-contract): BigNumber is a value component, not a chart HOC, so it does not inherit the common chart-HOC prop list. It uses label as its visible heading and supports description and summary; title and accessibleTable are invalid. Its percent format expects a ratio such as 0.97 and renders it as 97%. Agent action: Validate BigNumber against its own prop schema and remove inherited chart-HOC props such as title and accessibleTable before returning a proposal. Use label, description, and summary for text. For a 97-of-100 KPI, either pass value={0.97} with format="percent" or pass value={97} with format="number" and suffix="%"; do not combine value={97} with format="percent". Example:{ "component": "BigNumber", "props": { "value": 97, "label": "SLA attainment", "format": "number", "suffix": "%", "target": { "value": 99, "label": "target", "format": "number" } } } - Proportional symbol maps use geographic props (
geo.proportional-symbol-wire-shape): ProportionalSymbolMap reads point rows from points, longitude from xAccessor (default lon), latitude from yAccessor (default lat), and radius from sizeBy. sizeRange is the two-number pixel-radius range. Agent action: Use points, xAccessor, yAccessor, and sizeBy. Do not rename points to data or sizeBy to valueAccessor merely because the map renders circles. Example:{ "points": [{"city":"A","longitude":-122.4,"latitude":37.8,"incidents":18}], "xAccessor":"longitude", "yAccessor":"latitude", "sizeBy":"incidents", "sizeRange":[5,40] } - Physics charts separate chart mode from simulation input (
physics.sample-and-mechanical-inputs): Sample simulations use data plus the chart's accessors. Seeded no-data demonstrations use simulationMode="mechanical" (legacy mode="mechanical" remains accepted); mode otherwise carries chart display modes such as primary or sparkline. Agent action: For observed Galton values pass data + valueAccessor. For unit piles pass data + categoryAccessor + valueAccessor. Use seed for reproducibility, bins for Galton columns, and unitValue for the amount represented by one UnitPile body. Example:{ "component": "GaltonBoardChart", "props": { "data": [{"id":"a","value":1}], "valueAccessor":"value", "bins":4, "seed":42 } } - Physics push methods ingest source records (
physics.push-uses-source-records): Physics HOC refs push source records through the chart's accessors. pushRows and dataIdAccessor are not component props; stable source id fields are retained on spawned bodies without an invented accessor. Agent action: For React live code, call ref.current?.push(row) or pushMany(rows). For serialized snapshots, append the rows to data. Keep category/value/time accessors, but omit pushRows and dataIdAccessor. Example:ref.current?.push({ id: "c", team: "Blue", value: 2 }); <UnitPileChart ref={ref} categoryAccessor="team" valueAccessor="value" unitValue={1} />
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 (live aria-live region sits OUTSIDE role="img" so AT announces hovered/focused data). Auto-detects prefers-reduced-motion, forced-colors. Hooks: useReducedMotion(), useHighContrast().
Chartability audit: auditAccessibility(component, props, { inChartContainer?, describe?, navigable? }) (from semiotic/ai or semiotic/utils) grades a config against Chartability (POUR-CAF) — credits built-ins, flags author-actionable gaps, marks un-checkable items manual (not a false pass). formatAccessibilityAudit(result) renders the report. Surfaced as npx semiotic-ai --audit-a11y (non-zero exit on critical fail → CI gate) + the auditAccessibility MCP tool. Returns { ok, summary, findings[] }; each finding has { id, principle, heuristic, critical, status: "pass"|"fail"|"warn"|"manual"|"not-applicable", message, fix }. NOT a pass/fail cert — pair with manual NVDA/JAWS/VoiceOver testing.
Chart descriptions: describeChart(component, props, { levels?, locale?, capability?, audience? }) (from semiotic/ai or semiotic/utils) generates a layered natural-language description (Lundgard L1 encoding / L2 statistics / L3 trend) → { text, levels: {l1?,l2?,l3?,l4?}, annotations? }. Richest for XY/bar/part-to-whole/distribution; degrades to L1 for network/hierarchy/geo/value. Annotations: when props.annotations is present, the result carries an annotations sentence ("The author has marked 2 features…") and it leads text ahead of L1–L3 — an author-placed note is intent in its purest form. Provenance-aware: an authorKind/source of agent/ai/watcher qualifies it ("an AI-suggested callout"). Absent when no annotations, so un-annotated callers are unchanged. L4 (intent): pass a capability (a chart's descriptor or a resolved { family, intentScores }) and describeChart emits the illocutionary communicative-act sentence ("This is an alerting chart; the peak at March is the point to investigate") — opt-in, default output stays L1–L3. Helpers: resolveCommunicativeAct(component, capability), communicativeActForIntent(intent), type CommunicativeAct. Full-accessibility decoration (title, caption, description, navigation, data download) is the opt-in ChartContainer layer — not baked into the bare chart. <ChartContainer describe chartConfig={{component, props}}> renders an sr-only L1–L3 description (describe={{ visible:true }} to show it, { levels } for verbosity). The audit's assistive.features-described passes when describe is on.
Agent-reader grounding: buildReaderGrounding(component, props, { capability?, audience?, includeStructure? }) (from semiotic/ai or semiotic/utils) → { description (L1–L3), intent (act + L4 sentence), structure (nav tree), text } — the single payload an LLM reads to interpret a chart faithfully (the reader-side complement to a capability descriptor). MCP: groundChart tool.
Structured navigation (Olli/Data-Navigator model): buildNavigationTree(component, props, { maxLeaves?, locale? }) (from semiotic/ai or semiotic/utils) → a NavTreeNode tree (chart → axis/series → datum), labels composed via describeChart. When props.annotations is present it appends an Annotations branch (role: "annotation") so a reader encounters author notes during traversal — provenance + M7 editorial status surfaced inline, retracted and superseded notes skipped, added on every family (incl. root-only network/geo/hierarchy). describeChart also leads its text with an annotation sentence when annotations are present. AccessibleNavTree (from semiotic or semiotic/ai) renders it as a WAI-ARIA tree widget (arrows/Enter/Home/End, roving tabindex, onActiveChange, controlled activeId auto-expands to the node, chartId for telemetry), sr-only by default. With the conversation-arc store enabled it emits nav-node-focused/nav-branch-expanded reception events on genuine traversal (not on canvas-driven activeId changes). <ChartContainer navigable chartConfig={...}> mounts it (navigable={{ visible?, maxLeaves? }}). Audit's compromising.navigable-structure passes when navigable is on (incl. hierarchy charts). Audit options: auditAccessibility(component, props, { inChartContainer?, describe?, navigable? }).
Bidirectional sync: useNavigationSync({ tree, chartId?, matchFields?, selectionName?, annotations? }) (from semiotic or semiotic/ai) → { activeId, onActiveChange, selection, annotatedIds, focusAnnotation }. Tree→canvas highlights the matching mark (field-value selection); canvas→tree maps the hovered/clicked datum back to its leaf. Rides the module-global selection + observation stores — no provider needed: give the chart chartId + selection={sync.selection}, give AccessibleNavTree activeId/onActiveChange. Annotation anchors: pass the chart's annotations and an anchored annotation (carrying the datum's matchFields) resolves to a nav leaf — annotatedIds are the leaf ids with a note; focusAnnotation(annotation | index) jumps the tree + canvas to the anchored point so a non-visual reader can reach an AI's anchored note.
Usage Notes
- Push API: Omit
data.data={[]}clears on every render. - Tooltip datum shape: HOC tooltips get raw data. Frame
tooltipContentgets wrapped — used.data. - Tooltip format cascade:
valueFormat/xFormat/yFormatflow to default tooltip (axis + tooltip read identically). Customtooltipfully overrides — re-pass viaTooltip({format})/MultiLineTooltip({fields:[{format}]}). Bespoke-tooltip charts (Histogram, FunnelChart, LikertChart, GaugeChart) don't participate; customize viatooltip. tooltip="multi": shows all series at hovered X for LineChart, AreaChart, StackedAreaChart. Custom fn receivesdatum.allSeries.- Legend: "bottom" expands margin ~80px. MultiAxisLineChart: use
legendPosition="bottom". A bottom legend is placed outside the bottom axis chrome (tick labels, axis title) so it doesn't overdraw it; the band is auto-measured. Top axes are opt-in, so a top legend reserves nothing by default — setframeProps.legendLayout.axisGutterfor one.axisGutteralso overrides the measured bottom band (0anchors to the plot edge);sideGutteris the left/right counterpart. - Horizontal bars: need wider left margin (
margin={{ left: 120 }}). - Log scale: domain min clamped to 1e-6.
xScaleType: "time": createsscaleTime; required for landmark ticks with timestamps. barPadding: pixel value (40/60 default). Reduce for small charts.sort(BarChart/StackedBarChart/GroupedBarChart/DotPlot):falsepreserves insertion order;"auto"= insertion while streaming, value-desc on static (DotPlot default). StackedBar/GroupedBar default tofalse; the underlying frame value-sorts whenoSortis undefined, so always passsortexplicitly if order matters.fillArea:fillArea={["seriesA"]}fills named series only — names must matchlineBy/colorBykeys.hoverHighlight: requirescolorByas a string field.framePropsstyle functions: bypass HOC color resolution — usecolorByinstead.- Geo imports: always
semiotic/geo, neversemiotic. - Axis config:
frameProps.axes: [{ orient, includeMax, autoRotate, gridStyle, landmarkTicks, tickAnchor }].tickAnchor: "edges"flips first tick'stext-anchortostart, last toend(anddominant-baselineon vertical axes) so edge labels don't overflow. Pairs withaxisExtent: "exact". - Per-axis CSS: every axis renders as
<g class="semiotic-axis semiotic-axis-{bottom|left|right|top}" data-orient="…">. Style via[data-orient="left"] text { font-size: 14px }— CSS-var defaults are set inline viavar(--semiotic-tick-font-size, …), so cascade overrides win cleanly. Tick text:class="semiotic-axis-tick"; labels:class="semiotic-axis-label"; titles:class="semiotic-chart-title". scalePadding: pixel inset on scale ranges (viaframeProps={{ scalePadding: 12 }}).categoryFormat/xFormat/yFormat: can return ReactNode (renders in<foreignObject>). Tick deduplication: adjacent identical labels auto-removed.- Composing overlays: XY/Ordinal paint
--semiotic-bgacross the canvas; stack withframeProps={{ background: "transparent" }}on the overlay. Network/Geo don't paint bg by default. foregroundGraphics/backgroundGraphicsresolved scales: the function form receives{ size, margin, scales }—scalesis the frame's resolved scales ({x, y}for XY,{o, r, projection}for ordinal;nullbefore first layout). Anchor a bespoke SVG overlay toscales.x(...)/scales.y(...)so it can't drift from the axes the chart drew (the HOC analogue of a custom layout'sctx.scales). Fall back to your own mapping whilescalesis null.- Theming a custom chart: prefer
--semiotic-*tokens for decoration (stroke="var(--semiotic-text-secondary)") andctx.resolveColor(key)/ semantic role vars (var(--semiotic-danger)) for data, so the chart tracksThemeProvider/dark mode — or deliberately paint fixed editorial colors on abackground: "transparent"frame for art direction. Both are first-class; the kit's decoration helpers default to--semiotic-*tokens either way.
Performance
Prefer string accessors (xAccessor="value") — always referentially stable. Memoize function accessors with useCallback.
Trust
Not scanned yet. Artifacts are graded after they are crawled, so a recently discovered one may have no result for a while.
Versions
git-7e049550b8fb2026-08-04