All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- First-class
RadarChart— the new ordinal HOC accepts long-form series-by-attribute data and draws connected radial polygons with point markers. It supports legends, selection, linked hover, push mode, typed accessors, category formatting, SSR/static rendering, AI schema discovery, and a dedicated documentation page. - First-class static
WaterfallChart— cumulative signed deltas now have a controlled-data and push-capable HOC alongsideRealtimeWaterfallChart. Numeric, date, and categorical steps are supported, with stablepointIdAccessorupdates, positive/negative styling, connectors, selection, linked hover, SSR/static rendering, AI schema discovery, and a dedicated documentation page. - Chart-level brushing on common HOCs —
LineChartexposes an x-axis brush,Scatterplotan xy brush, andBarCharta value-axis brush throughbrush,linkedBrush, andonBrush.Histogram's existing brush contract is now schema-visible, so serialized and MCP-authored charts can request it. - Dense static Heatmap aggregation —
Heatmapnow acceptsheatmapAggregation="count" | "sum" | "mean",heatmapXBins, andheatmapYBins. Numeric heatmaps with more than 4,096 occupied cells automatically aggregate into a bounded grid when no explicit aggregation is supplied. - Expanded
renderChart()coverage —RadarChart,WaterfallChart,MultiAxisLineChart, andDistanceCartogramnow participate in the static chart registry, including dual-axis formatters/colors and cartogram chrome. - Legends for chord and hierarchy charts —
ChordDiagram,TreeDiagram,Treemap,CirclePack, andOrbitDiagramnow exposeshowLegend,legendPosition, and shared highlight/isolate legend interaction. Automatic legends appear whencolorByis present and reserve the correct plot margin. - Per-component MCP schema resources — agents can discover the compact
component catalog at
semiotic://schema-indexand read only the needed schema, metadata, accessibility guidance, and behavior contracts fromsemiotic://schema/{component}. The fullsemiotic://schemaresource remains available for bulk tooling. - Explicit Stream Frame plugin bootstrap APIs — direct-frame and SSR hosts
can call
registerBuiltInXYPlugins()fromsemiotic/xy, the root entry, orsemiotic/realtime/core, andregisterBuiltInNetworkLayouts()fromsemiotic/networkwhen marks/layouts must be ready before first render.
- Chart HOCs load only their XY mark plugins — scene builders and canvas
painters now resolve through a registry instead of one eager family map.
High-level HOCs register their own line/area/point/bar/heatmap/waterfall/etc.
plugins, while direct
StreamXYFrameusage lazily restores missing built-ins on first client paint and reports actionable SSR diagnostics. - Network layouts and force workers are demand-loaded — Sankey, force, chord, hierarchy, and orbit register explicitly instead of sharing an eager layout index. The force worker client is imported only when the worker policy selects it, keeping unrelated network charts off that graph.
- Optional overlay code stays off default chart paths — marginal graphics and their binning dependency load only for HOCs/configurations that request them; SSR detection and annotation-status filtering no longer import the larger scene-serializer or AI graphs.
- ChartDefinition now produces a real downstream artifact — the seven
pilot definitions generate their canonical AI-schema registry entries, with
equality coverage preventing the pilot and
ai/schema.jsonfrom drifting. - Faster same-style point painting — contiguous opaque canvas points with identical fill/stroke/radius share a path and paint operation. Pulses, patterns, translucent marks, and other overlap-sensitive points retain per-mark compositing.
- AI diagnostics provide concrete repairs — validation diagnoses now
suggest how to remove unknown props, add required props, or correct component
names.
BigNumberreports a dedicated error when chart-onlyaccessibleTableis supplied and directs authors tolabel,description, andsummary. - True push mode is scored as an omit-
datacontract — first-try AI evals distinguish push proposals fromdata={[]}, materialize pushed rows only for validation/render evidence, and can compare the packaged chart skill againstllms.txtunder matching fixtures and budgets. - Bundle and contributor gates measure the shipped product — the MCP binary is production-minified by default, bundle budgets use chunk-aware consumer graphs instead of tiny re-export facades, documentation examples use family subpath imports, and CONTRIBUTING now documents focused/fast/release check tiers plus the complete new-chart checklist.
- Docs routes recover from interrupted lazy-module loads — failed Vite preloads now trigger one guarded refresh, and repeated failures show a usable reload action instead of leaving examples or other documentation routes on a blank page in Safari.
- Automatic network legends now match in static rendering —
renderChart()and direct network SSR flatten hierarchy data when deriving legend categories, so Tree, Treemap, CirclePack, and Orbit output reserves and renders the same categorical legend as the browser. Force, Sankey, and Chord static charts now follow the samecolorByauto-legend default. - Network legend interactions now affect rendered marks — highlight and
isolate state is composed into node and connected-edge styling for chord,
hierarchy, force, Sankey, and ProcessSankey charts instead of changing only
the legend swatches. Push-mode network frames now report their live node
category domain, so a
ChordDiagramlegend can appear and stay synchronized after ref-based pushes; empty domains no longer produce an"undefined"legend item. - Realtime nullish category colors stay aligned — controlled
RealtimeHistogramandRealtimeSwarmChartdata now uses the same category coercion as push mode, preserving literal"null"and"undefined"stack keys across rendered marks and legends without weakening network legend filtering. - Waterfall selection and edge geometry — static Waterfall bars now consume
linked selections through their area style, and automatic x domains reserve
half a step around the first/last centered bars so the final total is not
clipped. Categorical axes contain only real steps, while explicit partial or
complete x-extents remain authoritative. The shared scene/domain path also
covers
RealtimeWaterfallChart. - Waterfall categorical updates stay stable — categorical step indexes are
retained across immutable
update()calls andpointIdAccessorpaths instead of being reassigned as objects change identity. - Radar formatting and series identity —
categoryFormatreaches radial ticks, functionalseriesAccessorvalues drive color correctly, and a one-series chart closes its connector polygon instead of rendering isolated points. - Static/client parity for newly registered charts — MultiAxis per-series axis formatters and colors, DistanceCartogram sparkline/strip chrome, Heatmap aggregation, Radar geometry, and Waterfall marks now agree across browser HOCs and server rendering.
- Marginal graphics paint immediately and survive toggles — Scatterplot and BubbleChart provide the marginal painter for SSR/hydration, while retained marginal values stay synchronized when graphics are enabled, disabled, or recomputed after mount.
- Custom layouts recover consistently across families — XY, ordinal,
network, and geo pipelines now share the same guarded layout attempt,
diagnostic,
onLayoutError, and last-good-scene preservation contract. A failed callback cannot erase a previously valid scene or leave overlays and restyle state out of sync. - Tree-shaken network layouts still register reliably — layout plugins use
explicit registration rather than side-effect-only imports, preventing blank
Sankey, force, chord, hierarchy, or orbit output under
"sideEffects": falsebuilds.
- Utils ESM barrel no longer produces a webpack warning —
semiotic/utilsnow exportsresolveResponsiveDimensiononly from its React-free core entry and explicitly forwards the remaining React helpers. Consumers retain the complete public API without conflicting star exports.
- Renderer-independent cursor styling — retained mark styles now accept a typed
cursorvalue. For example,StreamNetworkFramenode icons can usenodeStyle={(node) => ({ cursor: node.url ? "pointer" : "default" })}; the same contract applies to XY, ordinal, network, geographic, and physics marks across canvas, SVG, and static rendering. Realtime chart HOCs also expose a top-level cursor default, inherited by the staticTemporalHistogramsibling. Cursor styling is presentation only and composes with the existing click, observation, and keyboard interaction APIs. - Bounded event-time streaming —
RealtimeLineChartcan reorder out-of-order input witheventTime, reduce high-rate streams into retained windows withaggregate, and expose a chart-specificRealtimeLineChartHandle.flush()for explicit end-of-stream boundaries. Live policy/accessor changes drain the prior tail; structural aggregate changes replay controlled data or begin a documented new push-only epoch. - Observation and tooltip composition APIs —
ObservationReadout,observedDatum,TooltipRoot, tooltip chrome helpers, and the multi-line / multi-point tooltip builders make coordinated readouts and application-owned tooltip shells first-class public surfaces. - Reusable data, interaction, theme, and responsive helpers —
balanceSnapshotsToFlows,networkEdgeHitTarget,themeToCSSVariables,resolveResponsiveDimension,useResponsiveSize, and the public physics canvas paint context cover recurring application-level work without requiring internal imports. - Evidence-led documentation examples — new long-form examples include The Last Scarcity, Hellhole Changed Addresses, The Parataxis Machine, and How a Hit Travels, with checked data projections and browser contracts. Repository agent guidance was split into compact always-on instructions and on-demand references, and the shared blog-authoring workflow is now reusable.
-
Unified chart evaluation —
evaluateChartnow composes data-contract, representation/deception, accessibility, and optional render-evidence checks into ranked findings and notifications, withsemiotic-ai --evaluateand an MCPevaluateCharttool. -
Side legends reserve their focus-ring edge clearance — left/right legend auto-margins now include a measured 3px outer gutter, keeping swatches and keyboard focus rings inside the SVG in CSR and SSR.
legendLayout.edgeGutteris the explicit override;0restores flush legacy placement.frameProps.legendLayoutis now a typed, nested AI/schema contract across chart families. -
BigNumber's value-component boundary is explicit in the chart registry — generated AI descriptions now distinguish it from chart HOCs and direct agents away from unsupported
title/accessibleTableprops towardlabel,description, andsummary. -
CommonJS recipes keep geo dependencies lazy — the CJS
recipesfacade no longer eagerly loadsd3-geo; geographic dot-grid exports load their dedicated bundle on demand, and the built/package smoke path now guards the non-geo import graph. -
Typed chart accessors preserve caller datum inference — generic string and function accessors now retain concrete datum fields through high-level and realtime chart APIs while remaining compatible with the erased Stream Frame boundary. Named callbacks and misspelled-field failures have compile-time regression coverage.
-
Realtime charts use the shared chart contract — controlled data and push mode are both generically typed; title, description, summary, accessible tables, legends, legend interaction, loading/empty chrome, selection, linked hover, and observation metadata now flow consistently through all five realtime wrappers.
-
Continuous and compound chart semantics are complete — ChoroplethMap has an interactive gradient legend in client and static output, GaugeChart participates in loading/empty and selection behavior, and ScatterplotMatrix / MinimapChart expose one chart-level accessible table instead of duplicating one per internal frame.
-
ProcessSankey layout quality and example stability improved — boundary-fan centering, pool highlighting, ledger assertions, ordering heuristics, deterministic documentation layouts, and history-river integration coverage were tightened.
-
Physics layouts now reflow as one system —
responsiveWidth/responsiveHeightmeasure the HOC-owned container and rebuild bodies, colliders, projections, chrome, and semantic geometry together. This applies to the stock Physics charts, Crucible, and ChainReaction instead of stretching a child canvas over stale coordinates; directStreamPhysicsFramebounds also follow the measured frame. -
Package entry graphs preserve runtime identity with less duplication — ESM and CommonJS consumers share canonical client contexts across family subpaths, pure
themes/core,recipes/core, andutils/coreimports remain React-free, and mixed facades retain explicit client boundaries without fragmenting providers.
- Heatmap color-scale plumbing —
Heatmapnow routescolorScheme="custom"andcustomColorScale(value)into the shared heatcell scene graph instead of falling back to Blues in CSR and SSR. It also preservesframeProps.colorSchemeprecedence and the theme's sequential scheme in SSR.RealtimeHeatmapnow honors its documented named sequential schemes, theme fallback, and custom value-to-color functions. String fills returned byframeProps.areaStylealso override individual bounded Heatmap cells in both render paths. customColorScalemust be callable — theHeatmapandRealtimeHeatmapchart specs declaredcustomColorScaleas["object", "function"], so a non-callable object scale passed validation, was silently dropped by the server path, and threw on the client when the scene builder invoked it. The spec now declares"function"(matching the TypeScript type, which never allowed anything else), sovalidateProps/--doctorreport it up front, and both client paths degrade to the named scheme instead of throwing. d3 scales such asscaleSequentialare callable and unaffected.- Client, direct-SSR, and static-renderer parity — OrbitDiagram now renders deterministically on the server; grouped XY inputs, custom layouts, geo flow configuration, zero-valued network edges, theme scales, radial backgrounds, empty-scene graphics, solid-plus-graphics background ordering, and nested SVG gauge overlays follow the same contract in every renderer.
- Swimlane tracks honor rounded ends —
trackFillbackgrounds now shareroundedTopgeometry with their lane pieces, including functional radii, in canvas and static SVG output instead of showing square track ends behind rounded value bars. - Interaction geometry matches visible marks — canvas and quadtree hit testing agree for large points, glyphs, symbols, areas, lines, network marks, and geographic marks; BumpChart respects device pixel ratio; XY hover radius reaches every wrapper; gradient legends provide visible, roving keyboard focus; and stale cursor state is cleared when a mark is no longer hit.
- Authored frame styling remains authoritative — physics chart HOCs now compose
frameProps.bodyStyleover their generated defaults instead of silently replacing it, including cursor values; DistanceCartogram foreground rings and the north indicator also use the correct plot-relative coordinates with nonzero margins. - Physics settled rendering matches the live chart — static Physics HOCs use the same mode-specific margins as their client siblings, include chart-owned chrome and projections, settle paced runs deterministically, and inject theme tokens only when a server theme is explicit. Crucible terminal snapshots now have effectively pixel-identical SSR/CSR geometry and color across all three browser engines.
- Prototype-shaped identifiers are ordinary data — ProcessSankey layout, packing, ordering, and cache records; Crucible components, products, relations, and metrics; and realtime palette/registry state now safely accept keys such as
__proto__,constructor, andtoStringwithout mutation, omission, or inherited-value collisions. - Streaming resources terminate and invalidate correctly — superseded force-layout work can stop without rejecting another chart's request, tile cache identities include provider / URL / device pixel ratio and recover from failed loads, progressive ingestion normalizes invalid chunk options, staleness uses the injected frame clock, and CSS color observers are installed only while subscribers exist.
- Theme and canvas color behavior is stable — dynamic theme preset lookup remains a supported string-keyed API, Heatmap canvas colors resolve CSS variables with readable label contrast, CSS cache invalidation no longer leaks global observers, and explicit zero stroke widths are preserved.
- Release artifacts and CI are fail-closed — package-surface generation is deterministic across dev/prod builds, internal/test declarations are pruned, mixed React facades carry the correct client boundary, API compatibility is checked against the previous tag, TypeDoc and docs-asset warnings are enforced, browser/machine baselines are release gates, benchmarks compare on one runner, and npm / Cloud Run / MCP Registry publication now verify the same exact version and package integrity in order.
- Package and repository housekeeping — aligned Size Limit plugins, updated the MCP SDK and audited transitive graph, removed stale generated archives/pages from the tracked root, and added guards for release-version, README, package-surface, and deployment metadata drift.
- Zoom-aware canvas resolution — all Stream Frames now repaint when browser zoom, OS display scale, or display migration changes
devicePixelRatio, even when CSS layout dimensions stay fixed. The newmaxDevicePixelRatiochart/frame prop overrides the default desktop (3×) and coarse-pointer/small-screen (2×) backing-store caps for consumers that prefer high-zoom crispness over paint and memory cost. DPR / cap changes are resolution-only invalidations: retained geometry is re-rasterized without a full scene rebuild. - Per-axis extent modes for XY charts —
frameProps.axes[i].extentaccepts"nice" | "exact"and overrides chart-levelaxisExtentfor one axis. Tick generation, grid positions, y-domain padding, client SVG, and static SSR output resolve the same per-axis mode. Use chart-level"exact"(or an exact bottom axis) for a pinned time window, then{ orient: "left", extent: "nice" }so magnitude ticks stay rounded. - IANA adaptive time ticks —
adaptiveTimeTicks(granularity, { timeZone })accepts"UTC","local", or any IANA id (e.g."America/Los_Angeles"). Labels and calendar-boundary detection use that zone viaIntl. Legacyutc: falsestill maps to local;timeZonewins when both are set. Default remains UTC for deterministic SSR. - First-class multi tooltip config —
tooltip={{ mode: "multi", content? }}enables multi-series hover and optionally supplies a custom renderer that receives unwrapped data withallSeries/xValuere-attached. Equivalent totooltip="multi"whencontentis omitted; no separateframeProps.tooltipModeneeded.
- ProcessSankey is a full temporal process chart, not a thin Sankey wrapper.
Layout is modular (mass simulation, packing/reuse, bonded multi-slot groups,
crossing-min / inside-out / combined ordering, hug placement with post-scale
geometry refine, quality metrics, and optional module Web Worker offload).
Ribbons support feeder runway (
ribbonMinRun, including"auto"), system in/out lifecycle cutouts, style-rules hatch fills, density-budgeted labels (showLabels="auto"), selection/linkedHover withselectionDatumraw/scene modes, and a push API viauseProcessSankeyPush. Pure quality helpers (diagnoseProcessSankeyLayout/Props/explainProcessSankeyLayout) feeddiagnoseConfig. SSR and CSR sharebuildProcessSankeyScenes.inventoryAtTimefolds a ProcessSankey edge ledger into node stock at a given time (opening-stock lift for source-like nodes).toProcessSankeyTimeis the shared domain/accessor/tooltip time coercion. - History-river docs examples.
/examples/germany-still-becomingand/examples/united-states-drawn-togethertell vertical, time-down stock-and-flow stories on ProcessSankey (sharedProcessRiverExampleLayout, checked-in compact data projections, sync layout for deterministic docs paint). Integration and visual baselines cover hug placement and SSR/CSR parity. - A causal-process Sankey example.
/examples/good-earth-lying-flatuses a six-stage, vertical ProcessSankey to trace how modern security strategies can become risk, involution, precaution, and withdrawal. Carbon theming, claim lenses, confidence-aware ribbons, and incoming/outgoingHatchFillnode junctions keep the interpretive argument explicit and inspectable. - Local adaptive time ticks —
adaptiveTimeTicks(granularity, { utc: false })formats labels and detects calendar boundaries in the runtime's local timezone. UTC remains the default so server-rendered charts stay deterministic.
- Network entry graph budgets rise with ProcessSankey layout/worker surface
(
semiotic/network/aigzip graphs). Capability, API-surface, llms, JSDoc (≥2 examples), and visual-baseline gates know ProcessSankey push and linked-hover burn-down status. - Dependabot bumps for React Router 7.18.x alignment, Playwright, size-limit, Vite React plugin, testing-library, and setup-node 7.
- Docs river examples use the shared
process-river__*shell classes andlayoutExecution="sync"so Playwright source-route tests paint and stay free of worker/module-URL races in Vite. History-river demos shareHISTORY_RIVER_PROCESS_SANKEYdefaults,ProcessRiverStageReader, and scroll-linked stage selection; code snippets note that production apps should preferlayoutExecution="auto". - ProcessSankey release hygiene: dead helpers and unused types cleaned,
showLabels schema enum is string-only (
"auto"), file-size splits for ordering/packing, CI-friendly ordering budget, and refreshed visual snapshots for hug + process-sankey SSR/CSR sheets. SharedtoProcessSankeyTime/readChartAccessor, pureinventoryAtTimeledger helper, andModuleWorkerSession(force + ProcessSankey layout workers). - Stage-bound ProcessSankey nodes can opt into
nodeSizing="max", holding their peak causal mass across an authored extent so same-stage handoffs no longer make a node visually smaller than an attached incoming or outgoing ribbon. - Custom multi-series tooltip context — custom tooltip functions used with
multi mode (
frameProps.tooltipMode: "multi"or an equivalent multi hover path) retain hover-rootallSeriesandxValuemetadata after the raw datum is unwrapped, matching the data available to the built-in multi tooltip.
-
A high-touch evaluation example makes the prove track inspectable.
/examples/the-benchmark-is-a-chart-tooturns the completed 516-request GPT-5.6 compatibility baseline and its 603-request repeated follow-up into an evidence-first reading room. It separates answerable questions from correct abstentions, preserves the first-attempt failure matrix and cost/latency context, documents the scorer audit, and shows where the revised grounding payload and generation contracts held across repeated trials. Targeted result scoring now reports only submitted fixtures while retaining the available corpus size and distinguishes missing submissions from pending or otherwise unscored rows. -
The HRA/WPP typology pilot now runs on the real kidney ASCT+B v1.6 digital object. A pinned generator produces a checked-in, workshop-ready artifact for the observed direct children of kidney, with ontology IDs kept as semantic keys, provisional cell-type review signals kept separate from measured counts, and a public
/interoperability/hra-wppwalkthrough. The actual data exposed the pilot's first failure case: distinct ontology concepts share labels, while mixing parent and descendant rows turns hierarchy into a misleading rank. A focused regression test pins both corrections. A public workshop bundle now adds the exact kidney fixture, a 30-minute review protocol, response schema/template, explicit extension-vs-context decision rubric, and written co-sign template. -
MCP
3.8.6distribution is verified end to end. The officialio.github.nteract/semioticRegistry entry marks3.8.6latest, and stable Cloud Run revisionsemiotic-mcp-server-00071-2rwnow serves the exact release with commit/build identity. The production smoke passes health, initialize, public tools, resources/build-info, render evidence, and schema retrieval. Its initialize assertion is now channel-aware: stable requiressemioticplus the exact package version, while nightly retains the commit-qualifiedsemiotic-nightlycontract. -
MCP Registry publication now follows every release instead of one pinned version. The reusable publisher derives the exact version from the checked-out package manifest, runs only after npm publication, and remains manually dispatchable for backfills. The local Registry gate now rejects a reintroduced literal version or a release workflow that stops calling the publisher. A source-controlled distribution inventory records canonical paths, stale secondary cards, and the distinction between Registry publication and assistant-vendor acceptance; the broken Smithery link and stale Glama badge are no longer promoted in the README.
-
SSR/RSC and accessibility positioning now matches the shipped surface. README, SSR, and migration guidance consistently show non-streaming chart HOCs imported directly by a Next.js Server Component through Semiotic's client boundary; an app-owned wrapper is reserved for hooks, callbacks, browser state, or push-driven streaming. Accessibility language now states the EAA date and the infrastructure Semiotic supplies without claiming that a chart library certifies application-level legal compliance.
-
Physics charts now consume and produce linked hover. The eight standard physics HOCs already resolved named selection stores over
body.datum; theirStreamPhysicsFrame.onBodyHoverpath now publishes the source datum through the shared hover store and clears it on leave. Authored wrapper bodies such as Gauntlet cores are unwrapped throughsourceDatum, and frame-level hover callbacks still compose with the shared producer. Those eight charts now advertisesupportsLinkedHover: true;ChainReactionChart, whoseselectedTaskIDsis chart-specific, remains false. The capability gate recognizes the physics selection bridge so metadata cannot outrun behavior. -
Accessibility audits now report the shipped texture surface accurately.
perceivable.color-aloneandflexible.textures-adjustablerecognize serializableHatchFilldescriptors authored throughstyleRules, without treating one rule as proof every category has a redundant cue. The remaining warning is precise: texture assignment is not yet automatic, theme-aware, or exposed as a reader-controlled toggle. -
PhysicsPileChart→UnitPileChart,PhysicalFlowChart→PacketFlowChart. Both leaked the substrate into the user-facing name — nothing else in Semiotic isSVGLineChart— and "Physical" told a reader nothing about the reading protocol. The new names carry it: values unitize into countable bodies, and discrete packets travel authored routes.PacketFlowChartalso gives the four flow names a rule: Packet = discrete items on routes, Process = stages with capacity,FlowMap= geographic,SankeyDiagram= static proportional. The old names stay exported indefinitely as deprecated aliases (same shape as the existingGuantletChartalias), and the pre-3.9.0 docs routes/charts/physics-pile-chartand/charts/physical-flow-chartstill resolve. Registries (schema, validation, capabilities, MCP, server configs) carry only the canonical name, sosuggestChartscan't recommend one chart under two names.PhysicsChartNames.test.tspins all of this.Two names I did not change, correcting my own earlier claim:
ProcessFlowChart— "Process" names the domain being visualized, not the substrate, exactly likeFunnelChart; andPhysicsCustomChart— the four custom-layout escape hatches are named by frame family (XY / Ordinal / Network / Geo / Physics), which is the correct axis.
- Static SVG output now preserves chart-level primitive styling. Server
configs apply
stroke,strokeWidth, andopacitywith the same precedence as their browser chart HOCs across hierarchy, network, ordinal, XY, and value charts instead of silently falling back to built-in outlines. Heatmap cell borders and heat-cell opacity now reach SVG output as well. Focused SSR-to-browser fixtures cover treemap and chord primitives, Likert styling, and heatmap borders across Chromium, Firefox, and WebKit. - Horizontal legends no longer overlap axis ticks or titles. Browser and
static renderers now reserve a shared axis-chrome gutter before placing top
or bottom categorical and gradient legends, including XY, ordinal, heatmap,
minimap, and realtime histogram paths.
legendLayout.axisGutterexposes an exact override; setting it to0preserves plot-edge placement when that is intentional. - Value components no longer appear to inherit chart-HOC props in agent
guidance. The common-prop section now states its boundary before the chart
catalog, and the generated BigNumber behavior contract tells agents to
validate against the value-component schema and remove
title,accessibleTable, and other inherited chart-HOC props. The first-try fixture revision records this guidance change without rewriting the published 7/9gauge-staticfollow-up result. - Targeted evaluation summaries no longer label every unscored row as a
missing submission. Reports now expose
unscoredseparately frommissingSubmissions, so a queued or pending response remains visible without implying that its model output was omitted. - Runtime reduced-motion changes no longer fast-forward physics simulations. Entering the settled path clears the animation clock in both synchronous and worker modes; a preference change that arrives during worker work is replayed afterward. Returning to animation therefore starts with a zero delta instead of applying all wall-clock time elapsed while reduced motion was active.
ChainReactionCharttask tiles no longer overlap. Tasks sharing a lane and a dependency depth were nudged apart bymin(18, taskHeight * 0.3)while staying full height, so a 58px tile shifted 17px overlapped its neighbour by 41px — unreadable in any real plan, where several lanes carry 2–3 tasks at the same depth. The stack is now sized from the tile height: tiles shrink to fit their level band (floor 22px) and step by that height plus a gap, so they never collide and never drift into the neighbouring depth.ChainReactionChartlane labels no longer collide with the chart title. They were pinned aty=23, the same band the frame draws the title in. They now derive from the topmost task, so they sit just above the first row of tiles regardless of size.- Physics:
prefers-reduced-motionnow reaches the settled end state. A settle only admitted the spawns already due att=0and never advanced simulated time, so a reduced-motion reader of a paced physics chart saw one body while the projection overlay reported the full count (measured:GaltonBoardChartwith 60 rows rendered 1 ball), and elapsed-time event tapes —GauntletChart's gates,ChainReactionChart's dependencies — never fired at all, leaving the settled projection reporting the authored start rather than the outcome.PhysicsPipelineStoresettles now advanceelapsedSecondsper step and re-check due spawns inside the loop, soinitialSpawnPacing, per-datumspawnAt, and time-driven event tapes all complete. Both settle entry points share one loop (physicsPipelineSettle.ts) so they cannot drift apart again.StreamPhysicsFrameadditionally re-settles while the store reports pending work (bounded), because reduced motion never schedules a second frame and event-tape charts spawn bodies fromonTick. - Physics chrome and projections now track the theme. Ten physics modules
(plus
MobileChartContainer,SentenceFilter,IntentMark) referenced CSS variables the library never emits —--semiotic-accent,-negative,-positive,-background,-text-primary,-text-muted,-muted,-highlight,-fg— a shadow vocabulary for real roles (primary,danger,success,bg,text,text-secondary,selection-color). Their fallback hex always won, so the settled projection — the layer the docs designate as the chart — was the one layer that ignoredThemeProviderand dark mode. All now point at real roles, keeping their previous hex as the fallback so untouched (unthemed) rendering is unchanged. A new vocabulary test diffs every referenced--semiotic-*name against whatthemeToCSSemits, with an allowlist for genuinely consumer-set knobs, so the class can't return. The last holdout,--semiotic-on-primaryinMobileStandardControls, now reads--semiotic-bg— the contrasting surface for text on a primary-filled pill, which inverts correctly in dark themes instead of pinning white against a light primary. No phantom tokens remain.
- One-surface Stage C conference demo. The local
/intelligence/conference-demoroute now composes the October talk's full “question to a chart you can defend” path: ranked suggestions, deterministic refusal, declared production scale, governed audience, captured-model variant, the KStreams custom-chart escape, pinned server-render evidence, live reader grounding, and JSX handoff. Interactions record a fresh typed conversation arc that can be downloaded for rehearsal review. The committed recovery is now a normalized 13-event Playwright rehearsal arc rather than a hand-authored substitute. The talk-demo browser contract preloads the custom-chart chunk under throttling, takes the browser fully offline, and then exercises every beat with external requests and sockets blocked. - Conference recovery recording package. A reproducible Playwright capture produces an MP4, candidate/variant/handoff keyframes, the live typed arc, and a manifest with byte counts and SHA-256 digests. The conference page exposes the checked-in fallback, and a focused asset test authenticates all four files plus the required refusal, audience, evidence, grounding, and export events.
- Prove-track evaluation substrate.
evals/first-trynow carries 22 generation prompts across static and push modes plus three guard cases, with first-attempt and post-repair scoring against validation, diagnostics, andrenderChartWithEvidence.evals/groundingcarries 10 rendered charts, 50 answerable/unanswerable questions, PNG andbuildReaderGroundingartifacts, and a three-condition 150-trial expansion. Both suites have provider-result schemas, pinned run metadata, raw/summary output, and local regeneration and integrity checks. A spend-capped, resumable OpenAI Responses runner now reads credentials from the environment or macOS Keychain, disables provider storage, records hashes instead of raw outputs, estimates locked-rate cost, and backs off across TPM/HTTP retry signals. The first complete live run issued 516 requests acrossgpt-5.6-sol,gpt-5.6-terra, andgpt-5.6-lunafor $2.2792. First-try scores were 21/22, 17/22, and 17/22. Grounding's combined PNG-plus-payload condition beat PNG alone for Sol (45/50 versus 43/50) and tied it for Terra (45/50) and Luna (44/50), but did not improve answerable-question accuracy. A paired manual audit hardened the provider-neutral scorer before publication: explicit passive abstentions are recognized, answerable abstentions cannot pass by repeating an expected label, numeric matches use token boundaries, and reports carry a scoring revision. The published evidence therefore supports an abstention benefit for Sol, not the stronger expected claim that the payload closes an answerable chart-reading gap. - Typed refusal and render-proof conversation events.
proposal-refusedrecords the deterministic gate, reason codes, and repair alternatives;render-evidencerecords mark count, emptiness, and render warning codes separately from the weaker fact that a render was attempted. The event vocabulary moved into a focused type module while the existing store, sinks, replay, and import path remain backward compatible. The Variant Discovery docs now install and clean up a real hand-writtenregisterVariantDiscoveryproposer over committed bimodal data, explicitly distinguishingsource: "model"provenance from a live model call. - Experimental VACP 0.1.0 interoperability bridge.
unstable_createSemioticVACPBridgeat the neutralsemiotic/experimental/vacpsubpath exposes Semiotic chart grounding as a dependency-free, structurally typed VACP capability/state/action surface: stable graph references, scoped full-or-delta state, complete column discovery with bounded summaries, validated point/interval selection, unambiguous navigation, and explicitly registered custom actions.unstable_SemioticVACPBridgebinds that facade to liveLinkedChartsselection and observation stores without rendering UI. The React binding installs only after a client commit; the explicit installer refuses a server process global and any foreign page global, then removes only the bridge it installed. - Adopter-ready IDID v0.1 portability kit. The three library-neutral JSON
Schemas and six canonical worked fixtures ship at documented
semiotic/spec/v0.1/*resource paths, with Draft 2020-12 and dependency-free validation coverage. The spec guide now documents the paper/runtime annotation union, exact stretch-pick admission rule, npm CJS/ESM imports, and a core-vs-extension typology crosswalk. The standalone Vega-Lite binding preserves capability/audience metadata and annotation provenance through its strict attach/read round trip while ordinary Vega-Lite renderers safely ignore the namespacedusermeta.ididblock. - Safe activation of transported capability policy.
unstable_bindPortableCapabilityvalidates a carried descriptor, resolves it against an explicitly supplied host capability, and overlays its rubric, intent scores, variants, caveats, and mobile contract while retaining the host's executablefits,buildProps, numeric, scale, and quality gates. Structured refusal diagnostics cover invalid, unresolved, and mismatched descriptors. The Vega-Lite portability demo now routes both carried capability policy and audience metadata throughsuggestCharts. ChainReactionChartis now a registered chart. It shipped exported fromsemiotic/physicswith zero registry presence — no chart spec, capability descriptor,semiotic/aiexport, MCP registry entry, docs page, nav entry, or CLAUDE.md mention — so it silently skipped schema validation,suggestCharts, MCP, and the docs-coverage gate. All of those now exist, plus an SSR test asserting the HOC path derives the settled task state (a blocked prerequisite leaves its dependents unarmed) with no simulation. ItsserverChartConfigsexclusion is documented: the settled reading is an authored overlay over zero bodies, so a body-counting config would report an empty scene./examples/chain-reaction— "The Release Machine". The page existed but was routed nowhere, so it never shipped. It now does: two coordinate systems over one authored dependency graph — anintervalLanesLayoutswimlane answering when and who, thenChainReactionChartre-plotting the same rows by dependency depth so a blocker's downstream reach becomes the reading. Its centerpiece swimlane was ~20 lines of hand-rolled SVG (hardcoded palette, 7px labels, a magic/17day scale,index % 3fake sub-track packing); that is now the shipped recipe, so bars are real hit-tested marks with keyboard nav, tooltips, a data table, and correct interval packing. Landed through all five example registries plus the nav.- Registry-independent chart-registration gate.
check:surface-paritynow walks entry-point chart exports from the filesystem and requires each to be inCHART_SPECSor explicitly allowlisted. Every other check in that script derives its expectations from the registry, so a chart absent from the registry was invisible to all of them. This is the check that would have caughtChainReactionCharton day one. - Terminal-state contract for event-tape physics charts. A tape-driven 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
describeChartreceive.CrucibleChartandChainReactionChartalready satisfied it;GauntletChartdid not. NewresolveGauntletTerminalStates/resolveGauntletTerminalState(semiotic/physics) fold the authored tape, mirroring the live tick's effect order — with a regression test asserting the pure result equals a real simulated run. That test also surfaced and now documents a real property of the metaphor: withcrashDetectionarmed (the default), physics is a second outcome source no pure fold can predict, so the pure result is "what the plan earns on paper". PasscrashDetection={false}for a fully authored reading. - Physics stage-geography kit (
semiotic/physics+semiotic/recipes).physicsStageGeographynames the charge → apparatus → destinations structure every physics chart had been re-inventing locally (GauntletstartX/socketX/graveyardX, Cruciblechamber/mouth/outlets; Galton bins and Pile tubes independently wrote the same lane formula). Companions:physicsStageColliders,physicsChargePoint,physicsDestination,describePhysicsStageGeography. Authoring vocabulary for new charts andPhysicsCustomChartlayouts — shipped charts keep their layouts, and tests pin the builder to their exact lane math so anything built on it lands in the same visual family. - Physics family contracts filled in.
seedonGauntletChart(the only physics HOC missing it);rerunMSonEventDropChart,PhysicsPileChart,ProcessFlowChart, andPhysicalFlowChart(it was on 4 of 8); plus standardselectioninputs across all eight registered physics HOCs.selectionaccepts either a{ name }store config or a resolved{ isActive, predicate }body predicate; the store's datum predicate is lifted overbody.datum, so chrome bodies (walls, pegs, tubes) never match a data selection. ThelinkedHoverprop is retained as groundwork, but the capability remains false until body hover is wired to its producer callback.GaltonBoardChartandPhysicsPileChartalso declare capabilityvariants(observed vs mechanical; projected vs units-only) — no physics chart had any. - Settled ledger (
semiotic/experimental).buildPhysicsSettledEvidencenow reportsqueuedCount,sedimentedCount, and awarningsarray, plus an optionalledgerwhen the caller declares itscharge(the total bodies the chart claims entered). Every physics chart shares one deep structure — a charge enters, an apparatus routes it, destinations hold it at rest — so they all owe the same invariant: every charged body is accounted for in exactly one place.PHYSICS_QUEUE_UNDRAINEDandPHYSICS_LEDGER_MISMATCHsurface a violation, and the SSR renderer declaresinitialSpawns.lengthas its charge sorenderChartWithEvidencereports both warnings for physics charts.
BumpChart(semiotic/xy). A ranking chart: each x-column ranks its series byyAccessor, and rank becomes vertical position. Setribbonto encode the original magnitude as a true perpendicular-offset ribbon width (following the d3.svg.ribbon approach) instead of a fixed-width line — both modes share the same sampled centerline, soanimatetweens only the width during a line↔ribbon toggle.highlightTopcolors just the N best series by mean rank and greys the rest vianeutralColor. ExportsBumpChartand the purerankBumpDatahelper plus its types. Ships with SSR, AI schema/metadata and MCP-registry registration, and a docs page.AreaChartsemanticLine. A new prop (defaulttrue) paints hard, value-banded color segments along an area's top stroke wherever the value crosses asemanticGradientthreshold — the stroke counterpart to the semantic fill. A shared segment-splitter guarantees canvas and SVG render identical bands.legendLayout.sideGutter. Reserves space between a side (left/right) legend and the plot-adjacent axis chrome (ticks, titles), applied consistently across the client and SSR renderers.MultiAxisLineChartauto-sets a 70px dual-axis default; override viaframeProps.legendLayout.legendDistance. Controls the gap between the plot and its legend, with dynamic content-measured side-legend width and margin estimation.- Unified gradient config + exported types.
gradientFill,lineGradient, andsemanticGradientnow all accept a single{ stops: [{ offset, color?, opacity? }] }shape (legacy shorthands still work);GradientConfig,GradientStop, andSemanticGradientInputare exported fromsemioticandsemiotic/xy. - Data Viz for Dummies II–VI. Five new scrollytelling example pages expanding coverage across advanced XY, ordinal, network, geo, and physics patterns.
- Default legend distance reduced from 12px to 10px across the client and SSR paths, tightening automatic side/top margin reservations and the legend-tight diagnostic threshold.
- Legend placement no longer clamps to a minimum on-canvas offset. Numeric
margins are now authoritative for legend position (matching SSR), so a
numeric
marginsmaller than the legend needs can push it partly off-canvas. Rely on auto-margins (omitmargin) or size the margin to the legend. DistanceCartogramfractionallineModenow repositions flow-line endpoints and interpolates intermediate vertices by arc length; previously fractional lines were left un-repositioned.- Legend creation/interaction logic split into a dedicated hook module, re-exported from the shared hooks entry (no import changes).
- Removed the internal
semanticGradientToColorStopsexport (theSemanticGradientStoptype alias is retained).
renderChart/ SSR:framePropsoverrides now take precedence over mode defaults, so e.g.frameProps={{ showAxes: false }}is honored in server rendering.- Geo
DistanceCartogram: corrected radial interpolation so geography is no longer pushed out of the plot at partialstrength, and clampedstrengthto[0, 1]with a NaN guard. - Physics kernel: a body resting only against a side wall (or a sideways sleeping neighbor) no longer falsely counts as supported/sleeping, so it keeps settling.
- SSR/static legend parity: aligned static and client legend positioning, including gradient legend label alignment; resize remaps now rescale semantic area stroke color bands.
CrucibleChart. A new physics-backed HOC (semiotic/physics) where bounded peer components undergo authoredphasesandevents, form declaredproducts, and settle into reason-labelledoutletswith source lineage — the ledger/projection is authoritative, motion never infers classification, timing, membership, loss, or routing.buildCrucibleProductEvents(...)is pure authoring sugar forcombine → contribute* → complete-product. The ref handle'sreplay()atomically restarts the deterministic tape mid-run;reset()restores-and-pauses. Ships with SSR support, AI/schema registration (CrucibleChart.capability.ts), and full test coverage.- Word Trails recipe.
wordTrailsLayout(semiotic/recipes) is a quantitatively anchored word-cloud layout: column = category, segment = ordered vertical position, weight = font size, with overlap-free stable placement.wordTrailsProgressiveReveal(...)fades future/past segments without reflow for scrollytelling-style reveals. - Net Ensemble recipe.
netEnsembleLayout(semiotic/recipes) lays out ensembles of disconnected/trivially-connected DAGs as small multiples in motif bands — splitting into weakly-connected components, classifying each by directedness (single sink vs. branching), and grouping order-isomorphic motifs via Weisfeiler–Leman fingerprinting.analyzeNetEnsemble(nodes, edges)is the pure headless census with no rendering. Demo at/recipes/net-ensemble. - DistanceCartogram strip layout. A new
cartogramLayout: "radial" | "strip"prop adds a Langren-style 1D cost-axis layout (origin left, cost right), used automatically inmode="sparkline"and available in any mode. - ForceDirectedGraph node/edge stroke separation.
nodeStroke/nodeStrokeWidth(node-only outline) andedgeColor/edgeWidth/edgeOpacity(edge-only stroke) let a caller style nodes and edges independently; the genericstroke/strokeWidth/opacityprops continue to style all marks uniformly. Precedence per property: specific > generic > built-in default. RenderEvidenceexposes the resolved margin and plot rectangle.renderChartWithEvidencenow returnsmargin: {top,right,bottom,left}andplot: {x,y,width,height}— the actual geometry Semiotic used after auto-reservation (e.g. a legend), matching exactly where the SVG'sdata-areagroup is translated. A caller hand-drawing an SSR overlay no longer has to guess or conservatively bail when it can't reconstruct the plot rectangle from its input margin alone.- Chart-aware numeric data audit.
semiotic/ai,semiotic/ai/core,semiotic/utils, andsemiotic/utils/corenow exportauditData,formatDataAudit,profileNumericFields, andtoDataAuditNotifications. SerializablenumericContractson chart capabilities detect non-finite/non-numeric values, zero-span or single-row domains, invalid log inputs, negative size geometry, unsafe normalized or part-to-whole totals, and scale-dominating outliers with bounded row evidence.diagnoseConfiginherits the checks, schema-driven docs playgrounds show a live Data Truth Lens, andChartContainer dataAuditcan surface findings in its existing accessible notification bell. - Data Viz for Dummies example. A scrollytelling example organizes seven chart families by a two-way taxonomy (data structure vs. analytical task) through a fictional basketball season.
- Frame-batched network ingestion. Sequential
StreamNetworkFrame.push()calls now share one layout pass per animation frame instead of recomputing the whole layout for every edge.pushMany()absorbs pending single-push layout work into its synchronous batch; reads/mutations flush first, andclear()cancels uncommitted layout work so imperative semantics stay deterministic.
renderCharthonors customsvgAnnotationRulesserver-side. A chart'ssvgAnnotationRules(top-level orframeProps) now runs during static/SSR rendering the same way it does on the client — custom annotation shapes (endpoint bulbs, hover guides, bespoke glyphs) no longer silently vanish fromrenderChart/renderChartWithEvidenceoutput. Passed through for XY, ordinal, network, and geo static render paths alike.- SSR annotation context now infers
frameTypefrom scales/projection, matching the client overlay. A non-ordinal static render (geo, bare network) no longer advertises a default vertical ordinalprojection, which could make a customsvgAnnotationRulesrule branching on frame type resolve incorrectly server-side. framePropsescape-hatch overrides forshowAxes/showLegend/showLabels/showGrid/xLabel/yLabel/categoryLabel/valueLabelno longer silently dropped inrenderChart. The static common-prop builder used to reassert each of these from the mode-resolved default after spreadingframeProps, clobbering an explicit override the live HOC otherwise honors (frameProps spreads last there). A chart passingframeProps={{ showAxes: false }}(or any of the above) server-side now renders identically to the live component instead of always showing the mode's default axis/label chrome.- Ordinal
x-thresholdannotation now renders in SSR. On a horizontal ordinal chart (bar, swimlane, etc.) the value axis IS the x axis, but the static renderer never populated an XY-stylescales.xfor ordinal frames, sox-thresholdalways silently dropped — while the client (OrdinalSVGOverlay) already resolved it correctly against the r-scale.x-thresholdnow mirrors the same orientation-aware resolutiony-thresholdalready had. profileNumericFieldsno longer misclassifies dirty-numeric strings. Whitespace-only strings (" ") now count as missing rather than non-numeric, and an explicit"NaN"/"Infinity"/"-Infinity"string now counts as a non-finite hazard rather than being lumped in with unparseable garbage — distinguished from garbage (which also parses toNaN) by the literal token, not the parsed value.PhysicsSettledSVG'srenderBodySVGnow receives the resolvedidPrefix. A custom body renderer emitting its own<defs>(a<filter>, a gradient) can namespace those ids — SVG ids are document-global, so without this, two settled-physics SVGs embedded in one document could collide and mis-apply each other's filter.CrucibleChart's settled-product glow filter now uses it.
- Sequence diagram kit.
semiotic/recipesnow exportslayoutSequence(linear spines, span-arc packing, shared-edge partition) alongsidelayoutChipStrip,packSpanLevels,scaleArcBand,spanArcPath, andpartitionSharedEdgesfor hand-built sequence and span layouts. - Grid observatory recipe + example. A new "The Grid Is the Real AI
Infrastructure" example ships with a reusable
gridObservatoryrecipe module (fuel-stack series, reserve-margin snapshots, threshold bands, and event annotations) exported fromsemiotic/recipes. - Report-style example. The Semiotic Standard example gained a report-oriented "Plate C" layout demonstrating inline, context, and table chart deployments, and the Analyst Adventure example gained a secret-warp room and expanded Forecast Vault interaction.
- Recipe chrome helpers.
recipeChromeaddshullFromBoxes(and theHullBoxtype) for convex-hull enclosures around grouped marks.
- Physics pipeline refactor. Overflow eviction and quiescence tracking were
extracted into dedicated modules. Settling now recognizes sustained
quiescence as
atRestin addition to full sleep, so simulations settle and rerun reliably even when a straggler or tethered body never formally sleeps (GaltonBoard with many balls, GauntletChart force-held properties). Gauntlet chrome now gates to a compact layout and pop bursts scale with body size. - Network scene-rebuild stability.
StreamNetworkFrameuses a sharedrebuildSceneNowpath that avoids duplicate scene builds during hydration, keeps post-layout/ingest scene state current, and syncs hover/particle color caches from the authoritative scene fills. - Leaner entry graphs. Brush overlays for MinimapChart and ScatterplotMatrix (and the ordinal/XY brush overlays) are now lazy-loaded, and a chunk-aware bundle-size gate guards the reduction.
- SVG serializer split. Ordinal scene-node SVG rendering moved into
SceneToSVGOrdinalwith shared helpers insceneToSVGShared, keeping SSR alignment intact under the file-size limits. - Small-mode histograms. Histogram sparkline category padding now defaults to 0 for readability at small sizes.
- Schema/capability metadata. ProcessSankey, PieChart, and related capability and validation metadata were refreshed so suggestions and prop validation reflect the current API.
- SSR/CSR render parity.
renderChartand static SVG output now match the browser for mixed line+area fills, semantic gradients, line bands, pie/donut start angles, swimlane value extents and hatch fills, treemapcolorByand parent labels, range-mode candlesticks, streamgraph (baseline="wiggle") stacked areas, hierarchy and network styling, open-ended band annotations, and the LineChart series features (forecast,anomaly,directLabel,gapStrategy,showPoints), SankeystyleRules, and top-levelautoPlaceAnnotations. Regressions are guarded by dedicated parity tests. - Annotation editorial visibility. Retracted and superseded annotations are now hidden consistently across the client and server render paths.
- Imperative ref stability. The shared imperative handle and ordinal
replacesupport were stabilized across re-renders.
- Narrative examples. Added Earthquakes and Europa Languages examples, including deterministic data fixtures and example-gallery previews.
- Browser/server rendering alignment. Browser, static SVG, and MCP render paths now share chart-mode defaults, line and geo styling, legend layout, temporal histogram configuration, and specialized network, geo, gauge, and custom-chart rendering behavior.
- SSR parity coverage. Replaced the redundant SSR/CSR screenshot matrix with semantic markup and targeted geometry assertions, while retaining the dedicated cross-browser visual suite.
- Docs example route smoke test. The route contract now targets the
ExamplePageLayouttitle specifically, and the Earthquakes dashboard uses a nested heading level instead of duplicating the page's H1.
- MCP accessibility contract. Chart schemas, validation, repair guidance,
accessibility audits, and
getChartSchemanow consistently expose the directtitle,description,summary, andaccessibleTableprops.improveChartreturns suggested accessibility prose separately with its canonical location, whileauditChartno longer credits unsupported text. - Public
getChartSchemaoutput. The hosted five-tool profile now declares and returns structured results for component lists, component schemas, and unknown-component errors, including direct andChartContaineraccessibility guidance.
- Stateless MCP HTTP rendering. A normally completed request body no longer
cancels render work; hosted
createChartrendering is verified to returnstatus: "render-proven"with SVG and render evidence.
- MCP HTTP health endpoint.
GET /healthis the supported health and deployment-identity endpoint; the unnecessary/healthzalias was removed. - Hosted release channels. Nightly continues to validate
main; stable deployments remain separately release-pinned to the published npm package.
-
StreamPhysicsFrameand physics process runtime. A deterministic, streaming physics frame for data stories where motion is the mechanism: sampling, lateness, queues, routing, collision, threshold crossing, and accumulation. It includes resident worker execution; running/paused/settled lifecycle; chart-derived colliders, regions, portals, absorption, forces, annotations, selection, observations, semantic interaction, and an imperative control surface; optional Matter and Rapier adapters; capacity queues, finite service-resource pools, service-level and dependency-gate controllers, journey ledgers, reference envelopes, and trace comparison; accessible settled projections and tables; deterministic SVG/evidence output and server-stepped animated GIF frames; plus EventDrop, Galton board, gauntlet, pile, collision, physical-flow, process-flow, and custom physics HOCs. -
ChartContainer notifications. New
notificationsprop surfaces chart-level notices that have no single mark to anchor to (a data-pitfalls or accessibility-audit finding about the whole chart, an unplaceable data-quality result) and custom user-authored notes. They collapse into a single severity-colored toolbar bell with a count badge — the bell adopts the icon and color of the most severe visible notice — and clicking it opens a popover with the dismissible cards, so a notice arriving or being dismissed never reflows the chart body. EachChartNotificationis{ id?, level?, title?, message, source?, dismissible? }; levels (info|success|warning|error|neutral) resolve through the theme's semantic role CSS variables, a screen-reader-onlyaria-live="polite"region announces the current count + most-severe level so notices arriving while streaming are still voiced with the popover collapsed, and dismissal is tracked internally byidwith anonNotificationDismisscallback for host stores/telemetry. Semantic class hooks:semiotic-chart-notifications(wrapper),-toggle(bell),-badge,-popover, andsemiotic-chart-notification+--{level}/-source/-title/-message/-dismiss(cards). Types exported fromsemioticandsemiotic/ai; demonstrated on the Chart Container feature page and the Chart Clinic example. -
Worker-based force layout. ForceDirectedGraph gains
layoutExecution("auto"default |"worker"|"sync"),layoutLoadingContent, andonLayoutStateChange; inautomode, layouts whose estimated cost (iterations × (nodes + edges)) exceeds a threshold settle in a short-lived module Web Worker and fall back to the synchronous path when workers are unavailable. NewforceLayoutAsync()(Promise sibling of theforceLayoutrecipe, identical deterministic output) anduseForceLayout()(React wrapper; SSR and first hydration stay synchronous for markup parity, and settled positions are memoized by graph identity + options so remounts don't re-enter a loading state). Ships a packagedforceLayoutWorker.jsasset — CSP deployments needworker-src 'self'. -
x-bandannotation. Full-height vertical shaded region (x0,x1,fill,fillOpacity,label) for marking eras/phases, rendered by both the canvas annotation rules and static SSR output; annotations with a missing bound are skipped in both. -
intervalLanesLayoutminBarWidth(default 2): zero- and short-duration intervals stay visible and hoverable on long domains. -
Custom-layout readback:
ref.current.getCustomLayout(). All four custom chart HOCs (XY/Ordinal/Network/Geo) expose the most recentlayout(ctx)result on their ref, besidegetData()/getScales()— hosts that need the computed placement (stats readouts, inspectors, validation) no longer re-run the layout function themselves.nullbefore the first layout or on built-in chart types. -
unwrapDatumalso unwraps.datumnesting (some interaction payloads nest the raw object there rather than under.data), and is now documented as the single unwrap path for bothonObservationandframeProps.tooltipContentvalues.
- Force layout model overhauled (degree-aware charge, degree-normalized link
strength, radius-aware collision, weaker centering; the
forceLayoutrecipe is now backed by d3-force). Layouts settle to better-spread positions, but the same seed produces different geometry than earlier 3.x betas — regenerate position-pinned snapshots.forceStrengthdocumentation was corrected to its actual link-attraction semantics, and its response curve changed with the new model, so hand-tuned values may need revisiting. intervalLanesLayoutpacks in rendered-pixel space, so theminBarWidthfloor and inclusiveend + unitextension can no longer overlap a same-track neighbor (previously visible as darker doubled fills when one interval ended the same unit the next began).- DataPitfalls and GoFish adapters remain experimental. The DataPitfalls
bridge is exposed from
semiotic/experimentalasunstable_toDataPitfallsChain/unstable_buildDataPitfallsBridge, with return-path helpers for DataPitfalls PR #35 reports:unstable_toDataPitfallsNotificationsfor chart-level findings andunstable_toDataPitfallsAnnotationsfor host-anchored Semiotic v3 annotations. The GoFish DisplayList adapter remainsunstable_fromGofishIRon the same endpoint. Neither adapter is part of the stable API surface yet. - Force layout worker session.
runForceLayoutWorkerreuses a long-livedForceLayoutWorkerSession(request IDs, no spawn/terminate per layout), matching the physics worker lifecycle. Abort cancels the pending request without killing the shared worker. - Growing-window default cap.
windowMode: "growing"now defaultsmaxCapacityto 100_000 (was 1_000_000). Dev builds log a one-time warning when the buffer first crosses 50_000 points. world-atlasis an optional peer. Built-in reference geographies still work when the package is installed; consumers who never callresolveReferenceGeographyno longer take it as a hard dependency. (Still a devDependency for tests/docs.)- Library build target raised from
es2015toes2020. - CI: full gate suite runs once on Node 22; other engines run a smoke matrix (install, dist, vitest, typecheck).
- size-limit budgets added for
physics,server,ai,recipes,utils, andvalueentry points. - Stream frames memoized:
StreamXYFrame,StreamOrdinalFrame,StreamNetworkFrame,StreamGeoFrame, andStreamPhysicsFrameareReact.memo-wrapped so parent re-renders with stable props skip the frame body. - Interaction canvas idle skip on XY and Geo (one clear when hover ends).
- Shared
frameThemeColorsmodule for version-cached theme/background resolution (StreamXYFrame uses it; other frames can adopt the same helper). RingBuffer.resizeusesslicefor shrink (avoids O(n²)shiftloops when a growing window hits its cap).pipelineIdentityOpsshared helpers for id-keyed remove/update paths.- Shared
paintCanvasBackgroundhelper used by XY / Ordinal / Network / Geo frames (CSS-var-safe fill, transparent + backgroundGraphics opt-outs). - Network dirty-canvas path: full clear/redraw only when data, transition, particles, encodings, or continuous animation need it — annotation-only rAF retries no longer thrash the data canvas.
- Root entry diet: physics HOCs (
GaltonBoardChart,GauntletChart,PhysicsPileChart, etc.) are no longer exported fromsemiotic. Import fromsemiotic/physics(orsemiotic/aifor tooling). Measured full package entry ~233 KB gz (was ~296 KB gz). - XY decay/pulse share a version-cached datum→index map
(
buildDatumIndexMap) so continuous frames do not rebuild the map twice. - Geo dirty-canvas path: hover-only repaints skip the data canvas and only update the interaction layer.
- Shared
paintNeedshelpers (needsDataCanvasPaint/needsInteractionCanvasPaint) used by Network/Geo/XY paint gates. - Test typecheck is clean: baseline emptied (~228 → 0 known errors). Cleared keyboardNav (82), ordinalSceneBuilders (45), recipes, heatmap, hydration suites, networkColoring, and the remaining debt files.
GuantletCharttypo alias ofGauntletChart— useGauntletChart. Removal planned for the next major.
CONTRIBUTING.md/DEVELOPMENT.mdupdated for the tsup + Vite toolchain (removed stale Rollup/Parcel/src/processingguidance).
- ForceDirectedGraph edge width now honors a weight field. A string
edgeWidthaccessor (e.g.edgeWidth="weight") read the property off theRealtimeEdgewrapper instead of the underlying edge data, so every edge silently fell back to width 1. Field and functionedgeWidthaccessors now resolve against the raw edge (mirroring node styling), and thesemiotic/serverrenderChartpath gained the sameedgeWidth/edgeColor/edgeOpacityhandling for SSR parity. - Crowded bar-chart category labels no longer overlap. Ordinal category axes (BarChart and siblings) now thin their tick labels to an evenly-spaced subset when too many bins crowd the axis — the classic temporal-histogram case. Charts with few enough categories to fit are unchanged.
- Custom-layout overlays no longer drift on the first responsive resize. XY custom
layouts (
XYCustomChart, GoFish recipes) skipped the layout re-run on a dimension-only change and tookcomputeScene's fast coordinate-remap path, which rescales canvas scene nodes but never regenerates the SVG overlays — so glyph chrome (e.g. the GoFish flower petals) stayed at the pre-measurement width and sat offset from their scene nodes until any other change forced a rebuild. Custom layouts now always re-run on a size change, keeping overlays and scene nodes aligned.
- BarChart suggestion caveat for temporal categories. When a BarChart's category
axis holds time-bin labels (month/weekday names,
YYYY-MM,Q3,Week 12, or a temporal field name),suggestChartsnow surfaces a caveat pointing toward a time-aware treatment (real dates + LineChart/AreaChart, or a temporal histogram for streaming counts).
- Lineage DAG recipe helpers.
semiotic/recipesnow exports lineage DAG helpers for data-flow and KStreams-style network layouts, with tests covering stable topology layout. - KStreams docs demo. The docs add a KStreams recipe page and example data, plus expanded custom network chart guidance for selection-aware layouts.
- Network custom layout selection metadata. Network custom layouts now preserve selection
metadata through pipeline layout and render paths, with regression coverage for selection
actions and
NetworkCustomChartbehavior.
- Refreshed developer/dependency tooling, including Prettier, TypeScript ESLint, Rollup, and Sharp.
- OpenAI Apps domain verification for hosted MCP. HTTP mode now serves the raw
OPENAI_APPS_CHALLENGE_TOKENfrom/.well-known/openai-apps-challenge, allowing ChatGPT Apps domain verification against Cloud Run and other HTTPS-hosted MCP origins without committing the token. - Cloud Run verification playbook.
deploy/cloud-rundocuments the Challenge Base URL shape, token environment variable, andcurlcheck for ChatGPT Apps verification, and the wrapper now tracks thesemiotic@^3.7.3release line.
- Added MCP HTTP regression coverage for the OpenAI Apps challenge route while confirming the unauthenticated OAuth protected-resource probe continues to return 404.
- Stateless MCP HTTP mode for hosted connectors.
semiotic-mcp --httpnow creates an ephemeral MCP server + Streamable HTTP transport per request, returns JSON responses instead of holding long-lived SSE streams open, and exposes/mcp,/healthz, and/healthendpoints. This makes the read-only MCP tool surface suitable for autoscaling serverless hosts such as Cloud Run. - Cloud Run deployment wrapper.
deploy/cloud-runpackages a minimal public MCP deployment that runs the publishedsemiotic-mcpbinary, documents unauthenticated read-only deployment, health endpoints, host allowlisting, and ChatGPT/Claude connector setup, and includes hosted-app privacy and terms pages for app review.
- MCP HTTP hardening. HTTP mode normalizes the
Hostheader for optionalMCP_ALLOWED_HOSTSDNS-rebinding protection, returns clean 404s for non-MCP paths including.well-known/*probes, makes request teardown idempotent, and closes each per-request transport promptly to avoid serverless keep-alive leaks. - Docs dark-mode polish. The visible
AccessibleNavTreeselected row now resolves through--semiotic-surface,--semiotic-grid, and--semiotic-bg, with--semiotic-textapplied to visible rows. The Accessibility / Structured Navigation bidirectional BarChart now mirrors the docs theme by switching betweencarbonandcarbon-dark. - Annotation blog demo reliability. The "Annotations That Get Contested, and Heard" chart now uses numeric XY coordinates with month tick formatting, so editorial-status callouts render on the visible line while the prose and navigation tree keep human-readable month labels.
- Cloud Run wrapper release line. The Cloud Run package now depends on
semiotic@^3.7.2so the hosted wrapper resolves the 3.7.2 MCP server after the npm package is published.
- Fixed a light selected-row fallback in the accessible navigation tree when the docs site was in
dark mode but no
--semiotic-surfacetoken was present. - Fixed the bidirectional sync demo's BarChart rendering as a light Carbon island inside dark docs.
- Fixed missing visual annotations in the contested-annotations blog demo caused by string month values being used as XY coordinates.
- Render evidence.
renderChartWithEvidence()(semiotic/server) returns the SVG plus a machine-readableRenderEvidenceobject computed from the rendered scene graph — mark counts by scene type, resolved axis domains, anemptyflag, category/node/edge counts, annotation count, and the accessible name — so agent repair loops and CI assertions can verify a chart actually drew data marks without pixel inspection. The MCPrenderCharttool now returns the same evidence block alongside its SVG/PNG output. - Misleading-design diagnostics.
diagnoseConfig()gains a deception-check pack: inverted extents (INVERTED_AXIS), unlabeled dual-axis series (DUAL_AXIS_UNLABELED), trend windows cropped to a favorable slice (CHERRY_PICKED_WINDOW), negative values in part-to-whole encodings (PART_TO_WHOLE_NEGATIVE— an error for pie/donut/funnel), non-interpolatingcurve="basis"smoothing (NON_PASSING_CURVE), slope-distorting aspect ratios (EXTREME_ASPECT_RATIO), and over-sliced pies (PIE_TOO_MANY_SLICES). These patterns mislead human readers and — per the chart-deception literature — vision-language models the same way. - Theme contrast conformance gate. Every shipped theme preset is now tested against WCAG-derived
floors (4.5:1 for text/tooltip/annotation roles, 3:1 for the focus indicator), with sub-3:1 mark
colors pinned in an exact-match known-exceptions ledger so palette regressions fail and
improvements must shrink the ledger. The axe integration scan re-enables the
color-contrastrule on the strength of the gate. - Scorecard top-1 agreement. The capability quality scorecard now reports strict
top1AgreementRatebeside the lenient top-3 rate (current canonical set: 93% top-1 / 100% top-3), ranks the top-3 over distinct components rather than variants of one chart, and gains fixtures for the previously unexercised Heatmap, GaugeChart, FlowMap, and DistanceCartogram descriptors. - ChatGPT Apps widget (experimental). The MCP server gains
renderInteractiveChart, which renders a static-data chart through the same server render path asrenderChartand returns atext/html;profile=mcp-appwidget (ui://semiotic/chart-widget.html) with fit/zoom, data, hover, and render-evidence controls for ChatGPT developer-mode connectors oversemiotic-mcp --http. A deployment playbook lives in the repo asCHATGPT_APPS_DEPLOYMENT.md, and an MCP protocol test suite covers the tool and widget resource end to end. - Docs prerender route artifacts. The docs build now emits one prerendered HTML file per
route with sanitized, route-specific machine-readable content in each page's
<noscript>fallback, plus adocs/build/llms-routes.jsonroute index for agent readers; the docs route check verifies the output.
- Theme legibility fixes (WCAG AA).
pastelstextSecondary/focus/annotation,bi-tooltextSecondary, and thetufte-dark/journalist/playfulannotationcolors were deepened to clear the contrast floors; the empty-state / BigNumber-empty / data-table-caption fallback color moved from#999(2.8:1) to#666, matching the default theme'stextSecondary. - Capability descriptor judgment fixes.
DifferenceChartno longer takes fullcompare-seriesmarks when it would silently drop series beyond its native two; flatBarChartyields on crossed two-categorical matrices (Grouped/Stacked/Heatmap show the matrix) and on raw-observation data;ChoroplethMaprequires at least two area features (a one-region choropleth has nothing to compare). contrastRatio()now parses 3-digit hex shorthand (#333), making the default themes measurable.
- Colocated tests for the
NetworkCustomChartandOrdinalCustomChartescape hatches (the XY variant was already covered).
- Accessibility audit, descriptions, and structured reader navigation.
auditAccessibility()/formatAccessibilityAudit()grade chart configs against Chartability-style heuristics, whiledescribeChart(),buildNavigationTree(),AccessibleNavTree, anduseNavigationSync()provide layered chart descriptions, WAI-ARIA tree navigation, bidirectional tree/canvas focus sync, and annotation-anchor focus for non-visual readers. - IDID reader-grounding and receivability primitives.
describeChart()can emit an optional L4 communicative-act sentence from a chart capability,buildReaderGrounding()combines description, intent, and structure into one agent-readable payload,AudienceProfile.receptionModalityletssuggestCharts()penalize charts a non-visual audience cannot receive, andaccessibilityCaveats()feeds audit warnings into recommendation caveats. - Conversation-arc telemetry.
enableConversationArc(),disableConversationArc(),getConversationArcStore(),useConversationArc(), andsummarizeArc()expose a bounded, opt-in event stream for suggestion, interrogation, navigation, export, and annotation-status events, with zero overhead while disabled.registerConversationArcSink(),createLocalStorageConversationArcSink(),createIndexedDBConversationArcSink(),createWebhookConversationArcSink(),loadConversationArc(), andreplayConversationArc()add opt-in durable capture and replay hydration without duplicating sink or analytics events. - Variant-discovery API and MCP tool.
proposeVariant()emits registered variants, conservative heuristic transforms, and same-intent cross-family alternatives;evaluateVariantProposal()scores fit, novelty, risk, rubric deltas, and audience bias; and MCP now exposesproposeChartVariantsfor agent-driven variant exploration. - Chart repair workflow primitive.
repairChartConfig()and the MCPrepairChartConfigtool use capability fit and chart suggestions to critique a proposed chart choice and return safer alternatives for agent retry loops. semiotic/valueandBigNumber. A focal-value KPI component now ships as a lightweight value entry point, with formatting, threshold, comparison/target, staleness, push-buffer, and slot APIs for embedding trend or chart context.- First-class annotation design assistance for 3.7.0.
autoPlaceAnnotationsnow composes collision-aware placement, curved connector routing, density budgets, progressive disclosure, responsive shedding, redundant association cues, cohesion modes, audience-aware amount, and defensive annotations. Per-annotationemphasisestablishes hierarchy, while provenance confidence supplies a default reading order when hierarchy is not explicit. - Annotation provenance and editorial lifecycle.
AnnotationProvenanceandAnnotationLifecyclecarry actor, evidence, confidence, stable identity, freshness, editorial status, and supersession metadata.applyAnnotationLifecycle,applyAnnotationStatus, andfilterAnnotationsByStatuskeep visual treatment, descriptions, and structured navigation aligned on the current annotation set. - Stable semantic annotation anchors.
anchor: "semantic"/lifecycle.anchor: "semantic"now re-resolves annotations throughprovenance.stableIdafter data refresh, using point scene nodes or matching data rows before falling back to the recorded coordinate when the target is gone. - Annotation reception surfaces.
describeChartleads with author-marked features,buildNavigationTreeadds an Annotations branch, and the accessibility audit checks color-only note-to-target association. - Annotation connector diagnostics.
diagnoseConfig()now warns about far notes without a connector and very long connectors, keeping placement guidance aligned with the annotation design assistant. - Annotation design guidance docs. The first-class Annotations docs section now includes Overview, Design Guidance, Advanced Annotations, and Provenance & Lifecycle pages with live examples.
- Linked-hover series mode.
linkedHover={{ mode: "series" }}now resolves each chart's series-identity field automatically, withseriesFieldavailable as an override for cross-chart series highlighting. - Capability-driven visual baseline gate.
check:visual-baseline-capabilitiesderives SSR and linked-hover visual coverage requirements fromchartSpecs.ts, verifies the current Playwright evidence, and keeps the remaining SSR/CSR parity and linked-hover interaction snapshots in one-way burn-down maps. It is wired into CI,release:check, andprepublishOnly. - Shareable, restorable docs playgrounds. Every
/playground/*page now serializes its knob + dataset state into the URL (?sc=…&ds=…) and restores it on load, with "Copy link" and "Copy config (JSON)" affordances. The round-trip dogfoods the library's owntoConfig/toURL/fromURL/fromConfig/copyConfigso a playground configuration becomes a portable, inspectableChartConfigartifact; non-serializable composite playgrounds degrade gracefully (no toolbar, no URL writes). - Faithful "Copy" in docs live examples. The code a docs
LiveExamplecopies to the clipboard now serializes the real props the chart rendered with, instead of the trimmed/elided display stub — so copied example code reproduces the example rather than referencing undefined or shortened values. Display stays readable; copy is runnable. The pure code-generation moved to a React-freecodegenmodule. - Generated, complete
llms.txt.docs:llms(scripts/generate-llms-txt.mjs) regenerates the rootllms.txtretrieval index with the full chart catalog derived fromchartSpecs(grouped by family, every charted component), each entry tagged with its communicative act (resolveCommunicativeAct) so an agent reader gets what a chart is for, not just what it shows. Replaces the previously hand-maintained (and stale) index; kept fresh by thecheck:llmsgate in CI andrelease:check, and regenerated duringwebsite:build. - Docs: per-chart "At a glance" grounding panel. A reusable
ChartGroundingcomponent renders, live for each chart, the communicative act it performs (buildReaderGrounding), a layered L1–L3 description (describeChart), the chart type's reader caveats, and an accessibility badge (auditAccessibility— hovering the badge lists the specific findings; clicking opens the full audit) — the reader/agent grounding for the chart, computed from the shipped intelligence APIs so it can't drift. Now on all 39 static chart pages (props centralized in a reviewed fixtures map; realtime/push-only charts are exempt), and enforced bycheck:docs-coverage. - Docs: "Reshape to unlock" generative suggestions. The
/choosepicker now goes beyond charts the data already fits: from a flat table's field profile it proposes the transform that unlocks Semiotic's distinctive charts — pivot two columns into a Sankey, stamp an event log with time for a ProcessSankey, nest categories into a Treemap, etc. — each with the reshape and why that chart is worth it. Surfaces the flow/temporal/hierarchy/geo charts that a fit-only recommender can never reach. Driven by a puresuggestReshapes(profile)heuristic. - Docs: "Choose a Chart" front door. A new top-level
/choosepage profiles a dataset and ranks the catalog by fit and communicative act (livesuggestCharts), showing each recommendation's score, reasons, and caveats with links to the chart pages. An audience selector demonstrates how the ranking shifts per reader — with the biasing familiarity/targets and their rationale shown — and surfaces governed stretch picks. Users can arrive with data and intent instead of a component name. - Docs: a11y hooks & theming serialization. The Accessibility docs now cover the preference hooks
useReducedMotion/useHighContrastand theuseNavigationSynctree↔canvas sync hook; the Theming docs now coverresolveThemePreset,themeToCSS,themeToTokens, and building custom theme objects. Custom Charts cross-links the related Cookbook recipes. - Docs: AI authoring & tooling pages. The Intelligence docs section gains four pages closing the
previously-undocumented AI surface: CLI & MCP (every
npx semiotic-aiflag and everynpx semiotic-mcptool, with agent setup), Variant Discovery & Repair (proposeVariant,evaluateVariantProposal,registerVariantDiscovery,repairChartConfig), Capability Authoring (theChartCapabilitydescriptor,registerChartCapability,registerIntent, and the intent taxonomy), and Audience Profiles (theAudienceProfileshape, suggestion bias, stretch picks, reception modality, and governance). - Playground control-drift gate.
check:docs-playground-controlschecks each playground'sselectknobs againstchartSpecs.ts: a knob bound to an enum-typed prop can no longer offer an option the chart doesn't accept. It only gates enum-typed props (and treatsmapProps-transformed pages as informational), so an enum member renamed in the API now fails the build instead of leaving a dead knob. Wired into CI,release:check, andprepublishOnly. - Prop-table drift gate.
check:docs-prop-tablesresolves each chart's prop surface fromchartSpecs.ts(ownPropsover resolvedPROP_BAGS) and AST-checks every chart page's documented prop names against it, failing if a statically-required prop is undocumented. It keeps the hand-authored tables (and their curation) verifiable against the canonical registry rather than replacing them;--verbosereports props documented but absent fromchartSpecsas a follow-up backlog. Wired into CI,release:check, andprepublishOnly. - Docs coverage gate + per-page quality bar.
check:docs-coveragederives the required chart-page set fromchartSpecs.tsand verifies every chart page renders the standard contract (ComponentMeta, a prop table, and an interactive example), with one-way burn-down maps for the three charts documented elsewhere. Wired into CI,release:check, andprepublishOnly. The previously-inconsistentConnectedScatterplot,DifferenceChart,LikertChart,OrbitDiagram,BigNumber, andProcessSankeypages were backfilled to meet the bar. - Expanded SSR/CSR visual parity matrix. Shared Playwright fixtures now cover the high-risk
SSR paths for
DifferenceChart,Heatmap,QuadrantChart, geo maps, and statistical ordinal charts, plus frame background/foreground graphics, a dark-theme SSR case, annotation callouts, progressive disclosure, lifecycle/status styling, geo annotations, and a network widget annotation browser fixture. ThesupportsSSRburn-down drops from 29 to 17 entries.
- Default tick/axis font size raised from 10px to 12px across every shipped theme, the runtime
light/dark/high-contrast defaults, and the SVG-overlay CSS-var fallbacks, so axis text clears
Chartability's 9pt/12px legibility floor out of the box.
auditAccessibility'sperceivable.small-textheuristic now reportspasson the defaults instead of warning, and theChartGroundingdocs badge leads with the verdict ("Passes a11y · N advisories") with the specific findings surfaced on hover. Override per chart viatheme.typography.tickSizeor the--semiotic-tick-font-sizeCSS variable. - Annotation note-type semantics are centralized so layout, density, diagnostics, and accessibility checks agree on what counts as a note and which notes draw connectors.
- Static SVG annotation rendering now uses the shared note renderer for labels and callouts, including
callout-circleandcallout-rect. - React hooks linting now enforces
react-hooks/rules-of-hooks, with exhaustive-deps staged as a warning while legacy dependency sites are burned down. check:capabilitiesnow rejectsserverChartConfigs.tsentries that are absent fromchartSpecs.ts, with an explicit server-only exception forSparkline.
- Hook-order regressions across loading/empty/data transitions were fixed across the HOC catalog, including the remaining Minimap validation early-return path, so charts no longer trip React's "Rendered more hooks than during the previous render" failure when async data arrives.
- Animated network charts (OrbitDiagram and other hierarchy layouts) no longer crash the tab when
given unmemoized function props. The hierarchy-ingest effect in
StreamNetworkFrameno longer re-runs on pipeline-config identity changes — so a parent passing fresh inline-arrow callbacks (nodeStyle,revolution, …) on each render no longer re-ingests data and fires asetStateper render, which previously compounded with the continuous animation frame loop until React's max-update-depth guard tripped and the page ran out of memory. Config/style changes are still applied (via the dedicatedupdateConfigeffect), and data/dimension changes still re-ingest. callout-circleandcallout-rectare now handled by the default annotation rules instead of being documented but silently skipped.- Label and callout rules now pass connector disable, opacity, and stroke-dasharray metadata through to the annotation renderer, allowing lifecycle and editorial status treatments to render as documented.
- Value-anchored annotations now have explicit regression coverage for sort, filter, and rescale reflow so labels stay bound to their data values rather than incidental array positions.
- Progressive disclosure now reveals deferred annotations in geo frames and correctly hides/reveals network HTML widget annotations.
- Colored statistical overlays are no longer misclassified as color-only annotation-to-target correspondence failures.
- Variant discovery now mirrors
suggestChartsfor non-visual audiences:evaluateVariantProposalaudits a proposal's props and folds the receivability penalty into its fit, soproposeChartVariantsno longer ranks variants that can't be received via the declared modality. - The "horizontal ranked view" variant heuristic is restricted to categorical charts that actually
expose
orientation+sort(BarChart,GroupedBarChart,StackedBarChart,DotPlot), preventing unsupported prop leakage onto Pie/Donut/Gauge/Likert/Swimlane. - The MCP
proposeChartVariantstool strips the non-serializablebuildPropsfunction from each proposal instructuredContent, keeping the JSON output transport-safe while preserving the computedprops.
semiotic/aisubpath — the AI-facing API surface as a first-class entry point. 211 KB gzip; the heuristic engine works without any LLM call, but every primitive returns LLM-friendly structured context so a model can ride on top. The entry covers four families of capability:- Recommendation.
suggestCharts(data, options?)returns ranked chart suggestions for a profiled dataset and optional intent; each suggestion carries a runnablepropsobject, an intent-score breakdown, the chart's rubric (familiarity / accuracy / precision), human-readablereasons[], andcaveats[].suggestDashboardreturns a multi-panel composite covering distinct analytical intents (withintentsMissingfor honesty about what the data can't show).suggestStretchChartsreturns the literacy-growth surface — charts the audience is unfamiliar with but the data actually supports.scoreChartandexplainCapabilityFitgive single-chart introspection.useChartSuggestionsis the React hook wrapping the same engine for live UI. - Profiling.
profileData(data)returns aChartDataProfilewith candidate fields per role (x / y / size / category / series / time), distinct counts, monotonicity, structure detection (hierarchy / network / geo).diffProfilereports schema changes between two profiles.inferIntentis a zero-dependency regex classifier that maps natural-language phrases ("why is X different?","compare these","trend over time") to one of 13 built-in intents. - Audience calibration.
AudienceProfileis a serializable per-organization config —familiarity(chart → 1-5 number map) andtargets(chart →{direction: "increase" | "decrease", weight, reason}) — that biases recommendations toward what a specific audience already knows AND toward charts the organization is trying to grow into. Three built-in personas (executivePersona,analystPersona,dataScientistPersona) ship as starting points; bias is meaningful (target weight 2 = ±2.0 on a 5-point composite score) and visible (the audience's verbatim rationale string lands onreasons[]so the policy is auditable in the UI). - Capability descriptors per chart. Every chart now ships a
<ChartName>.capability.tsnext to its TSX, declaringfamily,rubric,fits(profile) → reason | null,intentScores, optionalvariants,caveats, andbuildProps. The registry is runtime-extensible viaregisterChartCapability/unregisterChartCapabilityso consumers can add their own charts to the recommendation pool without forking the engine. - 13 built-in intents in
intents.ts:trend,compare-series,compare-categories,rank,part-to-whole,distribution,correlation,flow,hierarchy,geo,outlier-detection,composition-over-time,change-detection. Each carries a descriptor with synonyms, alias phrases, and a default scorer;registerIntentextends the taxonomy at runtime.
- Recommendation.
useChartInterrogationanduseChartFocushooks (semiotic/ai) — the headless conversational primitives.useChartInterrogationgives consumers a{ ask, history, summary, annotations, loading, error, reset }surface; the consumer brings their own LLM viaonQuery, and the hook supplies it with the profiled summary, the suggestion list, and the current focus datum as structured context. Returned annotations route directly to the chart's standardannotationsprop so the AI's response can render as callouts, threshold lines, and bands, not just text.useChartFocussubscribes to the chart's observation store and returns the current point-of-focus ({ datum, x, y, source }), with configurable event-type filtering for sticky-focus UIs.semiotic-mcpserver — Model Context Protocol server (npx semiotic-mcp) exposingrenderChart,interrogateChart,suggestCharts, anddiagnoseConfigas MCP tools so agents inside Claude Code, Cursor, Windsurf, and other MCP-aware environments can drive Semiotic directly. The interrogation tool returns the same statistical summary and AI-facing instructions the hook produces; the suggestion tool returns ranked structured content with runnable props.semiotic-aiCLI extensions —--doctorvalidates a{component, props, data}JSON spec againstvalidateProps+diagnoseConfig;--schemaemits the chart-schema JSON;--compactand--examplesproduce LLM-prompt-sized context. Pair with the MCP server for agent workflows that need both schema and validation in one place.- Three case-study blog posts —
/blog/charts-that-know-what-theyre-for(the recommendation engine and audience layer),/blog/anchored-conversations(point-anchored AI conversation viauseChartFocus+useChartInterrogation), and/blog/live-conversational-dashboard(the streaming + interrogation + annotation composition). The three together describe the product surface 3.6.0 makes possible.
- AreaChart is now a single-series chart. Multi-series area overlays are an occlusion nightmare; the capability rejects the multi-series intent scores it previously claimed and
buildPropssubselects to the leading series (largest cumulative y) when the input has 2+ groups, surfacing acaveats[]line so the reader knows they're looking at one slice. Gradient (gradientFill: true,areaOpacity: 0.55) is the baseline default.trendscore is 5 for clean single-series and 3 when subselected.LineChart.trendyields to AreaChart on single-series (4 vs AreaChart's 5) but still wins on multi-series (5 vs AreaChart's 3) because LineChart shows the whole dataset. - DifferenceChart accepts 2+ series via top-2 subselection. Previously rejected anything other than
seriesCount === 2; now picks the two series with the highest cumulative y from the input and emits acaveats[]line when subselecting from 3+ series. Same ordered-x guard the other time-series capabilities apply (xProvenance === "scatter" && !monotonicXis rejected) so the chart no longer shows up for scatter-shaped data with two categorical groups. - Scatterplot and ConnectedScatterplot prefer the canonical 2-numeric form when a sequence axis is present. With a strong-x (time or named) AND 2+ other numerics in the dataset, both charts plot the two numerics against each other (revenue × profit) instead of recapitulating a line chart on the sequence axis. ConnectedScatterplot threads the sequence as
orderAccessorso the path encodes temporal progression. ConnectedScatterplot'scorrelationintent scores 5 when canonical (vs 4 otherwise), and Scatterplot'scorrelationsteps back to 4 when canonical is available so ConnectedScatterplot wins the tiebreak — both charts fit, but the one with the temporal annotation is strictly more informative. X_FIELD_HINTrecognizes calendar-segment field names. The profiler's x-axis name regex now matchesquarter,qtr,fiscal, andweekin addition to the existingyear/month/day/date/time/timestamp. Without this, data shaped as{quarter, revenue, region}fell into scatter-fallback provenance and series detection never fired —lineBy/areaBywere silently dropped and multi-series time-series charts zigzagged across regions.
bandprop onLineChartandAreaChart(andStreamXYFrame) — asymmetric min/max envelope drawn under the lines/areas, driven by independenty0Accessor/y1Accessor. Distinct from the existingboundsAccessor(symmetric ±offset) and fromAreaChart.y0Accessor(which replaces the area baseline). Pass a singleBandConfigor an array of them for percentile fans (e.g. p25/p75 stacked on top of p10/p90). Per-series by default — one ribbon perlineBy/colorBygroup, colored from the parent line at 0.2 fillOpacity. PassperSeries: falsefor an aggregate min/max envelope across all series. Non-interactive by default (hovers pass through to the line on top); setinteractive: trueif the band should participate in hit testing. Band y0/y1 values feedyExtentauto-derivation so a tall envelope can never clip, with explicityExtentstill winning.- Tooltip enrichment now covers every interaction surface: the hovered datum carries
band: { y0, y1 }(first band) andbands: [...](all bands) on the pointer hover path, eachallSeries[i].datumin multi-mode, and the keyboard-navigation datum — one sharedenrichDatumWithBandhelper drives all three. Bounds-sourced ribbons stay decorative and are excluded from the contract. - Default tooltip surfaces band values automatically: configure
bandwithout supplying a customtooltipfunction and the default tooltip gains one row pair per band (low + high). String accessors become labels; function accessors fall back tolow/high. Custom tooltips still readdatum.band/datum.bandsdirectly. Live demo at/charts/line-chart#band.
- Tooltip enrichment now covers every interaction surface: the hovered datum carries
tickAnchor: "edges"onframeProps.axes[i]— flips the leftmost tick'stext-anchortostartand the rightmost toendon horizontal axes (anddominant-baselinetohanging/autoon vertical axes) so edge tick labels can't overflow the plot area. Default"middle"keeps existing behavior. Pairs naturally withaxisExtent: "exact": exact pins the domain to the literal data min/max, edges keeps the labels readable at those bounds. Edge detection is pixel-based, not array-index-based, so inverted y scales (the default[height, 0]) and reversed x scales (streamingarrowOfTime: "left") anchor the right edges. Closes a common wrapper-library workaround that previously routed throughxFormatreturning ReactNodes withtranslateXmath.--semiotic-tick-font-sizeand--semiotic-axis-label-font-sizeCSS variables — emitted from the canonical theme typography fields (tickSize,labelSize) alongside the existing--semiotic-tick-font-familyand--semiotic-title-font-size. BoththemeToCSS(raw string serialization) andThemeProvider(inline style) write them;themeToTokensexports them as DTCGdimensiontokens. SVG axes consume the variables viastyle={{ fontSize: "var(--semiotic-tick-font-size, 10px)" }}so a CSS-var override on any ancestor (<div style={{ "--semiotic-tick-font-size": "14px" }}>) flows down without consumers needing!important. Landmark ticks bump bycalc(... + 1px)so the +1 size stays relative to the var.data-orientattributes and per-axis class names on axis groups — each axis now renders as its own<g class="semiotic-axis semiotic-axis-{bottom|left|right|top}" data-orient="…">inside the.stream-axeswrapper. Consumers can target one axis at a time from external CSS without affecting the others:[data-orient='left'] text { font-size: 14px }works. Tick text carriesclass="semiotic-axis-tick", axis labelsclass="semiotic-axis-label", and chart titlesclass="semiotic-chart-title"for class-based targeting too.loadingContentprop on all HOCs — sibling toemptyContent. Whenloadingis true andloadingContentis set, it renders in place of the default shimmer-bar skeleton (wrapped in the same sized container so the chart slot stays reserved). Passfalseto suppress the loading UI entirely (the early-return becomes null and a consumer's outer loading state takes over). Threaded throughuseChartSetup,useNetworkChartSetup, anduseCustomChartSetup; all 47 HOCs accept it viaBaseChartProps.
website:buildparcel resolution for the Atom feed link —docs/public/index.htmlpreviously declared<link rel="alternate" href="/blog/feed.xml">with an absolute path so prerendered nested routes (e.g./charts/line-chart/) wouldn't resolve it as/charts/line-chart/blog/feed.xml. Parcel's HTML packager couldn't resolve the absolute path during build, so the website build failed. Moved the link injection intoscripts/prerender.mjsalongside the existing/llms.txtalternate injection — same strip-and-inject pattern, same absolute-URL semantics, and Parcel no longer sees the unresolvable reference in source HTML.
boundsAccessorandbandnow share one rendering primitive. Both public envelope APIs normalize to a singleresolvedRibbons: ResolvedRibbon[]array at the PipelineStore layer, then flow throughxySceneBuilders/ribbonScene.ts— one scene builder, one y-extent expansion pass, one style cascade.boundsScene.tsandbandScene.tsare deleted; the public prop surfaces stay distinct (asymmetric pairs read better asbandthan as aboundsAccessorunion return type) but the implementation is no longer duplicated. The bounds ribbon also now correctly skips datums with null/NaNy(previously the coerced+null === 0could silently render a ribbon around the implicit-zero "value" of a missing row). Thekind: "bounds" | "band"discriminator on each ribbon lets the hover handler restrictdatum.band/datum.bandsenrichment to band-sourced envelopes — bounds stays decorative-only, matching its prior contract.
DifferenceChart(XY) — two-series A/B comparison chart that fills the area between two series with a color that switches at each crossover (seriesAColorwhere A > B,seriesBColorwhere B > A). Crossover x-values are linearly interpolated so adjacent segments meet at zero-width vertices (no jagged seams). Both series can be drawn as overlay lines on top of the fill viashowLines(defaulttrue). Renders throughchartType: "mixed"with the segment groups inareaGroups— single frame, single set of scales, perfect geometric alignment between fill and overlay. Push API supported (HOC owns internal raw-data state; push triggers segment recomputation). Classic uses: temperature anomaly, forecast vs. actual, budget variance, any A/B comparison where the direction of the difference is the message. Live demos + Quick Start streaming toggle at/charts/difference-chart. New SSR config + validation map entry + chartSpecs entry → schema regenerated.axisExtentprop on all XY and ordinal HOCs —"nice"(default, current behavior) uses d3-scale's rounded tick generator;"exact"pins the first and last tick to the literal data min/max with equidistant intermediate ticks. Applies to XY x/y axes and the ordinal value (r) axis only; no-op on network/geo/hierarchy. In exact mode the pipeline ALSO skipsextentPaddingso the domain reflects the literal data bounds, not a padded version — explicittickValues,xExtent/yExtent/rExtentstill win over both modes. Three demos at/features/axes#axis-extent(temporal LineChart, Scatterplot, SwarmPlot). CentralizedequidistantTicks+ticksForModehelpers insrc/components/charts/shared/axisExtent.ts.roundedTopon SwimlaneChart — pixel radius rounds the outermost ends of each lane (left+right for horizontal, top+bottom for vertical). Middle segments stay square so adjacent pieces butt against each other; single-segment lanes round all four corners. Implemented via a newcornerRadii?: { tl, tr, br, bl }field onRectSceneNodeand shared shape utilities insrc/components/stream/renderers/cornerRadii.ts(canvas and SVG renderers share the geometry; each owns its drawing language). Live demo at/charts/swimlane-chart#rounded-corners.buildHistogramTooltiphelper — histogram-specific default tooltip forRealtimeHistogram, sibling tobuildWaterfallTooltip/buildHeatmapTooltip. Surfacesrange: <binStart>–<binEnd>,count: <total>, andcategory: <category>instead of the canonicalx:/y:shape, which produced empty strings on aggregated bin datums. Falls back to the canonical shape when a non-binned datum sneaks through.tickValueson XY axes (frameProps.axes[i].tickValues) — explicit per-axis tick positions, mirroring the ordinal frame'srTickValues. Previously the field appeared in the docs and the docs LiveExample but was silently ignored bySVGOverlay's tick computation; now it bypasses both d3's "nice" generator andaxisExtent: "exact"and wins overincludeMax. Pixel-distance filtering still drops overlapping labels. AcceptsArray<number | Date>. Pinning regression test inSVGOverlay.tickValues.test.tsx.- ProcessSankey
systemInTimeAccessor/systemOutTimeAccessorandshowLabels— optional per-edge lifecycle timestamps let a source band show mass waiting before the edge departs and a target band show mass retained after the edge arrives. The band outline now extends to those lifecycle bounds and paints per-edge gradient stubs;showLabels={false}suppresses dense band labels without dropping the legend. The docs page adds a helpdesk-ticket example plus the Process Sankey vs. classic Sankey recipe. - Docs blog —
/blognow has article and index routes, a distinct no-sidebar shell, Atom feed generation, social-card generation, route prerender metadata, and seven launch entries covering release notes, chart explainers, and case studies.
- Area canvas renderer respects CSS-variable fills —
areaCanvasRenderer.tsnow resolvesstyle.fillthrough the existingresolveCanvasFillhelper (same primitive bars use), sovar(--…)references resolve from the canvas DOM ancestor. Previously the fill path skipped this resolution while the stroke path included it — passing a CSS variable as the area fill produced no visible color (canvas silently rejects unresolved CSS vars) and the gradient path fell back to the sentinel blue regardless of the requested color. Affects any chart that emits area-type scene nodes withvar(--…)fills. - DifferenceChart accessor coercion —
xAccessor/seriesAAccessor/seriesBAccessoroutputs now flow through atoNumbercoercer that handlesDate(→getTime()ms) and numeric strings ("5"→5) before theNumber.isFinitefilter. Previously, time-series data (Date objects inxAccessor) and CSV-style numeric strings were silently dropped at the segment-algorithm guard, producing an empty chart. - DifferenceChart crossover detection across non-finite rows — the segment algorithm now tracks the last VALID point (not
sorted[i - 1]) for crossover comparison, so a NaN gap between two valid rows no longer suppresses the segment break that should sit between them. - DifferenceChart
remove()/update()synchronous return values — both methods compute results from auseRef-backed live buffer and return them synchronously at call time. The earlier pattern built results inside thesetStateupdater, which could return empty arrays under React 18+ concurrent batching if the updater was deferred or replayed. - DifferenceChart bounded push buffer — new top-level
windowSizeprop caps the raw-row buffer with FIFO eviction. Long-running streams no longer accumulate unbounded rows that the segment algorithm has to re-sort and re-segment on every render. The previousframeProps.windowSizerecommendation in the docs had no effect (the underlying frame receives static data from this chart, not streaming inputs), so the docs streaming example and Quick Start were updated to use the new prop. - ProcessSankey hover and tooltip regressions — decorative gradient stubs opt out of hit testing, filled bezier-body hits now return finite pointer coordinates for ProcessSankey's custom datum shape, and the default tooltip no longer turns short numeric domains (day/month indices) into 1970 dates.
- ProcessSankey SSR parity for lifecycle stubs —
renderChart("ProcessSankey", …)now threadssystemInTimeAccessorandsystemOutTimeAccessorinto the shared scene builder, so static SVG output uses the same lifecycle band bounds as the client HOC. - Blog metadata correctness — blog entry dates are formatted at UTC midnight so US timezones do not display the previous day, and the Atom feed link is absolute so prerendered nested routes do not point at
/charts/blog/feed.xml.
extentPaddingskipped inaxisExtent="exact"mode — bothPipelineStoreandOrdinalPipelineStorenow treatextentPaddingas 0 whenconfig.axisExtent === "exact", so the scale domain pins to the literal data min/max and the first/last ticks read as the actual bounds. Trade-off documented: glyphs at the extremes can sit at the plot edge in exact mode. Default"nice"keeps the existing padded domain.- Per-corner radius geometry centralized —
hasAnyCornerRadiusand corner-clamping logic extracted frombarCanvasRenderer.tsandSceneToSVG.tsxto a sharedcornerRadii.tsmodule. Each renderer keeps its own path-tracing primitives (arcTovs SVGA); the geometry agrees by construction. - Capability matrix regenerated and release-gated —
ai/capabilities.jsonanddocs/capabilities.mdnow index all 45 chart schemas, includingDifferenceChart;QuadrantChartis markedsupportsSSR: trueto match itsrenderChartregistration;check:capabilitiesis wired into CI,release:check, andprepublishOnly. - Blog registry drift is release-gated — new
check:blog-entrieskeepsdocs/src/blog/entries.jsandentries-meta.jsin sync, and runs duringwebsite:build, CI,release:check, andprepublishOnly. - Server-rendered QuadrantChart officially supported — the SSR config emits quadrant fills, centerlines, and labels via
svgPreRenderers, which also powers the QuadrantChart blog OG card.
useSeriesFeatureshook —forecast+anomalyprops are now first-class on AreaChart, Scatterplot, and ConnectedScatterplot (previously LineChart-only). Each consumer collapses from ~85 LOC of synthetic-key + lazy-load + state-management boilerplate to ~10 lines via the shared hook.series-features,forecast, andanomalycapability tags surface throughchartSpecs.ts/ai/capabilities.jsonfor agent discovery.useEncodingDomainhook — generic[min, max]tracker over bounded data + push-mode values, extracted from BubbleChart'ssizeBylogic. Scatterplot'ssizeBynow picks up correctly-scaled radii in push mode (previously a latent bug that returned the rawsizeByvalue as the pixel radius). String-field accessors hitting numeric-string values ("5","12") coerce cleanly instead of leaking strings into downstream math.useStreamStatushook — user-facing observer for push-API charts. Wraps a ref, interceptspush/pushMany, and exposes a reactivestatusenum ("idle"|"active"|"stale") pluslastPushTime. Surfaced viasemiotic/utilsandsemiotic/realtime. Wrap-once symbol guard prevents StrictMode double-wrap.useXYLineStylehook (Phase 2 step 5 of the HOC/Frame audit) — the line-side analogue ofuseXYPointStyle. LineChart, MultiAxisLineChart, and MinimapChart (both main + overview lines) all collapse to a single hook call covering the five-step recipe: base stroke width → color resolution → optional group-aware fill →mergeShapeStyleprimitives overlay →wrapStyleWithSelection. AresolveStroke(d, group?)override absorbs MultiAxisLineChart's per-series colorMap; unset selection-hook / primitives args keep MinimapChart's main + overview paths intact (no-ops on the wraps preserve referential identity). LineChart's forecast/anomaly segment-aware wrap stays HOC-side as a post-pass over the hook's output — the lazy-load + state-management contract has no counterpart in the other two consumers. Net ~65 LOC removed across the three HOCs. 15 unit tests pin the recipe.- Bundle-size truth source —
scripts/sync-bundle-sizes.mjsreadspackage.json#exports, gzips each*.module.min.js, and upserts marker-block sections in README.md, CLAUDE.md, andai/system-prompt.md(the synced.cursorrules/.windsurfrules/.github/copilot-instructions.md/.clinerules/docs/public/llms-full.txtfollow from CLAUDE.md).check:bundle-sizesis wired intorelease:check,prepublishOnly, and the CI workflow alongside the other doc-correctness gates, so dependency bumps that nudge a bundle past its rounded KB boundary now fail CI when the docs haven't been regenerated. Drops the stale// 200 KB gziphero comment in the README — the autogenerated table is the only source of truth. radialGeometryhelpers —sweepToAngles,valueToAngle,computeArcBoundingBoxextracted from GaugeChart into a shared module, exposed viasemiotic/utils. Custom radial-chart authors (XYCustomChart, bespoke layouts) no longer have to re-derive the gauge sweep math.- ProcessSankey temporal validators —
validateProcessSankeyandformatProcessSankeyIssueexported fromsemioticandsemiotic/network. External code (data pipelines, AI agents, server-side validators) can pre-check graphs against the same value-conservation + endpoint-resolution rules the chart enforces. regressionprop on Scatterplot, BubbleChart, ConnectedScatterplot, BarChart, DotPlot — sugar over the trend annotation. Acceptstrue| method string ("linear"|"polynomial"|"loess") | fullRegressionConfig. Ordinal charts treat categories as integer indices and project the regression line through the band scale (with linear interpolation between band centers for LOESS fractional indices).- FlowMap push API — joined the realtime-capable HOC family. The frame gained a
geo-linesvariant onuseFrameImperativeHandlepluspushLine/pushManyLines/removeLine/getLines/lineIdAccessoronGeoPipelineStore.supportsPush: truein capabilities; docs streaming demo flipped fromsetState(flows)toref.current.push(flow). - Capability matrix at
ai/capabilities.json— 44 charts indexed across 5 categories withrenderModes/supportsPush/supportsSSR/supportsLegend/supportsSelection/supportsLinkedHover/colorModel/layoutMode/specialFeaturesfields. Generated alongsidedocs/capabilities.mdbynpm run docs:capabilities; locked againstchartSpecs.tsbycheck:capabilities.suggestCharts({ capabilities })accepts push/linkedHover/ssr/selection/legend constraints and surfaces afilteredOutlist with reasons. New/features/capabilitieswebsite page renders an interactive filterable matrix.
- ProcessSankey particles unified with SankeyDiagram — particles now ride the canvas +
ParticlePoolpath. The HOC writes pre-computed cubic bezier control points onto each ribbon spec;NetworkPipelineStore's particle-pool gate broadened fromchartType === "sankey"to also acceptcustomNetworkLayout. SVG particle overlay deleted (~80 LOC, including the<circle>-per-particle allocation per frame). Prop surface aligned:showParticles+particleStyle(sameParticleStyleshape as SankeyDiagram). IndividualparticleRadius/particleDuration/particleDensity/particleMaxPerEdgeprops removed. Particles inherit source-band colors vianodeColorMapbinding through invisible color-binding scene nodes. - Ribbon geometry unified — new
src/components/geometry/ribbonGeometry.tsis the single source of truth for the M-C-L-C-Z ribbon path emission. SankeyDiagram passescp1X = xi(curvature),cp2X = xi(1-curvature)(d3-sankey S-curve); ProcessSankey passescp1X = cp2X = cx(lane-aware single-point bend). Both buildScenes.ts (SSR) and the HOCs (CSR) call the same helper. algorithm.js→algorithm.ts— last JS file in the chart source tree migrated to TypeScript with all types inlined as the canonical source (algorithm.d.tsdeleted). 7 import sites updated to drop the.jsextension; test file converted with type-annotated fixtures.getSizeclamps tosizeRange— normalized position is now clamped to[0, 1]before mapping into the size range, so a pushed point whosesizeByvalue falls outside the running domain (most common in push-mode initial state) renders at the boundary radius instead of producing an arbitrarily large pixel value.- Push-mode bezier carry-through —
NetworkPipelineStore.ingestBoundednow copies pre-computedbezierfrom raw edges onto internalRealtimeEdgerecords, validated against theBezierCacheshape (object +circular: boolean+ 4-point or non-empty-segments + finitehalfWidth). Malformed shapes are silently dropped instead of crashing the particle pipeline. - Edge value preservation — bounded ingestion now uses
Number.isFinite(numValue) ? numValue : 1instead ofNumber(v) || 1, so a legitimatevalue: 0edge survives end-to-end (e.g. suppressed-flow markers in particle pipelines). - Particle CSS variable resolution —
networkParticleRendererruns all colors throughresolveCSSColorsoparticleStyle.color="var(--semiotic-primary)"and theme-token-returningedgeColorFnresults paint correctly (canvas'sfillStylesilently rejects CSS custom properties otherwise). - Particle color resolution moved out of the renderer — functional
particleStyle.coloris now invoked ingetParticleColorwith aresolveEdgeEndpoint-resolvedRealtimeNode. Custom-layout charts (ProcessSankey) whereedge.sourceis a string id now correctly invoke the user's color function instead of falling back to a hardcoded default. - Keyboard nav skips invisible scene nodes —
extractNetworkNavPointsskipsr <= 0circles andw <= 0 || h <= 0rects (matching the canvas renderer's own skip gates). Keyboard focus on ProcessSankey now lands on a real band/ribbon instead of an off-canvas color-binding placeholder. - Bundle counts —
semiotic/aicovers 40 HOCs (XY + ordinal + network + realtime);semiotic/recipesadded to the table; full schema covers 44 charts. - Documentation refreshed — README,
CLAUDE.md,ai/system-prompt.md, and the synced.cursorrules/.windsurfrules/.github/copilot-instructions.md/.clinerules/docs/public/llms-full.txtall carry current bundle sizes, chart counts, and entry-point inventory. - Bundle-size docs corrected — earlier in this release cycle the README/CLAUDE.md/ai/system-prompt.md bundle table briefly carried inflated numbers because
npm run dist(no--production) writes non-minified output todist/*.module.min.js. The published artifacts come fromnpm run dist:prod, which terser-minifies. Bothscripts/sync-bundle-sizes.mjsandsize-limitreaddist/*.module.min.jsdirectly, so a localdistbuild silently substituted unminified bytes into the docs. The corrected numbers (xy 81 KB gz, ordinal 66 KB, network 62 KB, etc.) now reflect actual published artifacts. The bundle-size check tolerates ±3 KB build-machine variance so local↔CI minor differences don't fail without real growth.
- Fixed ProcessSankey particles not flowing when
showParticleswas toggled on (root cause:customNetworkLayoutcharts skippedfinalizeLayout, so pre-computed bezier never reachedstore.edges). - Fixed ProcessSankey particles rendering as light grey instead of inheriting source-node category color.
- Fixed
Number.isFinitecoercion inuseEncodingDomainso the running domain is always numeric.
- Added explicit extent examples and tests covering chart-level
xExtent/yExtentpass-through.
- Fixed
yExtenthandling so explicit user bounds continue to control the rendered domain instead of being overridden by envelope-derived extents. - Fixed realtime heatmap tooltip metadata so bin-center values are available and
agg="sum"tooltips report summed values.
-
tooltip="multi"hover-anywhere tooltips for LineChart, AreaChart, and StackedAreaChart — opt-in mode that surfaces a multi-series tooltip anywhere inside the rendered x span, not only withinhoverRadiusof an explicit data point. This uses the sharedStreamXYFramemulti-tooltip path, so multi hovers are cursor-anchored within the data range. Interpolation remains generous between sparse path samples but is range-bounded so explicitxExtentpadding does not clamp to first/last values. Stacked areas report per-series band height instead of cumulative stack top, and synthetic no-hit hovers carry data-spacexValue/xAccessordata for linked crosshair and observations. SSR is unchanged because tooltips are gated on pointer-drivenhoverPoint. -
useHydrationLifecyclehook — extracts the post-hydration paint pattern that was previously duplicated as a 12-lineuseEffectacross all four Stream Frames (StreamXYFrame,StreamOrdinalFrame,StreamNetworkFrame,StreamGeoFrame). Each frame now has a 7-lineuseHydrationLifecycle({ ... })call. Three things happen on every commit-after-hydration: cancel the intro animation if we just rehydrated from SSR, mark the scene dirty, paint synchronously viarenderFnRef.current(). Frame-specific cleanup (XY/Ordinal clearing the streaming adapter, Geo clearing tile cache) is supplied via thecleanupoption. Adding hydration support to a hypothetical fifth frame is now a single hook call instead of a copy-paste-and-modify exercise. -
HYDRATION.mdintegration recipe —src/components/stream/HYDRATION.mdcodifies the six-step pattern for adding hydration support to a new Stream Frame: import hooks, gate SSR branch onisServerEnvironment || (!hydrated && wasHydratingFromSSR), attachresponsiveRefon the SVG branch, wireuseHydrationLifecycle, implementcancelIntroAnimation()on the store, mark the bundleclientOnly: true. Codifying this in source (not just memory) means the next contributor doesn't have to reverse-engineer the pattern from four examples — Phase 3.5's geo backfill happened because I miscounted frame families and missed Stream Geo from Phase 3 scope; doc-as-code prevents the same kind of miss. -
SSR-vs-CSR pixel-level Playwright gate —
integration-tests/ssr-parity.spec.ts+ the newssr-parity-examples/fixture snapshot both server-rendered SVG (viarenderChart) and client-rendered canvas for the same chart matrix (LineChart, BarChart, PieChart, SankeyDiagram, Treemap). The SSR side renders into apage.setContentpayload — no fixture file needed — while the CSR side renders through the live HOC components. 30 darwin baselines committed (5 charts × 2 sides × 3 browsers). Both sides baseline independently rather than direct pixel-comparing each other (SVG and canvas rendering pipelines differ in subtle ways and won't match byte-for-byte), so any drift in either pipeline lands as a snapshot diff for a maintainer to review. -
SSR-vs-CSR structural parity test —
src/components/server/ssr-csr-parity.test.tsxexercises the two SSR code paths (renderChartfromsemiotic/serverand the in-frame SSR branch viarenderToString(<Component />)) for the same chart matrix. Asserts both paths emit the dominant data-mark primitive with counts within a 3× ratio. Catches the regression class where one pipeline silently emits zero or wildly different data marks than the other — manual-placeholder users would otherwise see a different rendering than auto-hydrating users without anyone noticing until a real consumer hit it. Surfaces an expected divergence (renderChartis bare data marks, the in-frame path includes SVGOverlay chrome) which the assertions are tuned to permit. -
Interaction-state visual snapshots — 4 new states across XY + LinkedCharts — Closes the bulk of the P1 "Interaction-State Visual Snapshots" item. Added pixel-stable Playwright snapshots for four user-driven states the existing structural tests didn't pin visually:
hoverHighlightdim (LineChart multi-series withhoverHighlight: true): pointer over series A, series B should dim. Fixture inxy-examples/index.js+ test inxy-frame.spec.ts"Interaction states" describe.- Brush selection rect (Scatterplot with
linkedBrush): drag from (0.2w, 0.2h) to (0.8w, 0.8h), capture the resulting brush rect + selection-dim state. - Legend isolate (LineChart multi-series with
legendInteraction: "isolate"): click.legend-item:firstfor series A, capture series B dim. - Linked-hover cross-highlight (existing
linked-hoverfixture incoordinated-examples/index.js): pointer over the scatter half of the LinkedCharts dashboard, capture the matching-category bars staying lit while non-matching dim. Test inbrush-selection.spec.ts"Visual snapshots" describe.
All four use the same
page.mouse.move/page.mouse.down|up/locator.click+waitForRafs(page, 4)settling pattern as the existingxy-scatter-hover-statesnapshot.maxDiffPixels: 200because pointer-driven states have more anti-aliased motion edges than default-state renders. 12 darwin baselines committed (4 interactions × 3 browsers); ran clean twice consecutively for pixel-stability. Remaining gap recorded in OUTSTANDING_WORK: click-locked crosshair (2-step click-to-lock + click-to-unlock interaction) — lower-priority follow-up since the linked-hover snapshot already covers most of the regression surface. -
HOC-level visual snapshots — animated-HOC backfill closes the 43/43 coverage matrix — Added pixel-stable default-theme Playwright snapshots for the 5 HOCs whose canvases were intentionally never visually stable: 4 realtime charts (
RealtimeHistogram,RealtimeSwarmChart,RealtimeWaterfallChart,RealtimeHeatmap) andOrbitDiagram. The technique that made it work: rather than freezing rAF or pinning a frame count (the approach the prior OUTSTANDING_WORK note pre-emptively flagged), simply pass staticdataarrays + omit the continuous-animation props (decay/pulse/transition/stalenessfor realtime,animated: falsefor Orbit). Without those props, the canvas stabilizes after initial paint and ordinarywaitForChartReady(defaultstable: true) succeeds — no rAF instrumentation needed. Fixtures live alongside the streaming-mode siblings inrealtime-examples/index.js(withseedHistData/seedWaterfallData/seedSwarmData/seedHeatmapDataconstants) andnetwork-examples/index.js. Tests are scoped to a "HOC default coverage (static)" describe in each spec so the streaming-mode tests stay separate. 15 darwin baselines committed (5 charts × 3 browsers); ran clean twice in a row to confirm pixel-stability. After this pass, every HOC inchartSpecs.ts(43/43) has a default-theme visual snapshot, closing the P1 HOC-Level Visual Snapshots item. Remaining infrastructure work — bootstrap Linux baselines from the CIplaywright-snapshotsartifact — stays open as a one-time CI/user dance. -
HOC-level visual snapshots — static-mode coverage complete (ordinal + geo backfill) — Added default-theme Playwright snapshots for the 4 remaining static-mode HOCs that didn't have any:
DotPlotandRidgelinePlotinordinal-frame.spec.ts(fixtures land inordinal-examples/index.js),FlowMapandDistanceCartogramingeo-charts.spec.ts(fixtures ingeo-examples/index.js). Each follows the XY-family pattern from the prior pass — single "HOC default coverage" describe per spec, looping the testIds through a sharedtoHaveScreenshotbody. 12 darwin baselines committed (4 charts × 3 browsers). FlowMap fixture pins 5-node + 5-edge synthetic flow data with asimpleAreasbackground; DistanceCartogram pins a 5-node hub+spokes layout withcostaccessor for the cartogram distortion. After this pass every static-mode HOC in the registry has at least one default-theme visual snapshot. 99/99 ordinal + geo Playwright tests pass on darwin (was 87 before; +12); vitest 3574/3574 unchanged. -
HOC-level visual snapshots — XY family fully covered + suite-wide color-drift re-baseline — Added default-theme Playwright snapshots for the 6 XY HOCs that didn't have any:
StackedAreaChart,ConnectedScatterplot,QuadrantChart,MultiAxisLineChart,ScatterplotMatrix,MinimapChart. Each gets a fixture entry inintegration-tests/xy-examples/index.js(deterministic small data,colorspalette where categorical) + atoHaveScreenshottest inxy-frame.spec.ts's new "HOC default coverage" describe. 18 darwin baselines committed (6 charts × 3 browsers). The Linux baselines auto-generate on the next CI push via the existing smoke-fallback workflow (HAVE_LINUXcheck in.github/workflows/node.js.yml); commit them from theplaywright-snapshotsartifact to flip the regression gate on for Linux runners.Same pass re-baselined ~50 pre-existing snapshots that had drifted by ~0.02% pixel diff — sub-perceptual color shift introduced by the 2026-04-28 d3-scale-chromatic →
colorPalettes.tsswap (linear RGB interpolation between 9-11 stops vs d3's basis-spline 256-stop LUTs; ΔE < 1 across every gradient as documented in the helper, but enough to crossmaxDiffPixels: 100on charts with anti-aliased edges over 50k+ pixels). Affected:xy-range-plot,ord-gauge-180/240/needle,histogram-stroke-{light,dark,scoped},funnel-{light,dark},tree-{light,dark},choropleth-{light,dark}under primitive-theme-matrix,waterfall-{light,dark},heatmap-{tufte,bi-tool},likert-{light,dark}under status-scale-theme-matrix. Many of these previously had chromium-darwin only — firefox-darwin and webkit-darwin baselines now committed alongside, extending the regression gate to all three darwin browsers for those tests.Remaining HOC visual coverage gaps (recorded in OUTSTANDING_WORK P1):
RealtimeHistogram/RealtimeSwarmChart/RealtimeWaterfallChart/RealtimeHeatmap(continuous animation — needs rAF freeze),DotPlot/RidgelinePlot/OrbitDiagram/FlowMap/DistanceCartogram. These ship as follow-on PRs since each requires its own fixture page entry.Total: 732/732 Playwright tests pass on darwin after re-baseline; vitest 3574/3574 unchanged.
-
Realtime HOCs auto-fit
windowSizeto bounded data — closes the Consumer Workaround Audit P0 item — newresolveRealtimeWindowSize(windowSizeProp, data)helper atsrc/components/charts/realtime/resolveWindowSize.tsis wired into all 5 realtime HOCs (RealtimeLineChart,RealtimeHistogram,RealtimeSwarmChart,RealtimeWaterfallChart,RealtimeHeatmap) where the priorwindowSize = 200destructure default lived. Resolution rule: explicit userwindowSizealways wins (a 10-point window over a 100-point archive is a legitimate "show the last 10" view); otherwise auto-fit toMath.max(data?.length ?? 0, 200). Closes the historicalwindowSize={data.length}workaround that consumers had to write to keep staticdataarrays larger than 200 points from silently truncating against the sliding-window cap during bulk ingest. Our owndocs/src/pages/theming/SemanticColorsPage.jshad the workaround on the histogram example — removed in the same diff. Streaming-only consumers see no change (the 200 floor preserves the prior default whendatais absent or empty); push API behavior is identical (the buffer can still grow viawindowMode="growing"or be consumed viaref.current.push()regardless of the resolved size). 7 round-trip tests cover the resolution rules including the explicit-zero edge case. The helper is intentionally not exported from any public sub-path — it's an HOC-internal seam, not a consumer API. Closes the Consumer Workaround Audit P0 next-work bullet ("Audit realtime chart usage forwindowSize={data.length}or other bounded-mode workarounds; consider first-class bounded mode on realtime HOCs where static-data use is common"). -
OUTSTANDING_WORK P0 cleared (second sweep) — TypeScript Surface Cleanup, Consumer Workaround Audit, and
validationMapComposition all closed; P0 is empty again. The validationMap entry was a self-marked "skip if Chart Spec Registry ships" — registry shipped, file is generated, item retires automatically. Re-open P0 only when a new release-confidence or doc-correctness gap surfaces. -
TypeScript
anycleanup pass (321 → 274; -47, ~15%) — closes the long-running OUTSTANDING_WORK item — final sweep across the highest-leverage hotspots called out in the original entry, then the item retires:colorUtils.ts—getColor(dataPoint: any, …, colorScale?: (v: any) => string)andgetSize(dataPoint: any, …)re-typed withDatumand(v: string) => string. The internaldataPoint[colorBy]access (which is genuinelyunknown-shaped because Datum is loose) now stringifies once at the seam, matching what d3 ordinal scales do internally and threadingstring[]cleanly throughscaleOrdinal<string, string>without the prioras (v: any) => stringcasts on the return type.SceneToSVG.tsx— replaced 3 unsafe arc-noop casts with a typedARC_NOOP: DefaultArcObjectconstant; replaced thedominantBaselinecast-to-anywith a cast to React's typedSVGAttributes["dominantBaseline"]union (the cast stays — it's the boundary between our free-formNetworkLabel.baselinestring and React's strict SVG-spec union — but it's no longer a type-safety bypass).hierarchyLayoutPlugin.ts— typed everyroot: anyparameter asHierarchyNode<Datum>, everyd: anyposition-setter parameter as the layout-specificHierarchyPointNode<Datum>/HierarchyRectangularNode<Datum>/HierarchyCircularNode<Datum>(matching d3's per-layout node-type contract), and the descendantnodeMapfromMap<any, RealtimeNode>toMap<HierarchyNode<Datum>, RealtimeNode>. The 3 layout-specific subtype casts at the dispatch site are the genuine boundary —layoutTypediscriminant carries the type information, but TS can't see it without an explicit cast — and they're typed-cast notany-cast.orbitLayoutPlugin.ts— same treatment for the orbit-specific layout (buildOrbitLayout(root: Datum),buildTree(parentDatum: Datum),pieGen.value((kid) => …)); typed the__orbitStatecache via the existingunknownfield onNetworkPipelineConfig; replaced the revolution-style callback's(n: any) => numberwith a structuralDepthLikeshape ({ depth?: number }) that names exactly what the function reads; dropped a forgotten null SceneDatum cast (SceneDatumis alreadyDatum | null).chordLayoutPlugin.ts— the per-nodearcDataand per-edgechordDataextension fields are now declared as typedunknown-bag fields onRealtimeNode/RealtimeEdge(the same pattern__hierarchyNode/__radiususe), so callsites narrow at the read site instead of untyped arcData access. Thearc.centroid()argument now constructs an explicitDefaultArcObjectfrom theChordGrouprather than casting; the one remaining cross-type cast (Chord→ribbonGenerator's expectedRibbonparameter, where d3's configured-radius generator never reads the missingradiusfield) is now a typedas unknown as Parameters<typeof ribbonGenerator>[0]boundary cast with a comment instead of an unsafe any cast.XYBrushOverlay.tsx+OrdinalBrushOverlay.tsx— typed the d3-brush ref asBrushBehavior<unknown>, the brush event callbacks asD3BrushEvent<unknown>, and the brush-group selection asSelection<SVGGElement, …>so all 12 unsafe brush casts ong.call(brushFn)/g.call(brushFn.move, …)resolve cleanly through d3-brush's typed call signatures. Brushes are now fully typed at the d3 boundary — noanyremains in either overlay.
This closes the OUTSTANDING_WORK "TypeScript Surface Cleanup" P0 item per the rationale called out in earlier passes: the remaining ~274 anys cluster in places where the cost-benefit doesn't pencil — vendored sankey-plus type stubs (16, intentional),
renderToStaticSVG.tsxSSR boilerplate (27, hits many type seams across the server bundle),fromVegaLite.ts(11, dynamic Vega-Lite spec shape),canvasMock.ts(10, test utility deliberately loose), plus many small per-component sites that would each save 1–3 lines for non-trivial structural work. The principle stays: prefer modeling real shapes over mechanical replacement, and accept boundary casts that are legibly named (as DefaultArcObject,as HierarchyPointNode<Datum>) over unsafe any casts. Newanyin PRs is now opportunistic-flag territory, not a tracked backlog item. -
Drop four d3 micro-deps via inlined replacements — audited every d3-* dependency in
package.jsonand removed the four where the dependency was clearly bigger than the surface we used:d3-scale-chromatic→src/components/charts/shared/colorPalettes.ts. We use 3 categorical schemes (schemeCategory10,schemeTableau10,schemeSet3) and 19 sequential/diverging interpolators (interpolate{Blues,Reds,Greens,Oranges,Purples,Greys,Viridis,Plasma,Inferno,Magma,Cividis,Turbo,RdBu,PiYG,PRGn,BrBG,RdYlBu,RdYlGn,Spectral}). Categorical schemes are byte-identical hex arrays from the original ColorBrewer/Tableau/Vega palettes. Interpolators sample the canonical palette at 9–11 stops (vs d3's 256-stop precomputed LUTs) and use linear RGB interpolation rather than d3's Catmull-Rom basis splines — the ΔE across the gradient is sub-perceptual (< 1) for every palette, and binned legend output (the dominant consumer pattern) is identical at typical N=5–9. Output format matches d3's effectiveRgb#toString()shape (#rrggbbfor opaque colors), caught in transit by an SSR snapshot test that asserted on the hex form. ~76KB unpacked saved.d3-tile→ inlined ~30 lines of slippy-tile math at the top ofGeoTileRenderer.ts. Thetile()chained-setter API was over-abstracted for a single call site; the math itself is just "given a viewport size + Mercator scale + translate, which tile triples cover it and what's the per-tile pixel offset."tileWrapis a one-line antimeridian wrap. ~20KB unpacked saved.d3-format→src/components/charts/shared/numberFormat.ts. Implements the chart-axis-relevant subset of d3's spec syntax:[,][.precision][~][type]with typesf,%,e,d,s,r,g, plus the unparseable-spec fallback toIntl.NumberFormat. 19 round-trip tests lock in parity with d3 for the format strings semiotic emits internally and the realistic spec subset consumers pass viaxFormat/yFormat/valueFormat. Dropped fill/align/sign/symbol/width and the binary/octal/hex/code-point/comma-rounded types — none of those appear in chart axis labels. ~42KB unpacked saved.d3-time-format→src/components/charts/shared/timeFormat.ts. strftime token parser backed byIntl.DateTimeFormatpinned toen-USfor%b/%B/%a/%A(matches d3-time-format's default-locale behavior —timeFormatDefaultLocaleis opt-in over there too — and keeps tick labels stable across CI runners, browser system locales, and SSR snapshot baselines). Handles%Y/%y/%m/%d/%e/%H/%I/%M/%S/%L/%p/%b/%B/%a/%A/%j/%%(every token semiotic emits internally + the realistic chart-axis subset). Pre-tokenizes the spec so per-tick formatting walks a resolved program rather than re-scanning the spec string.dayOfYear(%j) reads local calendar components and projects onto a UTC integer day count to avoid 23h/25h DST days skewing the integer division. 13 round-trip tests cover semiotic's default%b %d, %Yform and each token. Dropped%U/%W/%Z/%c/%x/%X/%f— none appear in chart axis labels. Also drops the transitived3-time(~40KB) sinced3-time-formatwas its only consumer in the tree. ~120KB unpacked saved (incl. d3-time).
Cumulative: ~258KB unpacked install-size reduction, ~40KB gzipped on the
xy/ordinal/network/geo/realtimebundles. No public API change; the four removed packages plus@types/d3-scale-chromatic/@types/d3-format/@types/d3-time-formatcome out ofpackage.json. Companion analysis identifiedd3-brush/d3-selection/d3-zoomas theoretically droppable but the bundle savings (~150KB) don't justify the touch-event correctness risk concentrated in the resulting custom pointer model — left those alone. Full suite (3560 tests across 192 files; +32 from the new format-shim suites) green; all 9 release gates clean; rebuilt bundles smoke-tested viacheck:pack. -
check:jsdoc-coveragegate — newscripts/check-jsdoc-coverage.mjsenforces a minimum agent-visible documentation surface on every HOC registered inchartSpecs.ts: a top-line one-sentence summary (TypeDoc renders this as the component blurb on/api/charts) and at least 2@exampleblocks (examples drive/api/typedoc, generated AI docs, and the MCPgetSchemaprompt context — a single example doesn't show variation). Replaces the previous baseline of "TypeDoc resolves the type" — that only proved the JSDoc parsed, not that any of it was useful, so a new HOC could ship with zero examples and nothing in CI would notice. The 2026-04-26 38/38 audit was hand-checked at the time but had no regression detection; today's gate locks it in. Initial run flagged 8 gaps the previous audit missed:AreaChartandStackedAreaChart(a single@exampleapiece — both got a second covering gradients/normalization), and all 5 realtime charts (1@exampleeach, plusRealtimeHistogramhad a multi-scenario block flattened into a single@example— split into proper separate blocks). Backwards-compat aliases (export const RealtimeHistogram = RealtimeTemporalHistogram) are followed once: the gate audits the canonical declaration so the alias's intentionally-minimal@deprecatedblock stays honest. Wired intorelease:check,prepublishOnly, and the CI workflow alongsidecheck:test-quality. Closes the API Reference Documentation P0 next-work bullet. -
check:ai-examples-coveragegate — newscripts/check-ai-examples-coverage.mjscatches drift inai/examples.md, the canonical copy-paste reference that ships in the npm tarball. Two failure modes: (1) stale chart references — a chart was renamed or removed inchartSpecs.tsbut its section inexamples.mdsurvived, so agents that follow the example produce code that fails type-check (heuristic identifier match against the registry, with an explicit allowlist for non-chart surface APIs likeThemeProvider/LinkedCharts/Tooltipso they don't false-positive); (2) coverage gaps — a renderable chart was added tochartSpecs.tsbutexamples.mdwas never updated, so MCP /--doctoragents that read the file as the canonical example reference can't find a starting point. The 22-chart copy-paste backlog at gate-shipping time (SwarmPlot/BoxPlot/RidgelinePlot/DotPlot/PieChart/DonutChart/GaugeChart/FunnelChart/SwimlaneChart/LikertChart/BubbleChart/QuadrantChart/MultiAxisLineChart/CandlestickChart/ScatterplotMatrix/MinimapChart/ChoroplethMap/ProportionalSymbolMap/FlowMap/DistanceCartogram/RealtimeSwarmChart/RealtimeWaterfallChart) is captured in a one-wayCOVERAGE_BASELINEset inline in the script — mirrorscheck:test-quality's burn-down approach: any baseline name that becomes covered in a future PR must be removed from the set in the same diff (otherwise a regression that drops the example would silently pass), and adding a NEW name to the baseline requires diff justification. The HOC's source-level@exampleblocks are still enforced bycheck:jsdoc-coverage, and MCP /--doctoragents still discover each chart throughchartSpecs.ts+getSchema, so the baseline charts aren't agent-invisible — just narrative-light in the canonical copy-paste file. Wired intorelease:check,prepublishOnly, and the CI workflow alongsidecheck:ai-contracts. Closes the AI Surface Behavior Contracts P0 next-work bullet (the "regenerate examples from runtime fixtures and diff" half — the rule-section regeneration half was already covered bycheck:ai-contracts). -
OUTSTANDING_WORK P0 cleared — all four previously-tracked P0 items are closed (API Reference Documentation, Chart Spec Registry, AI Surface Behavior Contracts, Test Quality Gate). The doc now points at the gates that hold the line for each, so future drift surfaces in CI rather than as backlog text. Re-open P0 only when a new release-confidence or doc-correctness gap surfaces — it should be the place that catches "we haven't written the gate yet," not "we keep meaning to come back to this."
-
Canvas render-helper module (
canvasRenderHelpers.ts) — extracted four primitives that recurred acrossbar/area/line/pointCanvasRenderer.tsin byte-identical form:resolveCurveFactory(curve)(the d3-shape token switch was duplicated identically betweenareaandline; adding a curve token previously required two lockstep edits),resolveCanvasFill(ctx, fill, fallback)(replaces the(typeof X === "string" ? resolveCSSColor(ctx, X) : X) || fallbackform that appeared 5+ times across the four renderers and silently fell back to#000000when consumers passed CSS-variable strings),buildLinearFillGradient(ctx, fillGradient, baseFill, x0, y0, x1, y1)(replacesbarCanvasRenderer'sbuildBarGradientandareaCanvasRenderer's inline gradient block — both implemented the samecolorStops/topOpacitytwo-shape switch with the same offset clamping and the sameparseCanvasColoropacity-form normalization;baralready filtered NaN offsets before the 2-stop minimum check,areafiltered them inline and could silently render a 1-stop transparent fill — the helper unifies onbar's stricter behavior, with the renderer falling back to flat fill when the helper returnsnull), andbuildColorStopGradient(ctx, strokeGradient, x0, y0, x1, y1)(replaces the stroke-side colorStops gradient construction inarea's top-stroke branch andline's stroke branch). Renderer-specific path tracing (rounded-corner bar paths, area decay strips, line threshold-color crossings, areatraceAreaPathwith curve-vs-linear branching) stays in the renderer that owns the mark — this is an extraction of mechanical seam boilerplate, not an abstraction over what each mark draws. Net renderer surface: 784 → 686 lines (-98), with 163 lines of helpers added (net +65, but every duplication is gone — adding a new curve token, gradient form, or fill resolution case is one edit instead of four). NewcanvasRenderHelpers.test.ts(11 tests) locks in null-return semantics for pathological inputs (1-stop colorStops, NaN offsets), fallback behavior on null/undefined fills, and the linear-fallback sentinel. Companion P3 plan for afindNearestSceneNodehit-tester factory was audited and skipped — only XY + Ordinal share the quadtree-fast-path + closest-wins shape (~10 line overlap), Network and Geo have genuinely different two-loop / three-phase structures, and the per-mark hit functions that make up the bulk of each tester don't share. Full suite (3515 tests) green;check:test-qualitybaseline (156) unchanged. -
setupCanvasMockadoption sweep — four rAF stubbing flavors + leak-tight cleanup —setupCanvasMocknow acceptsstubRaf: boolean | "noop" | "microtask"so the four test-side rAF cadences live in one place:true(default, synchronous fire) for assertions that just want a paint,falsefor force-simulation specs that need jsdom's setTimeout cadence,"noop"for "observe initial mount-time state" regression suites that would recurse under sync fire (the per-frame*Style → pipelineConfigregression specs inStreamGeoFrame.test.tsxandStreamNetworkFrame.test.tsxnow use this), and"microtask"for tests where sync fire would recursescheduleRenderbut jsdom's setTimeout latency is too costly (the StrictMode HOC suite). Migration cleared the last three test files reimplementing canvas + Path2D + rAF/cAF spies inline (StreamGeoFrame.test.tsx,StreamNetworkFrame.test.tsx,StrictMode.test.tsx);StrictMode.test.tsx's previousteardownMocksonly restored rAF/cAF and leakedgetContext+Path2Dinto later test files via the sharedHTMLCanvasElement.prototypeandglobalThis, which the helper's symmetric capture-then-restore now closes. Sync-fire flavor returns rAF id0deliberately:useFrame.scheduleRendertreatsrafRef.currentas a truthy "pending" flag (if (rafRef.current) return), and the assignment sequencerafRef.current = requestAnimationFrame(cb)lets a non-zero return overwrite the renderer's ownrafRef.current = 0reset and silently coalesce the nextscheduleRenderinto a phantom pending rAF. Caught during the migration when the previously-inlined sync stubs inDotPlot.streaming-order/LikertChart.streaming-order/StreamOrdinalFrame/StreamXYFramepush-API specs started returninggetScales() === undefinedafter the second push; the docstring on the sync branch now spells out the invariant so a future "let's number the ids properly" change won't regress it.noopandmicrotaskflavors return monotonically-increasing ids because their callers never relied on the truthy-flag invariant. The pairedcancelAnimationFramemock now honors cancellation for the"microtask"flavor (tracks ids in acancelledset; the deferred callback no-ops on fire) so production cleanup paths —useFrameunmount,DataSourceAdapterchunk timers,MinimapChartpolling — don't leak into post-unmount state updates or runaway loops in StrictMode-style tests; sync-fire and noop flavors don't need it (already-fired and never-fires respectively). Full suite (3515 tests across 189 files) green;check:test-qualitybaseline (156 mount-only candidates) unchanged. -
Shared AI/MCP component metadata —
ai/componentMetadata.cjsis now the shared source for component category, import, renderability, and registry metadata across the CLI, MCP resources, and surface-parity checks.check:surfaceis wired into release gates so schema,semiotic/ai, MCP renderability, and server-renderer support cannot drift silently. -
Shared chart recommendation engine —
ai/chartSuggestions.cjspowers both MCPsuggestChartandnpx semiotic-ai --suggest, with recommendations for network, hierarchy, geographic, temporal, categorical, and magnitude data shapes. The CLI supports stdin on all platforms via fd0. -
MCP protocol smoke coverage —
src/__tests__/scenarios/mcp-protocol.test.tsexercises stdio JSON-RPC and Streamable HTTP initialization/tools-list flows, including robust parsing for JSON and SSE responses. -
API docs extraction coverage —
api-docs-extraction.test.jslocks down TypeDoc re-export resolution, props alias handling, function-signature formatting, examples, inherited-prop labels, and component summaries. -
Docs route smoke check —
check:docs-routesvalidates prerendered homepage/API routes, route metadata, sitemap entries, and generated API JSON assets;website:buildnow runs it after prerender. -
AI behavior contracts —
ai/behaviorContracts.cjsis now the structured source for agent-visible semantic rules that schema parity cannot express: categorical color precedence, required prop combinations, push/ref behavior, ID-accessor mutation requirements, and renderChart/static-data boundaries.semiotic-ai --doctorand MCPdiagnoseConfignow acceptusageMode: "static" | "push"so static/renderChart configs still require data while ref-push React HOCs can intentionally omit it. The MCPsemiotic://behavior-contractsresource and generated AI docs consume the same rule metadata;check:ai-contractsis wired into release gates. -
Test quality gate —
check:test-qualitybaselines existing frame/canvas mount-only assertion candidates and fails when new candidates are introduced without updating the baseline. The release and prepublish gates now run it so new tests must prefer semantic assertions against scene summaries, rendered output, callbacks, or user-visible behavior. -
Streaming legend frame-domain subscriptions —
StreamOrdinalFrameandStreamXYFrameexposelegendCategoryAccessorandonCategoriesChange; pushed legends now populate, shrink, relabel, and clear after insert/remove/update/clear.LinkedChartshas a live category registry so unified legends and shared category colors update from child chart domains. -
ThemeProvider first-render coverage — new tests prove preset/object themes, CSS custom properties, prop changes, and forced-colors initialization are visible to children on the first render.
-
PipelineStore config-only cache regressions — cache tests now cover scene rebuilds from
themeSemantic.primary,themeSequential, andbarColorschanges without data ingest. -
HOC JSDoc coverage — every public HOC (38/38) now ships with at least 2
@exampleblocks, a top-line summary that cross-references sibling charts via{@link}, and@defaultannotations on the most-used optional props. Previously only 5 HOCs had any examples; the rest relied on schema summaries. Format is consistent across families: simple usage → encoded variants → push-API or advanced cases. Surfaced through TypeDoc (/api/typedoc) and TypeScript hover-help. -
streamProps construction helpers —
src/components/charts/shared/streamPropsHelpers.tsexposes three pure helpers that replace the spreads recurring in every XY/ordinal HOC'sstreamProps = { ... }literal:buildBaseMetadataProps(the...(title && { title })chain acrosstitle/description/summary/accessibleTable/className/animate, with per-field truthy-vs-defined gates that match the inline form),buildTooltipProps(thetooltip === false ? () => null : (normalizeTooltip(tooltip) || defaultTooltipContent)ternary), andbuildCustomBehaviorProps(the conditionalcustomHoverBehavior/customClickBehaviorspread, with alinkedHoverInClickPredicateflag so geo /CandlestickChart-style HOCs that excludelinkedHoverfrom the click predicate get the right semantics from the same helper). 19 HOCs migrated across the four families; the 9 holdouts have non-standard variants (LineChart's 4-statetooltip === "multi"branch, ChoroplethMap's 4-statetooltip === truebranch, the geo HOCs'resolved.Xmetadata destructure, LikertChart and FunnelChart's chart-specific tooltip flow, MultiAxisLineChart's series unitization, QuadrantChart's overlay graphics) and stay inline because the helper would have to grow more knobs than the boilerplate it's saving. Net 19 files changed, 228 deletions vs 171 insertions (-57 lines on the HOC surface; the helper module adds ~135). The bigger win is centralizing the three predicates so a future change to the linked-hover-vs-click wiring rule edits one spot instead of 19. UnusednormalizeTooltipimports in the migrated files dropped during the same pass; typecheck + 3515 unit tests + chart-specs / context7 / mcp-registry / test-quality gates all green. -
AI-discoverability prep + DISCOVERABILITY.md playbook —
server.jsonat the repo root refreshed for the official MCP Registry publish flow (version + npm identifier sync'd to currentpackage.json, title/websiteUrl/registryBaseUrl fields added that the registry validator wants). README gained anmcp-name: io.github.nteract/semioticliteral string in the MCP Server section (registry validator substring-matches it as proof the npm package and the registry entry are the same artifact) and a "Where to find Semiotic for AI assistants" section linking the discovery surfaces (Context7, DeepWiki, GitMCP, MCP Registry, Smithery) plus the agent-facing files that ship inside the package (CLAUDE.md,llms.txt,llms-full.txt,ai/schema.json,ai/behaviorContracts.cjs). Newcheck:mcp-registrygate (scripts/check-mcp-registry.mjs) cross-references all three sources —server.json,package.json#mcpName/name/version, and the README literal — and fails the build if any of them drift, since drift only surfaces at publish time otherwise. Wired intorelease:check,prepublishOnly, and the CI workflow alongsidecheck:context7. New top-levelDISCOVERABILITY.mddocuments the 9 places worth listing Semiotic with copy-pasteable submission text and CLI commands; the README's discovery section is the consumer-facing summary,DISCOVERABILITY.mdis the maintenance playbook. -
Context7 manifest + freshness gate —
context7.jsonlives at the repo root and points the Context7 indexer at the agent-facing surface (CLAUDE.md,docs/public/llms-full.txt,docs/src/pages/charts,ai/) withexcludeFoldersfordist/build/node_modules/snapshot dirs, plus arulesarray distilled frombehaviorContracts.cjs(sub-path imports, push-modedatasemantics, ID-accessor requirements forremove/update, categorical color precedence, geo-import discipline, required prop combinations, server-rendering boundaries). Newcheck:context7gate (scripts/check-context7.mjs) validates JSON syntax, the 255-char-per-rule limit Context7 silently rejects on, that everyfoldersentry resolves on disk, and that the sub-path rule's import names line up withpackage.json'sexportskeys. Wired intorelease:check,prepublishOnly, and the CI workflow.behaviorContracts.cjscarries an inline maintenance note pointing edits atcontext7.jsonso content drift between the two stays visible during code review (the gate catches format drift but not semantic drift).useFrameImperativeHandle(ref, { variant, frameRef, overrides?, deps? })atsrc/components/charts/shared/useFrameImperativeHandle.ts— extracts the 7-methodRealtimeFrameHandlebridge (push/pushMany/remove/update/clear/getData/getScales) every HOC implemented inline. Three variants:"xy"(vanilla pass-through toframeRef.current),"network"(topology-walkingremoveNode/updateNode), and"geo-points"(removePoint-based with emulatedupdate). HOCs with bespoke wrappers —BubbleChart's wrapped push that tracks the streaming size domain,SankeyDiagram/ChordDiagram's edge-shapedgetData— passoverridesto selectively replace methods while keeping the variant defaults for the rest.MultiAxisLineChartandLikertChartkeep their inline handles (per-series unitization and pre-aggregation diverge enough that the helper would fight rather than help). Migrated 22 HOCs across the four families.const { width, height, enableHover, showGrid, showLegend, title, description, summary, accessibleTable, ...rest } = resolved— every HOC unpacked the post-useChartModealias bundle into a 9-to-11-line block ofconst X = resolved.Xlines. Replaced with a single destructure across 32 HOCs. Plain JS object destructuring (no helper, no API surface, no test) handles the same job, including the?? falsefallback case that ForceDirectedGraph uses (translates cleanly to the destructure-defaultshowLabels = falseform because theChartModeResultshape allows onlyboolean | undefined).
-
useChartSetupunification closed — the standing question of whetheruseChartSetupshould grow optional inputs for dual-axis (MultiAxisLineChart) and projection (geo) cases is resolved with no API change.MultiAxisLineChartalready integrates by passingcolorBy: SERIES_FIELD(a synthetic series field) and bumpingmarginDefaults.left/rightto 70 for the dual-axis layout — the unitization step that makes the chart dual-axis lives above setup, not inside it.ProportionalSymbolMap,FlowMap, andDistanceCartogramintegrate cleanly today with the existing setup surface; the d3-geo projection lives insideStreamGeoFrame, not in any HOC-reachable seam, so there is no "pre-projected coordinates" path that wants exposing.ChoroplethMapis a separate shape (sequential gradient legend driven by a value scale, not categorical) and is intentionally outsideuseChartSetup's scope. Outcome: no new optional inputs added, no new consumers in the queue — the unification work is complete. -
LineChart drops standalone
useStreamingLegend—useChartSetupowns the full legend pipeline — LineChart used to layer a separateuseStreamingLegendcall on top ofuseChartSetup: that hook re-tracked discovered categories viawrapPush/wrapPushMany, registered them with the parentLinkedChartsviauseLinkedChartCategories, and produced astreamingLegendelement +streamingMarginAdjustthat the HOC merged on top ofsetup.legend/setup.margin. After this change,useChartSetupalready does the equivalent end-to-end:useChartLegendAndMargin(called inside setup) registers the live category domain withLinkedCharts, and the synthesizedlegendColorScale+setup.legendcarry the same provider → scheme → theme → STREAMING_PALETTE precedence the marks use. LineChart now spreadssetup.legendBehaviorProps(which includeslegendCategoryAccessor+onCategoriesChangefor the frame, plus the legend slot and legend-interaction handlers) and readssetup.legend/setup.margindirectly.useStreamingLegendstays in the codebase as a low-level escape hatch for aggregator HOCs that need to intercept push calls BEFORE ingest (LikertChart re-aggregating streamed rows into level × count); its docstring now flags this as a niche case and points new code atsetup.legend/setup.margin. No behavior change in either bounded or push mode for LineChart consumers; 3507 unit tests including the dedicated push-mode legend color regression suite continue to pass. -
Test quality gate burn-down: ordinal bar family + Playwright — Two passes against the mount-only assertion baseline:
- Ordinal bar family unit tests:
BarChart.test.tsx(16 mount-only checks),StackedBarChart.test.tsx(8), andGroupedBarChart.test.tsx(1) replaced theirexpect(frame).toBeTruthy()mount-only assertions with semantic checks against the props the HOC actually forwards toStreamOrdinalFrame:chartType,data,oAccessor/rAccessor,projection,oSort(plus a sortedness check on the pre-sorted data whensortis"asc"/"desc"),barPadding,enableHover,showGrid,size,stackBy,groupBy,normalize,legend.legendGroups[0].items(distinct colors per category forcolorSchemechecks), andframePropsescape-hatch overrides. Caught and locked in a real distinction along the way:GroupedBarChartroutes throughchartType: "clusterbar", not"bar". The intentional sparse-array hardening test gets the documented// test-quality-gate: allow-mount-onlyopt-out so the gate doesn't fight a test whose whole purpose is to prove no-crash-on-sparse-input. - Playwright integration specs: 9
canvas.toBeVisible()/svg.toBeVisible()mount-only checks acrossaccessibility.spec.ts,brush-selection.spec.ts,coordinated-views.spec.ts,geo-charts.spec.ts,hoc-legend.spec.ts,realtime-charts.spec.ts, andstreaming-regression.spec.tsreplaced with semanticaria-labelregex matches (/\d+/). The data canvas'saria-labelis set bycomputeCanvasAriaLabelonly after the scene populates (e.g.scatter, 50 points), so the new check requires real paint — a regression that wires the canvas up but never draws would slip pasttoBeVisible()but trip the regex. The coordinated-views hover test now asserts a.stream-frame-tooltipelement appears after hover (proving the interaction-canvas hit-test resolved a point) instead of just checking the overlay canvas exists. - Net baseline change: 190 → 156 mount-only candidates (~18% reduction across both passes). The Playwright sweep is the higher-leverage half: those tests run in a real browser, where mount-only checks are blind to drawn-pixels regressions that semantic assertions catch.
- Ordinal bar family unit tests:
-
Sparse-array prop hardening across every ingestion path — public chart HOCs now filter
null/non-object entries from array props (data,points,nodes,edges,flows,series,areas) before any iteration. CSV/loader pipelines commonly emit[null, validRow, undefined]-shaped input, which previously crashed insideuseChartSetup,useColorScale,inferNodesFromEdges, the choropleth geometry validator, and the StreamFrame ingestion path. A new shared identity-preserving helper atsrc/components/charts/shared/sparseArray.ts#filterSparseArrayreturns the original reference when nothing is dropped (preservinguseMemocache hits in the clean case) and is wired through every ingestion seam:useChartSetupfilters itsdatainput and exposes the sanitized array assetup.data; the empty-state check filtersrawDatatoo, so[null, undefined]lands on the empty-state UI rather than rendering blank.DataSourceAdapterfilterssetBoundedData,setReplacementData,push, andpushMany. Push-moderef.pushMany([null, valid])now silently drops the null and lands the valid row instead of crashing extent reads inside the pipeline store.StreamGeoFrame.pushPoint/pushManyandStreamNetworkFrame.pushEdge/pushManyEdgesmirror the same filtering at the geo/network frame boundaries (those frames don't route throughDataSourceAdapter).- 26 HOCs replaced their
safeData = data || []with the identity-preserving filter;MultiAxisLineChart'sseriesprop, the network family'snodes/edges, andChoroplethMap's resolvedareasgot the same treatment. ForceDirectedGraph/SankeyDiagram/ChordDiagramempty-state checks route through their filtered arrays so sparse-only input triggers empty UI.ProportionalSymbolMapandFlowMapwere migrated to drop their own pre-setup filter and readsetup.datadirectly (eliminates the double-scan whenuseChartSetupwould re-filter the same array).- A new
EMPTY_ARRAYsingleton (frozen) is used as the stable push-mode default in HOCs that need an array literal; per-render churn through the sparse-filteruseMemois gone. - New regression test
src/__tests__/scenarios/sparse-array-hardening.test.tsxcovers one HOC per data-iteration shape, plus push-mode ingestion drops and empty-state routing — 17 cases, all green.
-
Push-mode legend color regression suite (XY + geo) —
src/__tests__/scenarios/push-mode-legend-colors.test.tsxexercises the legend-color synthesis precedence (CategoryColorProvider→ explicitcolorSchemearray → string scheme name →ThemeProvidercategorical → default theme) againstLineChart(xy /useStreamingLegendpath) andProportionalSymbolMap(geo /useChartSetuppath) mounted in push mode for each tier. 10 tests total. The "bare push" tier's behavior is documented inline: it resolves toLIGHT_THEME.colors.categoricalbecause the theme store seeds a non-empty default categorical palette, leavingSTREAMING_PALETTEas a defense-in-depth fallback rather than a reachable production path. Negative-anti-source assertions catch any regression that surfacesSTREAMING_PALETTEfrom a wrongly-ordered precedence chain. -
StreamGeoFrame push-mode legend category emission —
StreamGeoFramenow readslegendCategoryAccessor+onCategoriesChangefrom props and emits the live category domain after every scene rebuild, mirroringStreamXYFrame/StreamOrdinalFrame. Push-mode geo HOCs (ProportionalSymbolMap,FlowMap) now propagate their discovered categories back throughuseChartSetup'sframeCategoriesstate, so the synthesized legend renders with the correct swatches the moment data starts flowing. Previously the props were accepted on the HOC side but unread by the frame, so push-mode geo legends never populated. Frame-level coverage inStreamGeoFrame.test.tsxexercises the emission acrosspushMany/removePoint/clearso a regression in the wiring is caught at the frame level, not just through HOC scenarios. -
Chart Spec Registry — Phases 1–4 (all 43 chart families) —
src/components/charts/shared/chartSpecs.tsis the single source of truth for the prop specifications of every Semiotic HOC: 15 ordinal + 12 XY + 7 network + 4 geo + 5 realtime = 43 charts. Three pure generators inscripts/lib/chart-specs-generators.mjs(generateSchemaToolEntry,generateValidationMapEntry,generateMetadataEntry) produceai/schema.json,validationMap.ts, andcomponentMetadata.cjsentries from eachChartSpec;scripts/regenerate-schema.tsre-baselinesai/schema.jsonfrom the registry. A 130-test round-trip suite (chart-specs-round-trip.test.ts) iterates overCHART_SPECSand asserts deep structural equality on the parsed schema tool entry and validationMap entry per chart, plus componentMetadata category correctness and a top-level set-parity check thatCHART_SPECSkeys exactly match the canonical name sets inai/schema.json,validationMap.ts, andai/componentMetadata.cjs.check:chart-specsis wired into release/prepublish gates and the CI workflow. Adding a new chart is now one edit (aChartSpecentry) plus regeneration; previously it required three coordinated hand-edits across schema/validation/metadata files.
- Docs prerendering pipeline — route extraction handles nested and multiline route declarations, root and nested pages get idempotent canonical/LLM alternate/JSON-LD/noscript metadata, homepage metadata is normalized consistently, and prerender functions can be imported directly for scenario tests.
- TypeDoc API reference —
/api/typedocnow covers the full chart HOC surface, resolves re-exported props declarations instead of stopping at reference stubs, shows component summaries/examples, formats callback prop signatures, and labels inherited props from shared interfaces. - ThemeProvider initialization — the scoped ThemeStore is seeded with the resolved initial theme before children render. This removes the previous light-theme-first path for
useTheme(), chart color defaults, and CSS variables. - Canvas theme bridge —
useFrameowns theme-change invalidation from a layout-timed effect: clear CSS-var color cache, mark the frame dirty, and schedule a repaint whenever the ThemeStore theme changes. - Streaming legends and linked colors — push-mode legends now source swatch colors from the same provider/theme/color-scale path as rendered marks, so child legends and unified
LinkedChartslegends agree. - HOC shared setup unification —
LineChart,AreaChart,StackedAreaChart,QuadrantChart,ConnectedScatterplot,BubbleChart,ProportionalSymbolMap,FlowMap, andDistanceCartogramnow useuseChartSetupfor categorical color scales, legends, selection/hover/click behavior, loading/empty states, and margins, while keeping chart-specific logic (statistical overlays, gap handling, direct-label margins, line/area/stack transforms, size domains, quadrant overlays, projection, flow point→edge hover translation, and cartogram layout) local.useChartSetupitself now synthesizes a push-mode legend color scale from discovered categories using the same precedence asuseColorScale(provider → explicit scheme → theme → STREAMING_PALETTE), so legend swatches and rendered marks agree on every converted chart without each one needing to layeruseStreamingLegendseparately. - StackedAreaChart AI contract —
areaByis now marked required in the AI schema and validation map, matching the existing LLM guidance that StackedAreaChart needs an explicit grouping field for stacked series. - HOC rendering scenario tests — high-value public wrapper smoke tests now assert scene summaries, legend labels, annotation labels, and explicit empty/loading states instead of only proving that a canvas mounted.
- OUTSTANDING_WORK.md — collapsed into an active priority backlog only; completed dependency migrations, theming milestones, AI/MCP work, prerendering, streaming legend work, quadtree work, and cache/theme fixes were removed from the backlog and recorded here instead.
check:schema/check-schema-freshness.js— schema↔validation per-prop drift is now construction-guaranteed by the Chart Spec Registry round-trip (check:chart-specs). The CLAUDE.md component-coverage cross-check that lived insidecheck:schemais preserved as a slim, focused gate atscripts/check-claude-md-coverage.js(npm run check:claude-md-coverage).check:surfaceno longer asserts schema↔validation name parity (also covered by registry round-trip) and instead focuses onsemiotic/aiexports, MCP renderable registry entries, AI component metadata, and server renderChart configs. CI workflow and release/prepublish gate scripts are updated accordingly.
- Production builds preserve
"use client"after terser —npm run dist:prodwas failing the post-build directive-placement gate: every client bundle's minified output came out missing the directive. Root cause:useClientPlugin.renderChunkwas prepending"use client";to the output, then terser'srenderChunkran AFTER it and silently dropped the top-level string expression through its parse → compress → emit pipeline (default terser doesn't preserve"use client"the way it preserves"use strict"). Fix: splituseClientPlugin's registration so itstransformhook still scans modules during the parse phase but itsrenderChunkis appended LAST in the plugin array — after terser. The directive is the absolute final write to the chunk; terser never sees it. Caught by the inverse-direction post-build assertion that gates client-only bundles must carry the directive (added in the previous round); without that gate the regression would have shipped silently. PipelineStore.ingestis idempotent on identical bounded data refs — the SSR branch in every Stream Frame callsstore.ingest({ inserts: data, bounded: true })from inside render. React StrictMode renders components twice in dev mode, so the second render's ingest would clear the buffer and re-fill it from the same data array — wasted work, and a real correctness risk if a future change made the second pass non-trivially different. Now: the first call records_lastBoundedInsertsRef; a subsequent call with the same reference returnsfalseimmediately as a no-op.clear()resets the dedupe ref so post-clear ingests re-run normally. Streaming (non-bounded) ingests are unaffected — each push is meaningful and shouldn't dedupe.- Build's
"use client"detection accepts leading comments — the rollup plugin usedcode.startsWith('"use client"'), which silently missed every source file that opened with a JSDoc block (StreamOrdinalFrame, useFrame, useHydration, useChartSetup, several others — 7 files total). Bundles still ended up directive-tagged in practice because each chunk pulled in some other module that opened with the bare directive, but a future change isolating one of those JSDoc-headered files into its own chunk would silently lose the directive on its bundle. Replaced withhasLeadingUseClientDirective()that skips leading whitespace + line comments + block comments before checking — the same way Next.js / React's parsers interpret the directive. Added: (1) a 14-case unit test covering positive (with/without leading comments, single vs double quotes) and negative (directive in a string, after an import, no directive at all) inputs; (2) a complementary post-build assertion thatclientOnly: truebundles MUST carry the directive on output (catches the inverse regression where a detection bug silently drops the tag from a chart-family bundle, which would crash Next.js Server Components importing from that sub-path with browser-API errors at runtime). cancelIntroAnimationnow clears per-node_introClipFraction—synthesizeIntroPositionssets_introClipFraction = 0on line and area scene nodes (the canvas renderers consume it directly to clip the path from the left). The earlier Phase 4cancelIntroAnimationclearedprevPositionMap/prevPathMap/activeTransitionbut left the per-node flag, which would silently produce a fully-clipped (blank) line / area chart on the first canvas paint after SSR hydration. Now walks the scene and resets the flag toundefinedfor line / area nodes. Regression test inPipelineStore.cancelIntro.test.tsasserts every line node's clipFrac is undefined after cancel.- CSR mounts skip the wasted SVG render — the SSR/hydration gate on every Stream Frame was
if (isServerEnvironment || !hydrated), which forced a full SVG render on the first render of every client-side mount, even when there was no SSR HTML to match. Tightened toif (isServerEnvironment || (!hydrated && wasHydratingFromSSR))— the SSR signal we already use for intro-animation cancellation now also gates this branch. CSR mounts skip the SVG render entirely and go straight to canvas; SSR rehydration still gets the byte-identical SVG output it needs. Net effect: one less synchronous scene-build + SVG conversion per client-only chart mount, with no behavior change for SSR consumers. createStoreno longer callscreateContextat module load — even with the"use client"directive removed fromsemiotic/server, the bundle still pulled increateStore.tsx, which calledReact.createContexteagerly when each store factory ran. React Server Components ship a build ofreactthat omitscreateContextentirely, so importingsemiotic/serverfrom a Server Component threw(0, p.createContext) is not a functionbeforerenderChartever ran. RefactoredcreateStoreto defer thecreateContextcall untilProvider/useSelectoractually executes — both of which only run on the client. Added anRSC import safetyregression test that mocksreactto throw oncreateContextand asserts the factory call doesn't trip it. Caught by the SSR demo'smanual-placeholderroute at request time.ForceDirectedGraphacceptsnodeIdAccessor(camelCase) — the historical prop name wasnodeIDAccessor(uppercase ID), which was inconsistent with the rest of the network HOCs (SankeyDiagram,ChordDiagram,TreeDiagram,OrbitDiagramall use camelCasenodeIdAccessor). The casing inconsistency surfaced during the SSR demo's verification matrix. Both prop names are accepted now;nodeIdAccessoris canonical andnodeIDAccessoris a@deprecatedalias slated for removal in 4.0. When both are passed,nodeIdAccessorwins. Codemod follow-up tracked for the externalsemiotic-codemodrepo: aforce-directed-graph-node-idtransform that renames the JSX attribute on existing<ForceDirectedGraph>usages.semiotic/serverno longer carries"use client"— the build's directive plugin tagged any chunk that contained a transitively-imported client module, which leaked the directive into the server-only entry point. CallingrenderChart(or any other server export) from a Next.js Server Component threw at runtime: "Attempted to call X() from the server but X is on the client." The fix is a per-bundleserverOnlyflag on the build config that opts the server bundle out of the directive unconditionally, plus a post-build assertion (assertDirectivePlacement) that fails the build if the regression ever returns. Caught by an SSR demo that exercised the manual-placeholder pattern; the auto-hydrating XY/ordinal/network HOCs were unaffected because they don't import fromsemiotic/server.- HOC JSDoc accuracy pass — 13 doc inaccuracies caught and corrected against source:
ForceDirectedGraphnodeSize/nodeSizeRangedefaults, push-mode opt-in fornodes/edges, and the fabricatedframeProps.initialPositions;PieChart.startAngleunits (degrees, not radians) and the matching example value;PieChart.valueAccessoraggregation byMath.absfor negatives;QuadrantChartexample commentary;ChoroplethMap.areasshape (Feature[], notFeatureCollection) and the asyncresolveReferenceGeographyexample pattern;MinimapChartexamples that referenced non-existentchart,minimapHeight,initialExtent,onBrushChangeprops;ScatterplotMatrixexample that wrapped the matrix in an outer<LinkedCharts>even though the component already creates its own provider internally;ProportionalSymbolMappush example missing theidfield required bypointIdAccessor. All examples now match the real prop surface and copy-paste cleanly. - AI chart suggestions copy/paste correctness — ForceDirectedGraph suggestions no longer imply nodes are optional, hierarchy suggestions reference the provided data shape, heatmap recommendations require two dimensions plus a value, sample inputs are capped, and generated JSX string props are escaped safely with
JSON.stringify. - MCP HTTP test robustness — startup retries wait for child-process exit before rebinding; response parsing handles plain JSON and SSE data events instead of assuming a single
data:line. - Prerender structured-data duplication — injected JSON-LD now carries the same
data-jsonld="semiotic"marker the client hook checks, preventing duplicate runtime scripts. De-duplication is scoped to Semiotic-owned JSON-LD rather than all SoftwareApplication schemas. - Prerender metadata consistency — canonical and
og:urlmetadata are normalized together, including the homepage URL shape. - SSR alignment test isolation —
ssr-alignment.test.tschecks a temporary SceneToSVG copy instead of mutating tracked source files during parallel Vitest runs. - GaugeChart LLM docs — machine-readable docs now correctly list
thresholdsas optional, matching the TypeScript API. validatePropsdata shape handling — the"none"data shape is handled explicitly so no-data components do not accidentally fall through realtime validation assumptions.
- 3324 unit tests pass after the latest backlog cleanup and cache regression additions.
gradientFillonBarChart— same API asAreaChart.gradientFill:truefor a default 80%→5% opacity fade on the resolved bar color,{ topOpacity, bottomOpacity }for explicit opacity stops, or{ colorStops: [{offset, color}, ...] }for arbitrary multi-color gradients. Direction always runs from each bar's tip (opposite the baseline) toward its base, so positive/negative and vertical/horizontal orientations all do the right thing. The scene builder now tags every rect withroundedEdgeunconditionally (previously only whenroundedTop > 0) so gradient direction resolves without requiring rounded corners. NewbuildBarGradienthelper inbarCanvasRenderer.tsbuilds theCanvasGradientper bar;StackedBarChart/GroupedBarChartget it for free via the shared scene + renderer.- SVG / SSR rendering for bar gradients —
ordinalSceneNodeToSVGinSceneToSVG.tsxemits<defs><linearGradient>+fill="url(#id)"for rect nodes carryingfillGradient. Works through bothrenderToStaticSVG(server) andanimatedGif(GIF export) automatically since both delegate to the shared scene-to-SVG converter. UsesgradientUnits="userSpaceOnUse"with absolute coords so each bar's gradient tracks its own rect. NewsafeSvgIdhelper coerces category names containing spaces/punctuation to a legal SVG id charset before embedding them in the gradient'sidandurl(#...)reference. renderers/colorUtils.ts— sharedparseCanvasColor(ctx, color)used by bothbarCanvasRendererandareaCanvasRenderer. Resolves any valid CSS color (named like"steelblue",hsl(),rgb(), hex short/long) to an[r, g, b]tuple via actx.fillStyleround-trip that the browser normalizes. Uses a sentinel-probe pattern so silently-rejected invalid colors (canvas ignores them and leavesfillStyleat the previous value) fall back safely instead of being mis-parsed as the prior color. Unified the two previously-duplicated localparseColorhelpers. Bar-gradient dev demos on/charts/bar-chartcover three shapes: opacity fade, multi-color stops, and horizontal bars.
- JSX transform flipped to the automatic runtime.
tsconfig.jsonandtsconfig.mcp.jsonnow use"jsx": "react-jsx"instead of the classic"jsx": "react". JSX compiles to imports fromreact/jsx-runtimerather thanReact.createElement(...), matching React 17+ guidance and removing the "outdated JSX transform" runtime warning that was peppering test output. ESLint flat config layersreactPlugin.configs["jsx-runtime"].ruleson top ofrecommendedto disablereact/react-in-jsx-scopeandreact/jsx-uses-react(both obsolete under the new runtime). 17 test files lost their now-redundantimport React from "react"imports; the 51 files that referenceReact.Xtypes kept theirs. Rollup'sexternalpredicate inscripts/build.mjsextended to cover thereact/jsx-runtimeandreact/jsx-dev-runtimesubpaths alongside the existingreact-dom/serverentry (auto-external marks package roots external but not subpaths). - Empty legends no longer reserve margin.
useChartLegendAndMarginpreviously returned a truthy legend object withlegendGroups: [{ items: [], label: "" }]when mounted with nodata(push-API pattern) pluscolorBy— that reserved 110px of right margin and rendered only the legend's header neatline. Zero-item legends now resolve toundefined, so no margin is reserved and the chart uses the full width until categories arrive. Surfaced by the "Update: Bar Chart" demo on/features/push-api. - Dependency hygiene.
react-routerandmarked-gfm-heading-idmoved fromdependencies→devDependencies(both are docs-only — never imported by anything undersrc/). Removedtslibas an explicit dev dep (noimportHelpers: truein either tsconfig, so TypeScript never emits tslib references).@testing-library/domnow an explicitdevDependency(was a transitive peer of@testing-library/react@16+; missing onnpm install --legacy-peer-depsand broke 79 test files on the publish workflow). Net: Cloud consumers installing the library get 19 runtime deps instead of the 22 they were getting before.grep -c "react-router\|marked-gfm-heading-id\|tslib" dist/semiotic.module.min.jsreturns 0.
- Publish workflow OOM in
prepublishOnly. Four of the fivenode scripts/build.mjsinvocations across package.json scripts were missing the--max-old-space-size=8192flag thatdistanddist:prodcarry; the one that ran on publish hit Node's 4GB default heap ceiling and died during rollup minification with a mark-compact GC failure (exit 134). All five invocations (build:analyze,build:prod,pretest:dist,release:check,prepublishOnly) now share the heap bump, and the last two route throughnpm run dist:prodso there's one source of truth for how to invoke the build. parseCanvasColordetects silently-rejected invalid colors. The browser ignores invalid CSS color assignments without throwing —fillStylestays at whatever was set before. Without a probe, feeding an invalid color to the parser would mis-read the previous color as the caller's input. Added a sentinel-set-first pattern: assign#010203before the user's color, then compare; if the sentinel is still there (and the input wasn't literally the sentinel), return the fallback tuple. Also guards the non-stringfillStylecase (CanvasGradient/CanvasPattern).buildBarGradient/buildRectSVGGradient: < 2 valid stops falls back to solid. Both previously checkedfg.colorStops.length >= 2but then filtered outNaNoffsets inside the loop — a configured list of 2 stops with one NaN produced a single-stop gradient (flat color) or, on the SVG path, emittedoffset="NaN"which invalidates the whole gradient. Now both filter for finite offsets first, clamp, then require ≥2 valid survivors before building.- Bar renderer preserves
CanvasPatternfills whenfillGradientis set. The opacity branch ofbuildBarGradientused a hardcoded"#4e79a7"fallback when the resolved fill wasn't a string, silently replacing aCanvasPatternfill with a grey gradient. Now guards: if the resolved fill isn't a string, the gradient is skipped entirely and the pattern fill renders as intended. - Candlestick transition exit stubs preserve bodyWidth.
snapshotPositionsnow capturesnode.bodyWidthintoprev.wso the candlestick exit node reads the pre-transition width instead of falling through to the 6px default on the final frame.getNodeIdentityprefers an existing_transitionKeyover the datum-derived key so exit stubs stay stable across overlapping transitions (affects all exit-node types, not just candlestick). CandlestickChartOHLC validation gap. When the user asks for OHLC mode but the data is missingopen/closefields,warnMissingFieldandvalidateArrayDatanow cover all four accessors. Previously the scene builder silently dropped bars and the chart rendered blank with no feedback.
noUnusedLocals/ ESLint cleanup pass. 17 test files undersrc/components/andsrc/__tests__/scenarios/had bareimport React from "react"imports that existed only to satisfy the classic JSX transform. Removed with the transform flip. The 51 files that useReact.SomeType/React.ComponentProps<>kept their imports.- 3246 unit tests pass (was 3216 in 3.4.1). Net adds: 9
buildBarGradienttests + 6 SVGgradientFilltests inSceneToSVG.test.tsx+ 1BarChartempty-legend suppression test + 8parseCanvasColortests covering hex normalization, named colors, invalid-with-string-prev, invalid-with-non-string-prev, and the sentinel-self edge case + 2ordinalSceneBuilderstests for roundedEdge tagging and fillGradient attachment + 2 NaN-stop fallback tests.
/charts/bar-chart— new "Gradient Fill" section under Examples with three live demos (default opacity fade, multi-color colorStops, horizontal direction flip) and a props-table row. Generator seeds match the page's existing deterministic pattern (noMath.random()at module scope).- OUTSTANDING_WORK.md — added "Legend auto-population from pushed categories [YELLOW]" under the Push API section. Captures the 3.4.2 short-circuit (empty legends don't reserve margin), the real design (an
onCategoriesChangecallback on StreamOrdinalFrame + StreamXYFrame threading throughuseChartSetupstate intouseChartLegendAndMargin's existingcategoriesparam), known landmines (first-ingest timing, sorted-array dedupe, XY'sgroupAccessorvariant,LinkedCharts+CategoryColorProviderinteraction), and an estimated ~150 LOC surface across 5 files.
CandlestickChartHOC (semiotic/xy) — wrapschartType="candlestick"with the same mode-aware, animated, push-API conventions as the other XY HOCs. Required:highAccessor,lowAccessor. Optional:openAccessor+closeAccessor— omit both and the chart degrades to a range/dumbbell visualization (endpoint dots + wick, no body). Honorsmode="primary" | "context" | "sparkline":scalePaddingscales from width (12 / 10 / 3) to keep leftmost/rightmost bars from clipping,extentPaddingdrops to 2% at widths ≤200 so the y-domain isn't padded into uselessness, and sparkline zeroestop/bottommargin (axes are stripped, so the 2px defaults were dead space). Full docs page at/charts/candlestick-chartwith static ↔ streaming toggle, range-chart demo, compact-mode grid for OHLC + Range, and an Animation section demoing data-morph (seeded regenerate button) and a sliding push/remove window.- Candlestick animation support — the transition pipeline in
pipelineTransitions.tsgained full enter/update/exit branches fortype: "candlestick"nodes. Bars matching by x-identity smoothly interpolate all four y-coords (openY,closeY,highY,lowY) when data updates; new bars fade in; scrolled-off bars fade out with a held-in-place gray stub. Snapshot carriesbodyWidthtoo so exits don't jump to a 6px fallback on the final frame. Renderer now compositesdecayOpacity * style.opacityso decay and transition fades stack.getNodeIdentityprefers an existing_transitionKeyover the datum-derived key so exit stubs stay stable across overlapping transitions (fixes a latent reshuffle risk for all exit-node types, not just candlestick). - Server-side rendering for candlestick —
renderChart("CandlestickChart", ...)works through a new entry inserverChartConfigs.ts. Passthrough config: HOC-level accessors map 1:1 to frame-level ones;openAccessor/closeAccessorare forwarded without defaults soPipelineStorecan auto-detect range mode. compactMode: booleanonuseChartModereturn — the context∨sparkline union now lives on the hook instead of being recomputed in each HOC.GaugeChartconsumes it (replaces the localmodeIsContext || modeIsSparklineflag and collapses three conditional-render branches into one).candlestick-range-*visual regression fixtures — 3 new modes × 3 browsers = 9 baselines added to the chart-modes matrix specifically covering range-mode rendering (the path that motivated the dot-radius cap).
- Candlestick sparkline rendering — three rendering changes converge to make high/low lines actually visible at 120×24:
- Wick is drawn on top of the body at
layout.height < 60with a 2px minimum stroke. At sparkline heights the protrusion above/below a tiny body is often <2px and lands on subpixel boundaries, antialiasing to ~11% alpha (invisible). Drawing the wick last shows the full high-low range as a continuous line through the body. - Range-mode dot radius scales with
bodyWidth/2and caps atlayout.height * 0.12(was hardcodedmax(wickWidth * 2, 4)— ≥4px always, marble-sized on a 24px row). Scales up for primary/context. - Scene builder now computes the same gap-derived
bodyWidthin OHLC and range modes so the renderer has a scale-aware basis for dot sizing.
- Wick is drawn on top of the body at
GaugeChartneedle formula simplification —innerRadius > 20 ? innerRadius - 8 : radius - 1. TheMath.max(1, ...)/Math.max(2, ...)floors in the previous formula were dead: the guarded expression is always well above the floor in either branch.- Type safety sweep — ~216
anytypes eliminated across the codebase. Scene-node interfaces, scale helpers, hook returns, and accessor resolution gained concrete types. No behavior change; catches more regressions at compile time. - Major dependency updates —
@playwright/test+playwright-chromium^1.17.1→^1.59.1(regenerated 9 darwin baselines for chromium font-rendering shifts on label-heavy charts),vitest+@vitest/coverage-v8+@vitest/ui^4.0.18→^4.1.4,typedoc^0.28.17→^0.28.19,@axe-core/playwright^4.11.1→^4.11.2,@modelcontextprotocol/sdk1.27.1 → 1.29.0,@types/nodealigned to Node 22.19.17 (matches the Volta-pinned runtime)..node-versioncorrected from18→22.22.1.
RealtimeHistogram.showLegenddead pass-through —showLegendwas being fed intouseChartModebut the resolved value was never consumed (the HOC doesn't construct alegendprop for StreamXYFrame). Removed the feed-in and updated the comment to explain the absence.arrowOfTimewrongly exposed onStreamOrdinalFrame— removed. The prop only applies to XY time-series layouts; its presence on the ordinal frame was a leftover from a shared-types refactor.- Doc TOC duplicate-key warning — two sections titled "When to reach for which" on
/theming/semantic-colorsslugged to the same React key. Renamed to "When to reach for which role" and "When to reach for which primitive";PageLayoutadditionally de-dupes TOC keys defensively so a transient DOM overlap during route transitions can't re-surface the warning.item.idstill carries the real heading id for anchor navigation;item.keyis a separate React-only identifier. - Shadowed cookbook import in
App.js—import CandlestickChartPage from "./pages/cookbook/..."was re-importing the same symbol used by the new/charts/route, so the charts-route fell through to the cookbook recipe. Renamed toCandlestickCookbookPage.
ai/schema.jsonandvalidationMap.tsgainedCandlestickChartentries;check-schema-freshness.jsandcheck-ssr-alignment.jsboth pass.
- Tooltip format cascade —
valueFormaton ordinal HOCs andxFormat/yFormaton XY HOCs now flow through to the default tooltip automatically, so a BarChart withvalueFormat: d => \$${d/1000}k`shows "$450k" on both the axis and the tooltip. Wired into: BarChart, StackedBarChart, GroupedBarChart, DotPlot, SwarmPlot, SwimlaneChart, LineChart, AreaChart, StackedAreaChart, Scatterplot, BubbleChart, ConnectedScatterplot, QuadrantChart, Heatmap.buildOrdinalTooltipandbuildDefaultTooltipgained format params; a newapplyFormathelper wraps formatter calls in try/catch so a misbehaving formatter falls back to the built-informatValinstead of breaking the tooltip. Customtooltipprops still fully override the default (re-pass the formatter insideTooltip({format})/MultiLineTooltip({fields:[{format}]})if you want it to apply). New "Format Cascade" section on/features/tooltips`. sort: "auto"on ordinal HOCs — preserves insertion order while streaming and falls through to value-desc on static data. Applied tooSorton the frame and tosorton BarChart / StackedBarChart / GroupedBarChart / DotPlot. DotPlot's default changed fromsort: truetosort: "auto"— fixes categories shuffling during streaming in the quick-start docs demo and any push-API usage.replace()method onStreamOrdinalFrameHandle— atomically swaps the dataset while preserving the store's category insertion-order memory and the transition position snapshot. Routes through a newDataSourceAdapter.setReplacementData()(emits{bounded: true, preserveCategoryOrder: true}); falls through to progressive chunking for large replacements.LikertChart's re-aggregation now usesreplace()instead ofclear() + pushMany()so streaming question order stays stable across ticks.Changeset.preserveCategoryOrder— new flag on the ingest changeset. When true on a bounded changeset, the store replaces the buffer contents but does NOT clear its category insertion-order memory, and marks itself as having received streaming-sourced data. The machinery that makes aggregator HOCs (LikertChart, future density/bin charts) behave like live streams even though the transport is wholesale replacement.getScales()on the sharedRealtimeFrameHandle(optional) — routed through 8 ordinal HOCs + 9 XY HOCs + 5 realtime HOCs. Returns the frame's resolved scales ({o, r, projection}for ordinal,{x, y}for XY). Network/geo/hierarchy HOCs stay compliant by virtue of the method being optional.LikertChartHandle— narrowed ref handle type exported from the public entry point. ExtendsRealtimeFrameHandleand typesgetScales()as returningOrdinalScales, soref.current?.getScales()?.o.domain()works without casts.useFramecomposition hook (src/components/stream/useFrame.ts) — extracts shared Tier A concerns across all four Stream Frames (size + responsive sizing, margin merge, foreground/background graphics resolution, animate → transition, current theme subscription, stable accessible-table id, rAF-coalesced render scheduling with unmount cleanup, pointer-coalesced hover handlers, theme-change effect). ~300 lines of duplication removed. No behavioral change for consumers.- FlowMap SSR support —
renderChart("FlowMap", ...)now works server-side via a newflowMapentry inserverChartConfigs.ts. Expands{flows, nodes}into the line-shape StreamGeoFrame expects, with value-proportional edge widths,edgeColorBy/edgeWidthRange/edgeOpacity/edgeLinecaphonored. Function-valuededgeColorByreturning literal CSS colors passes through unchanged (via sharedgetColor). LikertChartadded to the server-sideChartNameunion — was registered inCHART_CONFIGSbut absent from the TS union, sorenderChart("LikertChart", ...)would type-error despite working at runtime.animateprop on every HOC chart —animate?: boolean | { duration?, easing?, intro? }wired across all XY, ordinal, network, and geo HOCs. Stream Frames resolveanimate→transitioninternally, with synthesized intro animations: bars from baseline, wedges from collapsed arc, lines/areas clipped from left, points fromr=0, network nodes from chart center, geo points from center. Wedge angle interpolation for pie/donut data changes. Respectsprefers-reduced-motion.- Quadtree spatial index for point hit testing on XY (scatter/bubble), Geo (proportional symbol maps), and Ordinal (swarm plots) when point count exceeds 500. Each store tracks
maxPointRadiusso the hit tester widens its query for variable-size points (BubbleChart, proportional symbols). SharedfindHitPointInQuadtree(src/components/stream/quadtreeHitTest.ts) usesquadtree.visit()to enumerate every candidate within the search region, eliminating the nearest-only miss thatquadtree.find()had on heterogeneous-radius scenes. Path2Dcache on network edges (NetworkBezierEdge/NetworkRibbonEdge/NetworkCurvedEdge) —_cachedPath2D+_cachedPath2DSourcefields invalidate whenpathDchanges. Shared betweenNetworkCanvasHitTesterandnetworkEdgeRenderer.waitForChartReady/waitForAllChartsReady/waitForRafs/waitForStreamingUpdateinintegration-tests/helpers.ts— event-driven Playwright waits replacing the per-specwaitForVisualization+waitForTimeout(N)pattern.HoverPointerCoordstype inhoverUtils.ts— narrower hover-handler signature replacing theas unknown as React.MouseEventcast that the rAF-coalesced path used to need.ordinalFixtures.ts+recordCanvasOpstest utilities — shared sample datasets for bar-chart tests; behavior-level draw-op recorder that replaces brittletoHaveBeenCalledTimesassertions in canvas-renderer tests.describe.eachcombinatorial coverage forlineCanvasRendererover (curve × decay × thresholds), exercising the path-selection invariants that previously had a single test.- 3000+ unit tests passing (was 2890 in 3.3.x). Added cache-invalidation regressions for
_colorMapCache,_colorSchemeMap,_categoryIndexCache,_stackExtentCache, accessor explicit-clear, ParticlePool free-list,findHitPointInQuadtreevariable-radius,resolveCSSColorversion counter, swimlane bandwidth clamp. - Theme-driven selection opacity —
theme.colors.selectionOpacity(already defined onSemioticTheme; built-in presets set it to 0.1–0.15) is now wired into the dimming applied byhoverHighlight, legend isolate, and linked selections. Previously the value was emitted as the--semiotic-selection-opacityCSS variable but never read. A newuseResolvedSelection(selection)hook merges the theme value into the selection config; every HOC plusTreemapnow passes through it. Resolution order isselection.unselectedOpacity(per-chart) →theme.colors.selectionOpacity→DEFAULT_SELECTION_OPACITY(library fallback). Clients that previously reached into the package to changeDEFAULT_SELECTION_OPACITYcan now do<ThemeProvider theme={{ colors: { selectionOpacity: 0.5 } }}>instead.
- Function comparator on ordinal
sort/oSortis now a category-key comparator — prior types said(row, row) => numberbut the frame always invoked it with category name strings, so any user passing a row-comparator was getting silently incorrect ordering. Tightened to(a: string, b: string) => numberon BarChart, DotPlot, GroupedBarChart, StackedBarChart, and the frame'soSorttype.useSortedDatatreats function-valued sort (and"auto") as pass-through since the frame owns category ordering. No usages insrc/,docs/src/, orintegration-tests/passed a function comparator. - Stream Frame perf pass —
OrdinalPipelineStoredecay/pulse no longer rebuild aMap<datum, index>every frame (cached against_dataVersion); pulse wedge inner loop went fromO(wedges × data)toO(matches per category)viagetCategoryIndexMap.PipelineStorestacked-area extent fused into a single pass;resolveColorMapshort-circuits on_ingestVersion. Geo line projection fused project + filter into one pass. ParticlePool.spawn()— O(1) free-list (stack of free indices) replaced the O(capacity) linear scan.evaluateBezierrewritten asevaluateBezierInto(out)so positions write into the particle directly — zero per-particle allocation per frame.- rAF-coalesced pointermove in all four Stream Frames — caps hit-testing + React re-renders at the display refresh rate (60 Hz) instead of the native pointer rate (often 120–240 Hz).
onMouseLeavecancels any pending move; latest coords always processed. - CSS-var color cache (
resolveCSSColor) — version-counter design plus a singletonMutationObserverondocument.documentElementand aprefers-color-schemematchMedialistener. Themes/class toggles/media-query swaps that bypass React still invalidate; per-framegetComputedStylethrashing is gone. DEFAULT_SELECTION_OPACITY: 0.2 → 0.5 — unselected (dimmed) elements stay readable when a selection is active. Override viaselection.unselectedOpacity(per-chart) ortheme.colors.selectionOpacity(viaThemeProvider, applies to every chart). Built-in theme presets set this to 0.1–0.15.barPaddingratio clamped to ≤ 0.9 inOrdinalPipelineStore— degenerate layouts (e.g. horizontal swimlane whereshowCategoryTicks: falseshrinks the left margin and the vertical content area is less thanbarPadding * 2) no longer paint zero-bandwidth bands.
- Streaming ordinal category shuffle — re-aggregating from a live buffer (LikertChart) or pushing into DotPlot made categories visibly jump around when per-category values changed rank. Two root causes, both fixed: (1)
replace()now routes through the newpreserveCategoryOrderingest path so the category Set isn't wiped on every re-aggregation; (2)sort: "auto"(DotPlot's new default) collapses to insertion-order while streaming instead of value-desc. - Composing charts as
position: absoluteoverlays no longer hides the base layer — StreamXYFrame and StreamOrdinalFrame used to paint--semiotic-bgacross the full canvas regardless of whether the chart was on top of another. PassframeProps={{ background: "transparent" }}on the overlay to short-circuit the fill; the built-in composed-brush demos (/charts/realtime-histogram) now use this pattern. Network/Geo frames already behaved correctly. - MultiAxis ordinal
rExtentsnot cleared on bounded ingest — whenrAccessoris an array, the per-axisrExtents[i]instances are distinct fromthis.rExtent, so clearing only the latter left stale min/max on subsequent bounded replacements. All per-accessor extents now clear together. - Streaming axis rendered ghost ticks after
replace()dropped a category —resolveCategoriesretains its insertion-order memory for FIFO stability on re-appearance, but only theundefined/"auto"branch was filtering to live categories. Explicit"desc"/"asc"/false/comparator branches rendered empty columns for evicted categories. Live-category filtering now happens once at the top of the function and every branch reads from it. - DataSourceAdapter progressive-chunk timer state —
scheduleNextearly returns (completed dataset, superseded data) didn't resetchunkTimer, sosetBoundedData/clearLastDatacould callcancelAnimationFrameon a stale token. Every exit path now resetschunkTimer = 0, preserving the "chunkTimer === 0iff no rAF scheduled" invariant. Fixed in bothsetBoundedDataand the newsetReplacementData. setReplacementDatamicrotask race — apush()/pushMany()buffered just before areplace()could flush after the replacement and append stale points onto the fresh dataset.setReplacementDatanow clears the pushBuffer +flushScheduledstate before emitting the changeset.react-dom/serverstripped to(void 0)(...)in the server bundle —rollup-plugin-auto-externalmarks package roots external but not subpaths, sorenderToStaticMarkupwas being tree-shaken to an undefined binding. Addedid === "react-dom/server"to the rollup external predicate. Verified no other production subpath imports hit the same trap.StreamOrdinalFrameHandle.replace()JSDoc and atomicity — routes throughsetBoundedData-style progressive chunking for large datasets, not a single synchronous change. Corrected the "Atomically replace" wording to describe the actual two-phase behavior (small datasets synchronous, large datasets chunked).- DataSourceAdapter unmount cleanup —
StreamXYFrameandStreamOrdinalFramenow calladapter.clear()in their lifecycle cleanup so in-flight progressive chunking and pending push microtasks can't fire after unmount. - MinimapChart polling rAF — tracks its handle and cancels on unmount + data change. Was leaking a recursive
requestAnimationFramepoll that kept callingsetOverviewScaleson unmounted components. - Cache invalidation completeness —
PipelineStore._stackExtentCachenow invalidates ontimeAccessor/valueAccessor/runtimeModechanges;OrdinalPipelineStore._colorSchemeMaponthemeCategorical/colorAccessor;OrdinalPipelineStore._categoryIndexCacheoncategoryAccessor/oAccessor. - Accessor re-resolution gates —
updateConfigblocks for x/y/time/value (PipelineStore) and category/o/value/r (OrdinalPipelineStore) usedconfig.X !== undefined, which silently skipped re-resolution when a caller explicitly cleared an accessor ({xAccessor: undefined}— valid React pattern). Switched to"X" in configso defined → undefined transitions revert to the fallback key. - GeoCanvasHitTester wasted fallback — when a quadtree is built, the linear scan after a quadtree miss is now skipped (the visit-based path is authoritative). Per-hit
.filter()array allocations for areas/lines also removed. - StreamGeoFrame hover via
e.currentTarget— handler readscanvasRef.currentinstead so it works under the rAF-coalesced path that passes a synthetic{clientX, clientY}payload. _resetCSSColorCacheForTestobserver leak — disconnects the globalMutationObserverandmatchMedialistener it installed; bumpscurrentVersionrather than resetting to 0 so any surviving WeakMap entries can't be re-validated.
- Bumped
hono4.12.8 → 4.12.14 and@hono/node-serverto 1.19.14 (transitive viascripts/og-server.mjs). Resolves seven advisories — six inhono(cookie validation, IPv4-mapped IPv6 mismatch, path traversal intoSSG,serveStaticslash bypass,hono/jsxHTML injection) and one in@hono/node-server(serveStaticmiddleware bypass via repeated slashes). All moderate; reachable only from the OG-image build script, not from the published library.
- Dev deps:
@modelcontextprotocol/sdk1.27.1 → 1.29.0,esbuild0.27.4 → 0.28.0,@types/nodealigned to Node 22.19.17 (matches the Volta-pinned runtime). - Realtime encoding docs — new "Tuning for streaming cadence" subsection on
/features/realtime-encodingwith guidance on duration-vs-push-interval tradeoffs (fast / pulsed / slow streams) and thereplace()requirement for aggregator HOCs to participate in the transition system. - CLAUDE.md / AI docs — "Composing overlays" pitfall note added.
scripts/create-release-branch.shnow (a) syncsai/schema.jsonversion to the bumped package version, (b) verifiesCHANGELOG.mdhas an entry for the new version, and (c) gates onnpm audit --audit-level=moderate. Override the audit floor withAUDIT_LEVEL=...if a release is intentionally shipping with known low-severity transitives.prettier3.8.1 → 3.8.3 (dev-only patch).
- The per-spec
waitForVisualizationhelpers in 9 Playwright spec files (consolidated intointegration-tests/helpers.ts).
sortprop on StackedBarChart and GroupedBarChart — Default:false(data insertion order). Accepts"asc","desc",boolean, or custom(a, b) => numbercomparator. Maps to frameoSort. Previously categories were always sorted by total value.edgeIdAccessoronNetworkPipelineConfig— EnablesremoveEdge(edgeId)single-ID edge removal. Accepts string or function accessor. Throws descriptive error if not configured when single-ID form is used.- Transition exits on
remove()—remove()now callssnapshotPositions()before buffer mutation in PipelineStore and OrdinalPipelineStore. Removed items get fade-out exit transitions instead of vanishing instantly. - Selection clearing on
remove()— All three stream frames (XY, Ordinal, Network) clear hover state when the removed datum matches the current hover. Prevents stale tooltips and ghost highlights. serverChartConfigs.ts— ExtractedrenderChart()dispatch from a 400-line switch statement into a lookup table of{ frameType, buildProps }entries. Each chart type is independently readable and testable.- Shared
computeDecayOpacity()— Decay algorithm consolidated from 4 inline implementations (OrdinalPipelineStore, NetworkPipelineStore, GeoPipelineStore) into the existingpipelineDecay.tsutility. Single source of truth. HoverDataunified type — All four stream frames now construct typedHoverDataobjects instead of ad-hoc shapes. Network frames usenodeOrEdgefield (replaces untypedtype); geo frames usepropertiesfield. Fixed GeoFrame mismatch where tooltip andcustomHoverBehaviorreceived different shapes. Breaking: NetworkcustomHoverBehavior/tooltipContentcallbacks no longer received.type— used.nodeOrEdgeinstead.- SSR angle convention fix — SVG wedge/arc rendering adds
π/2to convert from canvas convention (0 = 3 o'clock) to d3-shape convention (0 = 12 o'clock). Fixes -90° rotation on all SSR pie, donut, gauge, and chord charts. - SSR hierarchy theme colors — Treemap, CirclePack, and TreeDiagram
colorByDepthnow usesconfig.colorScheme(from theme) instead of hardcodedDEPTH_PALETTE. Default fill uses first scheme color instead of#4d430c. - SSR GaugeChart needle — Needle rendered via React elements (XSS-safe), positioned from inner (margin-adjusted) dimensions, uses
resolveTheme()for color, divide-by-zero guard ongMax === gMin. - SSR
sweepAnglepassthrough —sweepAnglewas on the props but missing from thepipelineConfigbuilder. Gauge arcs now render with correct sweep. - SSR
hierarchySumstring resolution — StringvalueAccessor(e.g.,"value") now resolved to a function before passing tod3-hierarchy.sum(). - SSR bottom legend positioning — Legend placed at
totalHeight - margin.bottom + 38(below axes) instead of hardcoded offset that overlapped chart area. - SSR ID uniqueness — All SVG element IDs (
data-area,axes,grid,legend,chart-title,annotations,semiotic-title,semiotic-desc, hatch patterns) prefixed with_idPrefixin multi-chart documents.renderDashboardpasses per-chart prefixes. - 88 new tests — Push API edge cases (17), server rendering coverage (27), HOC rendering integration (22), callback wiring + accessibility + bad data resilience (22). Plus 9 PipelineStore cache invalidation tests.
- Ordinal scene builder tests refactored — 14 exact-pixel assertions replaced with relationship/proportional assertions. Tests now survive layout constant changes.
- Unsafe any casts reduced: 240 → 164 — Hover data types, renderer arrays, pipeline config, accessor utils, SSR prop threading.
- SSR
framePropsoverride priority —framePropsspread first, explicit top-level props override.margin/colorScheme/legendPositiononly override when defined (notundefined). - SSR gallery — All 15 charts use
renderChartwith explicit themes (11 different presets). Dark-themed charts have dark card backgrounds.
getColor()/getSize()null datum guard — Optional chaining prevents crash when datum is undefined.ProportionalSymbolMapsizeDomain crash —filter(Boolean)+ optional chaining in accessor.resolveCSSColorcache — Restored per-canvasWeakMapcache withhas()check (handles falsy values).clearCSSColorCache()invalidates on theme change.pieceStylemerge null guard — UserframeProps.pieceStylereturningundefined/nullno longer crashes spread.- GeoFrame hover/click shape mismatch —
customHoverBehaviorandtooltipContentnow receive the sameHoverDataobject. - Bottom legend overlap — Positioned below axes area in reserved margin.
semiotic/serverproduction API —renderChart(component, props)renders 27+ HOC chart types to standalone SVG strings. Supports all themes, legends (4 positions), grid, annotations (y-threshold, x-threshold, category-highlight, widget, enclose), and accessibility attributes (role="img",<title>,<desc>,aria-labelledby). SVG groups haveidattributes for Figma layer naming (data-area,axes,grid,annotations,legend,chart-title).renderDashboard(charts, options)— Multi-chart dashboard layout with title, theme, configurable columns. Each chart entry supportscolSpanfor wide charts.renderToImage(component, props, options)— PNG/JPEG rasterization via sharp (peer dependency). Configurablescalefor retina output.renderToAnimatedGif(chartType, data, props, options)— Animated GIF from streaming data windows. Options:fps,transitionFrames,easing,decay,windowSize,loop,scale.generateFrameSequence(frames)— Snapshot-based animation for topology changes (network failover, edge removal). Each frame is an independentrenderChartcall.- SVG hatch patterns —
createSVGHatchPattern()for server-rendered diagonal hatch fills. Used by FunnelChart vertical mode for dropoff bars. - Push API
remove()andupdate()— Selective data removal and in-place update across all stores (RingBuffer, PipelineStore, OrdinalPipelineStore, NetworkPipelineStore) and all HOC/frame handles.remove(id)orremove([ids])by ID (requirespointIdAccessor/dataIdAccessor).update(id, updater)for in-place mutation. Network:removeNode(id)cascades to edges,removeEdge(source, target)removes parallel edges. pointIdAccessor/dataIdAccessor— ID accessor props on BaseChartProps forremove()andupdate()targeting.- GaugeChart server rendering — Sweep angle, start angle, inner radius, threshold zone fills, needle indicator.
- FunnelChart server rendering — Horizontal and vertical modes with trapezoid connectors. Vertical mode supports hatch pattern dropoff bars.
- Sparkline server rendering —
renderChart("Sparkline", props)with no axes, 2px margins, no grid/legend/title. - 6 interactive docs pages — Render Studio, Theme Showcase, Dashboard Gallery, Email Preview, Export & Embed (with real GIF downloads), Push API demo.
hoverHighlightsimplified — Changed fromboolean | "series"to justboolean. Any truthy value triggers series-based dimming (requirescolorBy).- CSS variable resolution in canvas —
resolveCSSColor()resolvesvar(--name, fallback)viagetComputedStyleat paint time. Per-canvas cache avoids repeated calls within a paint cycle;clearCSSColorCache()invalidates on theme change. All 9 canvas renderers updated. extentPaddingnullish coalescing — Changed|| 0.05to?? 0.05soextentPadding: 0is respected.- Swimlane
skipMaxPad— Prevents trailing gap in swimlane charts by skipping max-side extent padding. frameProps.pieceStylemerging — Ordinal HOCs now merge user'spieceStylewith computed base style instead of excluding it. Enables stroke overrides.resolveGroupColor()in server rendering — XY line/area style fallbacks callresolveGroupColor(group)instead of hardcoding#007bff. Theme categorical colors flow through to server SVG.- Force layout
iterations: 0— Now skips simulation entirely for pinned node positions. Previously warm-start detection overrode to 40 iterations. - Background rect positioning — Server SVG background rect renders at SVG root, not inside translated group (fixes Figma import).
- Dependency bumps — vite 8.0.5, typedoc 0.28.18, vulnerable devdeps fixed.
- Server legend margin — Legend position expands margin before width/height calculation (right:100, left:100, bottom:70, top:40).
- Server
framePropspassthrough —framePropsspread into renderer common object sopieceStyle,lineStyleflow through. - Server
effectiveColorScheme— Falls back totheme.colors.categoricalwhencolorSchemeprop not set. - Network
remove()return value — Returns node data before removal instead of empty array. - RingBuffer
update()snapshot safety — Proper type-aware cloning (array spread for arrays, object spread for objects). - Timestamp buffer desync on remove — Lockstep compaction removes matching indices from timestamp buffer.
buildRealtimeNodespreserving positions — Usesx: d.x ?? 0, y: d.y ?? 0instead of hardcoded zeros.- Dark mode CSS var strokes — Docs site sets
--semiotic-bgin both dark/light blocks.LiveExampleuses MutationObserver for chart remount on theme toggle.
- GaugeChart — New ordinal HOC for single-value gauges with threshold zones, needle indicator, and configurable sweep angle. Built on StreamOrdinalFrame radial projection (reuses pie/donut rendering pipeline). Supports
fillZones={false}for fixed-zone displays where only the needle moves (e.g. election needle). Exported fromsemioticandsemiotic/ordinal. - Range/dumbbell plot — Candlestick chart type now supports range mode: omit
openAccessor/closeAccessorand provide onlyhighAccessor/lowAccessorto render vertical lines with endpoint dots. SinglerangeColorviacandlestickStyle. No new HOC — demonstrates StreamXYFrame flexibility. scalePadding— Pixel inset on XY scale ranges to prevent glyph clipping at chart edges. Available onStreamXYFrameProps; HOCs pass viaframeProps={{ scalePadding: 12 }}. Domain and tick values unchanged.xScaleType="time"— New scale type createsd3.scaleTimefor Date-aware tick generation. Required for landmark ticks with timestamp data.sweepAngle— New prop onStreamOrdinalFramePropslimiting pie/donut arc to less than 360° (used internally by GaugeChart).- Multi-point tooltip —
tooltip="multi"on LineChart shows all series values at hovered X with color swatches. Custom functions receivedatum.allSerieswith{group, value, valuePx, color, datum}. - Click-to-lock crosshair — In
linkedHoverx-position mode, click locks the crosshair. Escape or click again to unlock. Source-aware unlock prevents multi-chart interference. - Hover-based sibling dimming —
hoverHighlighton all HOCs dims non-hovered series on data mark hover (requirescolorBy). - Per-series fillArea —
fillArea={["A","B"]}on LineChart fills named series as areas, others stay as lines. New"mixed"chart type with dedicated scene builder. - Multi-color gradient fills —
gradientFill={{ colorStops: [{offset, color}] }}on AreaChart for semantic color bands. Supportstransparent. - Line stroke gradients —
lineGradient={{ colorStops }}on LineChart/AreaChart for horizontal gradient strokes. - Axis config extensions —
includeMaxforces domain-max tick,autoRotaterotates labels 45° when crowded,gridStyle("dashed"|"dotted"|string) for grid lines,landmarkTicksbolds month/year boundaries. baselinePadding— Boolean prop on bar chart HOCs. Defaultfalsemakes bars flush with 0 baseline.hoverRadius— Configurable hit-test distance (default 30px) on all XY HOCs andStreamXYFrameProps.- ReactNode tick labels —
xFormat,yFormat,categoryFormataccept=> string | ReactNodewith<foreignObject>fallback. - Tick deduplication — Adjacent identical tick labels automatically removed.
getHitRadiusandMultiPointTooltipexported fromsemiotic/utils.isTimeLandmarkandtoDateexported fromhitTestUtils.ts(shared across SVGOverlay and tests).
- 30px default hit radius — All 4 hit testers (XY, Network, Geo, Ordinal) now use
getHitRadius()from sharedhitTestUtils.ts. Previous 12px Fitts's law cap was too small for comfortable interaction. lineDataAccessordata flattening — StreamXYFrame now flattens line-object data before pipeline ingestion. Previously the pipeline readxAccessoron line objects (which lack that field), producing NaN extents.scaleTimedomain comparison —valueOf()comparison for Date objects prevents stale scales from blocking updates.- Annotation dark mode —
Annotation.tsxtext usesvar(--semiotic-text), connectors usevar(--semiotic-text-secondary)instead of hardcoded black. - SwimlaneChart
showCategoryTicks={false}— Now suppresses both tick labels and axis title. - Floating point tooltip precision —
formatValuerounds viatoPrecision(6). - Default tick format Date-aware —
defaultTickFormathandles Date objects (formats as "Jan 7" style). bodyWidth: 0on candlestick — Body rect skipped entirely, no invisible canvas elements.- Ordinal bar baseline — Value axis baseline draws at
rScale(0), not chart edge. Include-zero applied before padding. - Remap fast-path with
scalePadding— Disabled proportional remap when padding is set (forces full rebuild for correctness). - Candlestick
updateConfig— OHLC accessors andcandlestickRangeModerecomputed on prop changes.
baselineStylerenamed togridStyle— Applies to grid lines (not axis baselines, which stay solid).- Build system —
rollup-plugin-typescript2replaced with@rollup/plugin-typescript(fixes TS compilation). - Playwright CI —
serve-examples:ciscript skips redundantnpm run dist. Timeout bumped to 120s.
- Exhaustive scene builder test coverage — 346 new tests across all XY scene builders (line, area, stacked area, point, swarm, heatmap, waterfall, candlestick, bar) and ordinal scene builders (funnel, bar-funnel, swimlane). Tests assert actual coordinates, baselines, cumulative positions, and style resolution — not just "it didn't crash."
- FunnelChart and LikertChart HOC tests — First test suites for the two previously untested HOCs. FunnelChart: 29 tests covering horizontal/vertical modes, multi-category mirroring, connector opacity, tooltip metadata. LikertChart: 31 tests covering raw/pre-aggregated modes, diverging colors, neutral sentinels, error states.
- Render pipeline benchmarks —
benchmarks/unit/render-pipeline.bench.tscovering scene builder throughput (scatter 50k: 4ms, line 10k: 0.45ms, stacked area 10k: 1.3ms), RingBuffer push/iteration, and end-to-end ingest-to-scene-build. Identified heatmap at 50k (49ms) as the only builder exceeding frame budget. - Dev-mode
d.dataaccess warning — Frame callbacks (nodeStyle,edgeStyle,nodeSize) now warn in development when users access properties that exist on.databut not on the RealtimeNode/RealtimeEdge wrapper (e.g.,d.categoryinstead ofd.data?.category). Zero production overhead. Applied to all 5 layout plugins (sankey, force, chord, orbit, hierarchy). - Streaming-first docs narrative — Landing page and Getting Started page restructured to lead with the streaming engine (push API, two-canvas RAF loop, ring buffer, decay/pulse/staleness/transitions) as the primary differentiator.
- Heatmap scene builder optimized — Streaming path uses flat
Int32Array/Float64Arraygrids instead ofMap<string, {data[]}>, eliminating 50k string key allocations and per-datum array pushes. Static path uses numeric Map keys and precomputed 256-entry color LUT (cached per scheme) instead of per-cellscaleSequentialcalls. Streaming 50k points into 20×20 grid: 0.37ms (was ~49ms with Map+string approach). Static path ~15% faster at high cardinality.
@modelcontextprotocol/sdkremoved from production dependencies — The MCP CLI (semiotic-mcp) now bundles the SDK via esbuild, sonpm install semioticno longer pulls in the 4MB+ MCP SDK and its transitive deps. The bundled CLI works identically — zero behavior change fornpx semiotic-mcpusers.@types/d3-quadtreemoved to devDependencies — Type declaration packages are always dev-only.- Stacked area points at wrong Y position —
emitPointNodesused rawctx.getYinstead of cumulative stacked Y. Fixed by addingyGetOverrideparameter and computing stacked positions frombuildStackedAreaNodes'stackedTopsmap — no duplicate stacking pass. - Null Y datums assigned stacked Y — Added
y != null && !Number.isNaN(y)guard before setting stacked point positions. - Stale forecast overlays on prop removal — Early return in LineChart effect when both
forecastandanomalybecome falsy now clears previous statistical overlays. - GeoCanvasHitTester inconsistent hit radius — Quadtree path used
(r||4)+4, linear scan usedMath.max((r||4)+5, 12). Unified toMath.max((r||4)+5, 12)everywhere. - backgroundGraphics not honoring margins — StreamXYFrame and StreamGeoFrame rendered
backgroundGraphicsoutside the margin-translated<g>. Fixed in both client and SSR paths.
- LikertChart — new ordinal HOC for Likert scale survey data. Horizontal (default): diverging bar chart centered at 0% with negative levels extending left, positive right, and neutral (odd count) split 50/50 across the centerline. Vertical: stacked 100% bar chart. Supports raw integer scores (1-based, auto-aggregated) and pre-aggregated (question, level, count) data. Works with any scale size (3-point to 7-point+). Push API for streaming — the chart accumulates raw data and re-aggregates percentages on each push.
onClickprop on all HOCs — Direct click handler receiving(datum, { x, y })with the original unwrapped datum. Works on lines, bars, areas, pie slices, nodes, and geo features. No moreonObservationfiltering orframeProps.customClickBehaviorescape hatch for simple click handling.categoryFormatprop on ordinal HOCs — Custom formatting function for individual category tick labels. Receives(label, index?)and returns a formatted string. Covers truncation, abbreviation, and custom labeling without dropping toframeProps.oLabel.category-highlightannotation type — Highlights a specific category column/row in ordinal charts with a semi-transparent band. Usage:{ type: "category-highlight", category: "Q3", color: "#4589ff", opacity: 0.15 }. Uses raw band scale from annotation context for correct positioning.labelPositionon threshold annotations —y-thresholdsupports"left"|"center"|"right"(default).x-thresholdsupports"top"(default) |"center"|"bottom". Previously labels were fixed at right/top.- Coordinate-based linked crosshair —
linkedHover={{ name: "sync", mode: "x-position", xField: "time" }}broadcasts the hovered X data value across charts. Consuming charts render a synced vertical crosshair at that X position with independent Y values. Wired through all 9 XY HOCs via sharedgetCrosshairPropsutility. Crosshair positions are cleaned up on unmount. - Tooltip viewport-aware flip — Tooltips auto-flip horizontally and vertically when near container edges. Uses
useLayoutEffectmeasurement with proper dependency array for precise flip decisions. Applied to all four stream frames (XY, ordinal, network, geo). - Data-driven histogram bin snapping — RealtimeTemporalHistogram brush now snaps to actual computed bin boundaries (binary search via
floorBinBoundary/ceilBinBoundary) instead of uniform grid math. Works with irregular bin widths.snapDuring: trueenables continuous snap feedback during drag. Bin boundaries are defensively sorted. Auto-populated from pipeline store — zero config change for existing usage. - IBM Carbon color palettes —
CARBON_CATEGORICAL_14(14-color),CARBON_ALERT(danger/warning/success/info), and"carbon"/"carbon-dark"theme presets. Exported fromsemioticandsemiotic/utils. Integrated into Theme Explorer and Theme Provider docs. - Legend line wrapping — horizontal legends now wrap to multiple rows when items exceed the available chart width, preventing overflow. Applies to all charts with
legendPosition="bottom"or"top". showPointson AreaChart and StackedAreaChart — Data point markers are now supported on area charts, matching LineChart's existingshowPoints/pointRadiusprops. Scene builders (line, area, stacked area) now emitPointSceneNodeentries whenpointStyleis configured, andpointCanvasRendereris included in the renderer dispatch for all three chart types.
- ThemeProvider categorical colors flow to all HOCs — When
colorByis set butcolorSchemeis not explicitly provided, charts now use the ThemeProvider'scolors.categoricalpalette instead of falling back to d3category10. Priority: explicitcolorScheme> theme categorical >"category10". Previously, ordinal and XY charts always defaulted tocategory10regardless of theme. - 12px minimum hit target (Fitts's law) — All four canvas hit testers (XY, ordinal, network, geo) now enforce
Math.max(node.r + 5, 12)as the minimum interactive hit radius. Previously formulas varied across hit testers (some as small as 5px), making small points difficult to hover. useColorScalecolorSchemeparameter is now optional — Callers that don't pass a color scheme get the effective scheme fallback instead of requiring an explicit argument. Fallback uses scheme-based scale instead of hardcoded"#999".LinkedCrosshairStoreoptimized subscriptions —useCrosshairPositionuses no-op subscribe/snapshot when the crosshair name is undefined, avoiding unnecessary store subscriptions on charts that don't use crosshairs.
- Legend
styleFncontract — LikertChart (and any chart using customlegendGroups) now passes(item: LegendItem, index)tostyleFncorrectly, fixing grey legend swatches. - LikertChart tooltip — shows category name (bold) and level name with percentage/count instead of raw internal field values. Uses standard tooltip chrome (dark background, rounded corners) matching all other charts.
category-highlightannotation in ordinal charts — Annotations now receive the raw band scale (scales.o) andprojectionin the annotation context, fixing cases where theoCenteredwrapper didn't expose.bandwidth().- Crosshair cleanup on unmount — Linked crosshair positions are cleared when a chart unmounts or when crosshair config changes, preventing stale crosshair markers in coordinated dashboards.
FlippingTooltipuseLayoutEffectdependency array — Added proper dependencies (children,className,containerWidth,containerHeight) to prevent stale measurements.- Removed dead
slicePaddingprop — Removed from PieChart and DonutChart interfaces, validation map, schema, tests, and all documentation. The prop was declared but never wired to any rendering logic. - Removed unused
DEFAULT_COLORimport in Heatmap — Eliminated dead import.
slicePaddingprop on PieChart/DonutChart — This prop was never functional. UseframeProps={{ oPadding: value }}for slice padding.
- Hover dot color matching — The hover indicator dot now automatically matches the hovered element's color (line stroke, area stroke, point fill) instead of hardcoded blue. Override with
frameProps={{ hoverAnnotation: { pointColor: "#custom" } }}. Fallback chain: explicitpointColor→ element color →--semiotic-primaryCSS var →#007bff. Affects all XY and Geo charts. pointColoroption onHoverAnnotationConfig— New opt-in override for hover dot color on Stream Frames.- Adaptive time tick formatting — New
adaptiveTimeTicks(granularity?)export fromsemiotic. Produces hierarchical axis labels: first tick is fully qualified, subsequent ticks only show what changed (e.g. seconds when the minute is the same, full timestamp when the hour rolls over). Tick labels auto-space based on label width to prevent overlap. - Forecast: training line styling —
trainStroke("darken" or CSS color),trainLinecap("round"),trainUnderline(true | "lighten"),trainOpacity,forecastOpacityonForecastConfig. Enables dashed training lines with solid underlines for visual distinction. - Forecast: per-datum anomaly styling —
anomalyColor,anomalyRadius, andanomalyStyleonForecastConfignow accept functions(datum) => valuefor data-driven anomaly rendering (e.g. sizing dots by anomaly count). - Forecast: multi-metric boundary duplication —
_groupByinternal field onForecastConfig. WhenlineByandforecastare both active, boundary points are duplicated within each metric group (not across groups), preventing stray cross-metric connecting lines in interleaved data. training-basesegment type — New segment for solid underlines beneath dashed training lines. PipelineStore renders training-base first (insertion order) so the solid line appears beneath the dashed one.resolveNodeColorshared utility — Extracted tosceneUtils.ts, used by both StreamXYFrame and StreamGeoFrame for consistent hover color resolution. HandlesCanvasPatternfills correctly.- 128 new unit tests — Multi-metric boundary duplication (3 tests), ThemeStore dark mode merging (5 tests), PipelineStore reproduction (9 tests), LineChart integration (8 tests), plus expanded statisticalOverlays coverage.
- SVGOverlay left axis label missing in dual-axis mode —
MultiAxisLineChartpasses left axis label viaaxesconfig, but SVGOverlay only read theyLabelprop (which is suppressed in dual-axis mode). Now readsleftAxis?.label || yLabel. - ThemeStore
mode: "dark"merged onto wrong base —{ mode: "dark", colors: { categorical: [...] } }was merging ontoLIGHT_THEME, so dark-mode text/background/grid colors were lost. Now correctly merges ontoDARK_THEME. - Tick label overlap on time axes — X-axis tick spacing now accounts for actual label width (estimated at 6.5px/char) instead of using a fixed 55px minimum, preventing label collision on dense time axes.
tickFormatsignature expanded —AxisConfig.tickFormatandxFormatnow receive(value, index, allTickValues)so formatters can produce hierarchical labels (e.g. show full date only on first tick or at boundary crossings).- Function accessors with forecast/anomaly — When
xAccessororyAccessoris a function, resolved values are now baked into data under__resolvedX/__resolvedYfields so the statistical overlay pipeline and annotation renderer can access them by string key. - Geo hover ring color — Geo frame point hover ring now uses
resolveNodeColor(shared utility) instead of inline logic, and correctly handlesCanvasPatternfills. - Tick color dark mode fallback — SVGOverlay tick color CSS var chain is now
--semiotic-text-secondary→--semiotic-text→#666, improving visibility when only--semiotic-textis set. - Annotation accessor fallback — SVGOverlay annotation renderer receives
"__resolvedX"/"__resolvedY"when accessors are functions, preventing annotations from rendering at wrong positions.
SegmentTypeunion expanded — Added"training-base"to the exported type.ForecastConfiginterface expanded — AddedtrainStroke,trainLinecap,trainUnderline,trainOpacity,forecastOpacity,anomalyStyle,_groupBy.anomalyColorandanomalyRadiusnow accept functions.- HOC early return guard — LineChart (and other HOCs with statistical overlays) no longer returns early before loading/empty state, ensuring all hooks are called unconditionally (React rules of hooks compliance).
Note: v3.1.1 was yanked from npm due to broken MCP tool schemas. Upgrade directly from 3.1.0 to 3.1.2.
- MCP server tools received no arguments — all 5 tools used empty
{}Zod schemas, causing the MCP SDK to strip all incoming parameters. Every tool call silently fell into "missing field" error paths. Fixed by defining proper Zod input schemas for all tools (getSchema,suggestChart,renderChart,diagnoseConfig,reportIssue). - MCP geo chart rendering —
renderHOCToSVGcalledvalidatePropswhich rejected geo components not in its validation map. Geo components (ChoroplethMap, ProportionalSymbolMap, FlowMap, DistanceCartogram) now skip validation and render correctly. - MCP
--portparsing —--httpwithout--portno longer produces NaN (falls back to 3001). - MCP "top-level fields" dead code — removed unreachable spread logic from
renderChart/diagnoseConfighandlers; updated Zod descriptions to match actual schema behavior (MCP SDK strips fields not in Zod schema). - suggestChart Histogram heuristic — removed unreachable
data.length >= 10check (suggestChart accepts 1–5 samples per its Zod schema). - renderHOCToSVG validation fragility — tightened unknown-component skip check to require exactly one "Unknown component" error instead of
.every()over all errors.
- MCP geo chart support — ChoroplethMap, ProportionalSymbolMap, FlowMap, and DistanceCartogram added to the MCP render registry (25 renderable components total).
- MCP HTTP transport —
npx semiotic-mcp --http --port 3001starts a session-based HTTP server with CORS headers for browser-based MCP inspectors and remote access. - suggestChart input validation — Zod schema enforces
.min(1).max(5)on data array.
- MCP
reportIssuetool — generates pre-filled GitHub issue URLs for bug reports and feature requests directly from AI coding assistants. No auth required. - MCP
getSchematool — returns the prop schema for a specific component on demand, reducing token overhead vs loading the full 63KB schema. Omitcomponentto list all 30 chart types. - MCP
suggestCharttool — analyzes a data sample and recommends chart types with confidence levels and example props. Supportsintentparameter for narrowing suggestions (comparison, trend, distribution, relationship, composition, geographic, network, hierarchy). - MCP server documentation — comprehensive setup instructions, tool descriptions, and usage examples in README.
- npm keywords —
mcp,model-context-protocol,mcp-server, and other discovery keywords for MCP directory indexing. - CI coverage thresholds — unit test coverage gated at 62/52/63/65% (statements/branches/functions/lines) with
@vitest/coverage-v8. - CI bundle size guardrails —
size-limitchecks for all 6 entry bundles in CI pipeline. - axe-core accessibility scanning — automated
@axe-core/playwrightscans across all chart category pages in E2E tests. - Self-healing error boundaries —
SafeRenderrunsdiagnoseConfigon chart failures (dev mode) and displays actionable fix suggestions alongside the error message. - 61 new unit tests — coverage for
withChartWrapper(SafeRender, warnDataShape, warnMissingField, renderEmptyState, renderLoadingState), network utilities, and push API on 7 ordinal chart types.
- MCP server — added
getSchema,suggestChart, andreportIssuetools (5 tools total). Added geo chart rendering support (ChoroplethMap, ProportionalSymbolMap, FlowMap, DistanceCartogram). - npm description — updated to highlight MCP server capability for discoverability.
prepublishOnlycleans dist/ — prevents stale dynamic import chunks from accumulating in published tarball.
- MCP
componentkey leaking into props — flat-shape calls like{ component: "LineChart", data: [...] }no longer passcomponentas a chart prop. - Missing dynamic import chunk —
dist/*-statisticalOverlays-*.jsadded tofilesarray so forecast/anomaly features work when consumed via ESM.
-
Geographic visualization — new
semiotic/geoentry point with 4 HOC chart components and a low-levelStreamGeoFrame, all canvas-rendered with d3-geo projections.ChoroplethMap— sequential color encoding on GeoJSON features. SupportsareaOpacity, function or stringvalueAccessor, and reference geography strings ("world-110m","world-50m", etc.).ProportionalSymbolMap— sized/colored point symbols on a geographic basemap withsizeBy,sizeRange, andcolorBy.FlowMap— origin-destination flow lines with width encoding, animated particles (showParticles,particleStyle), andlineType("geo"|"line").DistanceCartogram— ORBIS-style projection distortion based on travel cost. Concentric ring overlay (showRings,ringStyle,costLabel), north indicator (showNorth), configurablestrengthandlineMode.StreamGeoFrame— low-level geo frame with full control over areas, points, lines, canvas rendering, and push API for streaming.
-
GeoCanvasHitTester— spatial indexing for hover/click hit detection on canvas-rendered geo marks. -
GeoParticlePool— object-pool polyline particle system for animated flow particles. Supports"source"color inheritance, per-line color functions, and configurable spawn rate. -
GeoTileRenderer— slippy-map tile rendering on a background canvas. Mercator-only with retina support. ConfigurabletileURL,tileAttribution,tileCacheSize. -
Zoom/Pan — all geo charts accept
zoomable,zoomExtent,onZoom, with imperativegetZoom()/resetZoom()on the frame ref. Re-renders projection directly (no CSS transform). -
Drag Rotate —
dragRotateprop for globe spinning (defaults true for orthographic). Latitude clamped to [-90, 90]. -
Reference geography —
resolveReferenceGeography("world-110m")returns Natural Earth GeoJSON features.mergeData(features, data, { featureKey, dataKey })joins external data into features. -
Geo particles —
showParticlesandparticleStyleonFlowMapandStreamGeoFramefor animated dots flowing along line paths. -
6 geo documentation pages — ChoroplethMap, ProportionalSymbolMap, FlowMap, DistanceCartogram, StreamGeoFrame, and GeoVisualization overview.
-
2 geo playground pages — interactive prop exploration for geo charts.
-
1 geo recipe page — ORBIS-style distance cartogram walkthrough.
-
Geo test suites — unit tests for FlowMap (25 tests), ChoroplethMap (16 tests), DistanceCartogram (19 tests), colorUtils (+6 tests), hooks (+3 tests).
-
Accessibility foundation — moves Semiotic from ~30% to ~70% WCAG 2.1 AA compliance.
- Canvas
aria-label— every<canvas>element now has a computedaria-labeldescribing chart type and data shape (e.g., "scatter chart, 200 points"). All four Stream Frames:StreamXYFrame,StreamOrdinalFrame,StreamNetworkFrame,StreamGeoFrame. - Legend keyboard navigation — interactive legend items are focusable (
tabIndex={0},role="option"), withrole="listbox"on the container. Enter/Space activates (click), Arrow keys navigate between items. Visible focus ring on keyboard focus. aria-multiselectableon legend listbox whenlegendInteraction="isolate"orcustomClickBehavioris present.aria-selectedon legend items reflecting isolation state.aria-live="polite"region —AriaLiveTooltipcomponent mirrors tooltip text for screen reader announcements on hover.- SVG
<title>and<desc>— all SVG overlays (SVGOverlay,OrdinalSVGOverlay,NetworkSVGOverlay) includerole="img"and accessible<title>/<desc>elements derived from the chart title. aria-labelon ChartContainer toolbar buttons — Export, Fullscreen, and Copy Config buttons have descriptive labels and title attributes.- 35 Playwright integration tests —
integration-tests/accessibility.spec.tscovering canvas aria-labels, AriaLiveTooltip, legend keyboard traversal, focus rings, SVG title/desc, and ChartContainer toolbar buttons.
- Canvas
-
Streaming legend support — new
useStreamingLegendhook discovers categories from pushed data and builds legends dynamically with minimal re-renders via version counter. Integrated into StackedBarChart, PieChart, DonutChart, GroupedBarChart. -
Streaming regression test suite — 20+ Playwright integration tests (
streaming-regression.spec.ts) covering:- Canvas pixel sampling to verify colored fills (saturation > 0.1) across 8 streaming chart types
- Legend items appear after push API data arrives (4 chart types)
- Area chart tooltip contains numeric values, not dashes
- LineChart streaming stability (no "Maximum update depth" errors)
- Force graph content centroid within 30% of canvas center
- Error-free rendering across all 11 streaming test fixtures
-
Performance: color map cache —
PipelineStorecaches the category→color map across rebuilds using a sorted category set as cache key. Skips rebuild when categories are unchanged. (PipelineStore.ts) -
Performance: stacked area cache —
PipelineStorecaches stacked area cumulative sums using abuffer.size + ingestVersionhash. Skips expensive groupData + cumulative sum computation when data is unchanged. (PipelineStore.ts)
- Grey fills on push API charts — When using
ref.current.push(), HOC charts passed undefined color scales to style functions, causing grey fallback fills. Fixed end-to-end:- HOC
pieceStyle/pointStyle/lineStylefunctions now omit fill/stroke when colorScale is unavailable OrdinalPipelineStore.resolvePieceStylefills in from the frame's color scheme when HOC returns no fillPipelineStore.resolveLineStyle/resolveAreaStyle/point scene builder do the same for XY charts- New
resolveGroupColormethod provides centralizedSTREAMING_PALETTEassignment for streaming groups - Affected charts: StackedBarChart, PieChart, DonutChart, GroupedBarChart, BubbleChart, StackedAreaChart, AreaChart, LineChart, Scatterplot, QuadrantChart, ChordDiagram
- HOC
- LineChart infinite re-render loop — circular dependency between
useEffect→setSegmentAwareStyle→baseLineStyle→colorScale→statisticalResult. Fixed by guarding statistical effect to only run when forecast/anomaly is present and derivingeffectiveLineStylewithout unnecessary state. createColorScalecrash on undefined data — added null guards (d?.+.filter(v => v != null)) so push API charts with sparse data don't throw.OrdinalSVGOverlayduplicate React keys — keys now include category/group for uniqueness across stacked/grouped layouts.
- Area/StackedArea tooltips showing "-" —
hitTestAreaPathnow extracts the specific data point at the hover index (likehitTestLinedoes) instead of returning the entire data array. - Ordinal frame tooltips — default tooltip now shows category + value using
__oAccessor/__rAccessormetadata. - Geo chart tooltips — ChoroplethMap shows country names (not numeric IDs), ProportionalSymbolMap shows formatted metrics with labels, FlowMap shows source → target with values.
- Force graph centering — added
forceCenterto simulation, strengthenedforceX/forceY, clamped node positions to canvas bounds. FixedfinalizeLayoutoverwriting force-computed positions from stale bounding boxes during streaming warm-starts. - Streaming force refresh — force simulation now runs on topology changes during push API streaming.
- FIFO category ordering — streaming ordinal charts preserve insertion order instead of re-sorting by value (fixes violin/histogram column flicker).
- Edge hit areas — expanded to 5px minimum tolerance across XY lines, network edges (bezier + path), and geo lines. Added
pointToSegmentDistfor accurate perpendicular distance. Line hit tolerance now scales with stroke width. - Network edge ctx.lineWidth leak —
hitTestBezierEdgeandhitTestPathEdgenow save/restorectx.lineWidtharoundisPointInStrokecalls. - Sankey crossing reduction — added barycenter-based initial node ordering before iterative relaxation.
- QuadrantChart streaming — fixed quadrant backgrounds disappearing after first point; points now auto-color by quadrant when no
colorByprovided. - Anti-meridian line handling — geo lines that wrap across the projection edge are split into segments with smooth opacity fades.
- Distance cartogram centering — center node is pinned to viewport center during streaming.
- Orthographic drag jank — pointer-move rotations now coalesce via
pendingRotationRef, applying once per rAF frame.
- Orbit diagram — ring/connecting lines changed from
currentColor(invisible on canvas) torgba(128,128,128,0.35). Root nodes use scheme color instead of grey depth palette. - Treemap/CirclePack labels — luminance-based contrast text color (white on dark fills, dark on light fills). Treemap parent labels positioned at top-left of rectangle.
- ScatterplotMatrix diagonal histograms — now colored by category with O(1) Map lookups instead of grey fills with O(n)
.indexOf(). - Dark mode fixes — serialization page text contrast, streaming system model background, candlestick wick color, uncertainty tooltip background.
tooltip={false}now correctly disables tooltips on all 22 remaining HOCs. The patternnormalizeTooltip(tooltip) || defaultTooltipContentwas replaced with an explicittooltip === false ? undefined : ...check.normalizeTooltipunwrap heuristic tightened — the HoverData unwrap now only triggers when the object has.type === "node" | "edge"AND.data, preventing false unwraps when a user's datum has a.dataproperty.- ForceDirectedGraph empty state —
renderEmptyStatenow checksnodesinstead ofedges, so a graph with nodes but no edges no longer shows the empty state. - ChoroplethMap validation — added GeoJSON-aware validation that checks for a
geometryproperty on area features, replacing the inapplicablevalidateArrayDatacheck. - "Rendered more hooks than during previous render" in
FlowMapandChoroplethMap— hooks were called after early returns for loading/empty states. All hooks now run unconditionally before any early return. colorScalecrash with null areas in ChoroplethMap —useMemonow returns a fallback sequential scale whenresolvedAreasis null during async loading.- Variable name collision in ChoroplethMap — local
areaStylerenamed toareaStyleFnto avoid collision with destructured prop. - Function
colorByproduced undefined colors —useColorScalenow derives categories from data whencolorByis a function and builds a proper ordinal scale.getColormaps non-CSS-color strings throughcolorScale. - LineChart validation —
validateArrayDatanow receives the rawdataprop instead of post-processedsafeData, so push API mode (dataundefined) correctly skips validation instead of triggering "No data provided". - QuadrantChart
sizeDomainNaN —sizeByvalues are now filtered to finite numbers before computing min/max, preventing NaN propagation to point radius.
- Home page: meaningful tooltips on bar chart, bubble chart, network graph (degree centrality)
- Streaming sankey pastel colors, chord multi-color fix
- Highlight hover uses distinct red line, more distinctive custom theme
- Top/bottom legend examples, chart container year controls work
- Responsive frame data fix, styling offset fix, linked dashboard color consistency
- Candlestick dark mode, uncertainty tooltip dark mode, isotype chart person icons
- Radar/isotype duplicate key fix, network explorer
.datawrapper access - Rosling bubble annotations/extent/tooltip, benchmark log scale fix, forecast sparkline card
- Force graph sparse preset parameters, choropleth playground sizing
- DocumentFrame: added 100+ missing prop names to
processNodes - Tile map: production provider documentation
emphasisprop — all charts acceptemphasis="primary" | "secondary".ChartGriddetectsemphasis="primary"on children and spans them across two grid columns for F-pattern dashboard layouts.directLabelrendering — new"text"annotation type inannotationRules.tsxsodirectLabellabels actually render. Automatic right margin expansion prevents label clipping.gapStrategyfixes —"break"now correctly splits lines at null boundaries using synthetic_gapSegmentgroup keys."interpolate"filters gap points in the HOC before data enters the pipeline, preventingresolveAccessor's unary+from coercingnullto0.- Chart States docs page (
/features/chart-states) — dedicated page for empty, loading, and error state documentation. Moved from LineChart and ChartContainer pages. - Gap strategy tabs — consolidated three separate subsections in LineChart docs into a tabbed interface.
- Tabs component — reusable tab switcher for docs pages.
- Export default format —
exportChart()now defaults to PNG instead of SVG. PNG export composites the canvas data layer underneath the SVG overlay, producing a complete chart image. SVG export only captures the overlay (axes, labels). - Type widening — eliminated unsafe any casts at HOC/Frame boundaries by widening
rFormat,oSort,colorBy, andTooltipFieldConfig.accessortypes in stream type definitions.
- Export captured only axes — PNG export now finds the
<canvas>element and draws it as the base layer before compositing the SVG overlay on top. directLabelannotations silently dropped —type: "text"was not a recognized annotation type; it fell through to the default case and returnednull.gapStrategy="break"drew lines through gaps — flattening re-merged segments because the Frame re-grouped by the originalgroupAccessor.gapStrategy="interpolate"dropped to zero —resolveAccessorused+(d)[key]which convertednullto0.colorBytype mismatch in network charts — hierarchy charts that color by depth index returned a number, but the type expected a string. AddedString()coercion.- Duplicate
amplitudeproperty inStreamOrdinalFrameProps.
Complete rewrite of Semiotic. Stream-first canvas architecture, 37 HOC chart components, full TypeScript, AI tooling, coordinated views, realtime encoding, and native server-side rendering.
Stream-first rendering. All frames are canvas-first with SVG overlays for
labels, axes, and annotations. Legacy frame names (XYFrame, OrdinalFrame,
NetworkFrame) have been removed entirely.
| Frame | Purpose |
|---|---|
StreamXYFrame |
Line, area, scatter, heatmap, candlestick charts |
StreamOrdinalFrame |
Bar, pie, boxplot, violin, swarm charts |
StreamNetworkFrame |
Force, sankey, chord, tree, treemap, circlepack |
Every frame supports a ref-based push API for streaming data.
Functional components + hooks. All components converted from class-based to
functional. Full TypeScript strict mode with generic type parameters on all
Frame and Chart components. "use client" directives for React Server
Components compatibility.
38 higher-order chart components that wrap the core Frames with curated, simple prop APIs.
XY Charts (wrap StreamXYFrame):
LineChart— line traces with curve interpolation, area fill, and point markersAreaChart— filled area beneath a lineStackedAreaChart— multiple stacked area seriesScatterplot— point clouds with color and size encodingConnectedScatterplot— sequential path through 2D space with Viridis gradientBubbleChart— sized circles with optional labelsHeatmap— 2D binned density visualization
Ordinal Charts (wrap StreamOrdinalFrame):
BarChart— vertical/horizontal bars with sort and color encodingStackedBarChart— stacked categorical barsGroupedBarChart— side-by-side grouped barsSwarmPlot— force-directed point distributionBoxPlot— statistical box-and-whiskerHistogram— binned frequency distributionViolinPlot— kernel density per categoryDotPlot— sorted dot stripsPieChart— proportional slicesDonutChart— ring variant of PieChart
Network Charts (wrap StreamNetworkFrame):
ForceDirectedGraph— force-simulation node-link diagramsChordDiagram— circular connection matrixSankeyDiagram— flow diagrams with weighted edgesTreeDiagram— hierarchical tree layoutsTreemap— space-filling hierarchical rectanglesCirclePack— nested circle packingOrbitDiagram— animated orbital hierarchy with solar/atomic/flat modes
Realtime Charts (canvas-based streaming):
RealtimeLineChart— streaming lineRealtimeHistogram— streaming histogram barsRealtimeSwarmChart— streaming scatterRealtimeWaterfallChart— streaming waterfall/candlestickRealtimeHeatmap— streaming 2D heatmaps with grid binning
All chart components feature:
- Full TypeScript generics (
LineChart<TDatum>) - Sensible defaults for width, height, margins, colors, hover
framePropsescape hatch for accessing the underlying Frame API- Automatic legend rendering when
colorByis set - Smart margin expansion to accommodate legends and axis labels
- Built-in error boundary (never blanks the page) and dev-mode validation warnings
Two SSR paths, both producing identical SVG output:
Component-level SSR — Stream Frames detect server context
(typeof window === "undefined") and render <svg> elements with scene
nodes instead of <canvas>. Same component, same props — works automatically
in Next.js App Router, Remix, and Astro.
Standalone SSR — semiotic/server entry point for Node.js environments
(email, OG images, PDF, static sites):
import { renderToStaticSVG } from "semiotic/server"
const svg = renderToStaticSVG("xy", {
lines: [{ coordinates: data }],
xAccessor: "date",
yAccessor: "value",
size: [600, 400],
})renderToStaticSVG(frameType, props)— generic entry pointrenderXYToStaticSVG(props)— XY-specificrenderOrdinalToStaticSVG(props)— ordinal-specificrenderNetworkToStaticSVG(props)— network-specific- Shared SceneToSVG converters used by both paths
decayprop — configurable opacity fade for older data (linear, exponential, step modes)pulseprop — glow flash effect on newly inserted data points with configurable duration/colortransitionprop — smooth position interpolation with ease-out cubic easingstalenessprop — canvas dimming + optional LIVE/STALE badge when data feed stops- All four features work on StreamXYFrame, StreamOrdinalFrame, and all realtime HOCs
- Features compose freely (e.g., decay + pulse creates a data trail with flash-on-arrival)
marginalGraphicsprop onStreamXYFrame,Scatterplot, andBubbleChart- Four types: histogram, violin, ridgeline, boxplot
- Margins auto-expand to 60px minimum when marginals are configured
LinkedCharts— cross-highlighting, brushing-and-linking, and crossfilter between any charts- Selection hooks:
useSelection,useLinkedHover,useBrushSelection,useFilteredData ScatterplotMatrix— N×N grid with hover cross-highlight or crossfilter brushingCategoryColorProvider— stable category→color mapping across charts
- Annotations with
type: "threshold"automatically split lines into colored segments - Interpolates exact crossing points between data samples
ThemeProviderwraps charts and injects CSS custom properties- Presets:
"light"(default) and"dark" useTheme()hook
ChartGrid— CSS Grid layout with auto columnsContextLayout— primary + context panel layout
semiotic/ai— HOC-only surface optimized for LLM code generationai/schema.json— machine-readable prop schemas for every component- MCP server (
npx semiotic-mcp) — renders charts as SVG tools for any MCP client- Per-component tools for all 21 SVG-renderable chart types
- Generic
renderCharttool accepting{ component, props } diagnoseConfigtool for anti-pattern detection
validateProps(componentName, props)— prop validation with Levenshtein typo suggestionsdiagnoseConfig(componentName, props)— anti-pattern detector with 12 checks:EMPTY_DATA,EMPTY_EDGES,BAD_WIDTH,BAD_HEIGHT,BAD_SIZE,ACCESSOR_MISSING,HIERARCHY_FLAT_ARRAY,NETWORK_NO_EDGES,DATE_NO_FORMAT,LINKED_HOVER_NO_SELECTION,MARGIN_OVERFLOW_H,MARGIN_OVERFLOW_V- CLI (
npx semiotic-ai) —--schema,--compact,--examples,--doctor CLAUDE.md— instruction file for Claude, Cursor, Copilot, Windsurf, and Cline- Schema freshness CI — cross-references schema.json, VALIDATION_MAP, and CLAUDE.md
onObservation— structured events (hover, click, brush, selection) on all HOCsuseChartObserver— aggregates observations across LinkedChartstoConfig/fromConfig/toURL/fromURL/copyConfig/configToJSX— chart serializationfromVegaLite(spec)— translate Vega-Lite specs to Semiotic configsexportChart()— download charts as PNG (default) or SVGChartErrorBoundary— React error boundaryDetailsPanel— click-driven detail panel insideChartContainer- Data transform helpers (
semiotic/data):bin,rollup,groupBy,pivot TooltipandMultiLineTooltipcomponents with field-based configuration- Keyboard navigation utilities
Eight separate entry points for reduced bundle sizes:
| Entry Point | Contents |
|---|---|
semiotic |
Full library |
semiotic/xy |
XY Frame + XY charts |
semiotic/ordinal |
Ordinal Frame + ordinal charts |
semiotic/network |
Network Frame + network charts |
semiotic/realtime |
Realtime charts |
semiotic/server |
SSR rendering functions |
semiotic/ai |
HOC-only surface for AI generation |
semiotic/data |
Data transform utilities |
Extracted shared logic from all HOC chart components into reusable hooks:
useChartSelectionhook — selection/hover setup used by 21 chartsuseChartLegendAndMarginhook — legend + margin auto-expansion used by 18 chartsbuildOrdinalTooltiphelper — shared tooltip builder for ordinal charts- Network utilities —
flattenHierarchy,inferNodesFromEdges,resolveHierarchySum,createEdgeStyleFn
- Rollup 2.x → Rollup 4.x with Terser minification
- Modern ESM output with
constbindings (ES2015 target) sideEffects: falsefor aggressive tree-shaking- Modern
exportsfield in package.json for proper ESM/CJS resolution
Minimum React version is now 18.1.0 (was 16.x in v1, 17.x in v2). Also supports React 19.
The monolithic processing/network.ts has been split into focused layout plugins:
sankey, force, chord, tree, cluster, treemap, circlepack, partition.
- All legacy frames —
XYFrame,OrdinalFrame,NetworkFrameand their Responsive/Spark variants. UseStreamXYFrame,StreamOrdinalFrame,StreamNetworkFrame. FacetController— useLinkedChartsRealtimeSankey,RealtimeNetworkFrame— useStreamNetworkFramewithchartType="sankey"baseMarkProps,ProcessViz,Mark,SpanOrDiv— removed internal utilities
- Chord diagram arc/ribbon angle alignment
- Stacked area streaming flicker (stable sort of groups)
- Violin plot IQR positioning
- Sankey particle colors
- Canvas clip region (marks no longer draw into margins)
- Tooltip position flipping at chart edges
- Stacked bar color encoding, streaming aggregation, and category flicker
- Force layout initial positions (phyllotaxis spiral)
- Treemap hover (smallest containing rect wins)
- Axis label floating-point noise and overlap
- ThemeProvider integration with SVG overlay axes and canvas background
- HOC chart data validation (visible error element instead of blank)
- 30+ additional rendering, theming, and coordination fixes
Version 2.0 was an internal milestone that began the transition from class components to functional components and introduced initial TypeScript support. It was never promoted to a stable release.
Notable changes from v1:
- Initial functional component conversions
- TypeScript adoption began
- React 17 compatibility
- Add
customClickBehaviorwith hover pointer state for legend interactions - Make difference between vertical and horizontal group rendering explicit
- Fix canvas interactivity with custom canvas function
For the complete v1.x changelog, see the git history.