Skip to content

Release history

nteract/semiotic releases

React data visualization MCP server with 30+ chart types. 5 tools: suggest charts for a dataset, render validated React configs to SVG, diagnose configuration anti-patterns, get component schemas, and report issues.

All releases

19 shown

No immediate action
v3.6.0 New feature

`semiotic/ai` API surface

No immediate action
v3.5.4 New feature

Band prop + tooltip enhancements

No immediate action
v3.5.3 New feature

DifferenceChart + axisExtent

No immediate action
v3.5.2 Breaking risk

New hooks + unification + TS migration

v3.5.1 Bug fix
Notable features
  • Added explicit extent examples and tests for chart-level xExtent/yExtent pass-through
Full changelog

Added

  • Added explicit extent examples and tests covering chart-level xExtent/yExtent pass-through.

Fixed

  • Fixed yExtent handling 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.
v3.5.0 Breaking risk
Notable features
  • `tooltip="multi"` hover-anywhere tooltips for LineChart, AreaChart, and StackedAreaChart — multi‑series tooltip anywhere inside the x span.
  • `useHydrationLifecycle` hook – extracts post‑hydration paint pattern into a reusable hook used by all Stream Frames.
Full changelog

Added

  • 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 within hoverRadius of an explicit data point. This uses the shared StreamXYFrame multi-tooltip path, so multi hovers are cursor-anchored within the data range. Interpolation remains generous between sparse path samples but is range-bounded so explicit xExtent padding 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-space xValue/xAccessor data for linked crosshair and observations. SSR is unchanged because tooltips are gated on pointer-driven hoverPoint.

  • useHydrationLifecycle hook — extracts the post-hydration paint pattern that was previously duplicated as a 12-line useEffect across all four Stream Frames (StreamXYFrame, StreamOrdinalFrame, StreamNetworkFrame, StreamGeoFrame). Each frame now has a 7-line useHydrationLifecycle({ ... }) 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 via renderFnRef.current(). Frame-specific cleanup (XY/Ordinal clearing the streaming adapter, Geo clearing tile cache) is supplied via the cleanup option. Adding hydration support to a hypothetical fifth frame is now a single hook call instead of a copy-paste-and-modify exercise.

  • HYDRATION.md integration recipesrc/components/stream/HYDRATION.md codifies the six-step pattern for adding hydration support to a new Stream Frame: import hooks, gate SSR branch on isServerEnvironment || (!hydrated && wasHydratingFromSSR), attach responsiveRef on the SVG branch, wire useHydrationLifecycle, implement cancelIntroAnimation() on the store, mark the bundle clientOnly: 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 gateintegration-tests/ssr-parity.spec.ts + the new ssr-parity-examples/ fixture snapshot both server-rendered SVG (via renderChart) and client-rendered canvas for the same chart matrix (LineChart, BarChart, PieChart, SankeyDiagram, Treemap). The SSR side renders into a page.setContent payload — 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 testsrc/components/server/ssr-csr-parity.test.tsx exercises the two SSR code paths (renderChart from semiotic/server and the in-frame SSR branch via renderToString(<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 (renderChart is 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:

    • hoverHighlight dim (LineChart multi-series with hoverHighlight: true): pointer over series A, series B should dim. Fixture in xy-examples/index.js + test in xy-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:first for series A, capture series B dim.
    • Linked-hover cross-highlight (existing linked-hover fixture in coordinated-examples/index.js): pointer over the scatter half of the LinkedCharts dashboard, capture the matching-category bars staying lit while non-matching dim. Test in brush-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 existing xy-scatter-hover-state snapshot. maxDiffPixels: 200 because 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) and OrbitDiagram. 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 static data arrays + omit the continuous-animation props (decay/pulse/transition/staleness for realtime, animated: false for Orbit). Without those props, the canvas stabilizes after initial paint and ordinary waitForChartReady (default stable: true) succeeds — no rAF instrumentation needed. Fixtures live alongside the streaming-mode siblings in realtime-examples/index.js (with seedHistData / seedWaterfallData / seedSwarmData / seedHeatmapData constants) and network-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 in chartSpecs.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 CI playwright-snapshots artifact — 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: DotPlot and RidgelinePlot in ordinal-frame.spec.ts (fixtures land in ordinal-examples/index.js), FlowMap and DistanceCartogram in geo-charts.spec.ts (fixtures in geo-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 shared toHaveScreenshot body. 12 darwin baselines committed (4 charts × 3 browsers). FlowMap fixture pins 5-node + 5-edge synthetic flow data with a simpleAreas background; DistanceCartogram pins a 5-node hub+spokes layout with cost accessor 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 in integration-tests/xy-examples/index.js (deterministic small data, colors palette where categorical) + a toHaveScreenshot test in xy-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_LINUX check in .github/workflows/node.js.yml); commit them from the playwright-snapshots artifact 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.ts swap (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 cross maxDiffPixels: 100 on 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 windowSize to bounded data — closes the Consumer Workaround Audit P0 item — new resolveRealtimeWindowSize(windowSizeProp, data) helper at src/components/charts/realtime/resolveWindowSize.ts is wired into all 5 realtime HOCs (RealtimeLineChart, RealtimeHistogram, RealtimeSwarmChart, RealtimeWaterfallChart, RealtimeHeatmap) where the prior windowSize = 200 destructure default lived. Resolution rule: explicit user windowSize always wins (a 10-point window over a 100-point archive is a legitimate "show the last 10" view); otherwise auto-fit to Math.max(data?.length ?? 0, 200). Closes the historical windowSize={data.length} workaround that consumers had to write to keep static data arrays larger than 200 points from silently truncating against the sliding-window cap during bulk ingest. Our own docs/src/pages/theming/SemanticColorsPage.js had the workaround on the histogram example — removed in the same diff. Streaming-only consumers see no change (the 200 floor preserves the prior default when data is absent or empty); push API behavior is identical (the buffer can still grow via windowMode="growing" or be consumed via ref.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 for windowSize={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 validationMap Composition 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 any cleanup 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.tsgetColor(dataPoint: any, …, colorScale?: (v: any) => string) and getSize(dataPoint: any, …) re-typed with Datum and (v: string) => string. The internal dataPoint[colorBy] access (which is genuinely unknown-shaped because Datum is loose) now stringifies once at the seam, matching what d3 ordinal scales do internally and threading string[] cleanly through scaleOrdinal<string, string> without the prior as (v: any) => string casts on the return type.
    • SceneToSVG.tsx — replaced 3 ({} as any) arc-noop calls with a typed ARC_NOOP: DefaultArcObject constant; replaced the dominantBaseline cast-to-any with a cast to React's typed SVGAttributes["dominantBaseline"] union (the cast stays — it's the boundary between our free-form NetworkLabel.baseline string and React's strict SVG-spec union — but it's no longer a type-safety bypass).
    • hierarchyLayoutPlugin.ts — typed every root: any parameter as HierarchyNode<Datum>, every d: any position-setter parameter as the layout-specific HierarchyPointNode<Datum> / HierarchyRectangularNode<Datum> / HierarchyCircularNode<Datum> (matching d3's per-layout node-type contract), and the descendant nodeMap from Map<any, RealtimeNode> to Map<HierarchyNode<Datum>, RealtimeNode>. The 3 layout-specific subtype casts at the dispatch site are the genuine boundary — layoutType discriminant carries the type information, but TS can't see it without an explicit cast — and they're typed-cast not any-cast.
    • orbitLayoutPlugin.ts — same treatment for the orbit-specific layout (buildOrbitLayout(root: Datum), buildTree(parentDatum: Datum), pieGen.value((kid) => …)); typed the __orbitState cache via the existing unknown field on NetworkPipelineConfig; replaced the revolution-style callback's (n: any) => number with a structural DepthLike shape ({ depth?: number }) that names exactly what the function reads; dropped a null as any SceneDatum that was just a forgotten cast (SceneDatum is already Datum | null).
    • chordLayoutPlugin.ts — the per-node arcData and per-edge chordData extension fields are now declared as typed unknown-bag fields on RealtimeNode / RealtimeEdge (the same pattern __hierarchyNode / __radius use), so callsites narrow at the read site instead of (node as any).arcData. The arc.centroid() argument now constructs an explicit DefaultArcObject from the ChordGroup rather than casting; the one remaining cross-type cast (ChordribbonGenerator's expected Ribbon parameter, where d3's configured-radius generator never reads the missing radius field) is now a typed as unknown as Parameters<typeof ribbonGenerator>[0] boundary cast with a comment instead of as any.
    • XYBrushOverlay.tsx + OrdinalBrushOverlay.tsx — typed the d3-brush ref as BrushBehavior<unknown>, the brush event callbacks as D3BrushEvent<unknown>, and the brush-group selection as Selection<SVGGElement, …> so all 12 as any casts on g.call(brushFn) / g.call(brushFn.move, …) resolve cleanly through d3-brush's typed call signatures. Brushes are now fully typed at the d3 boundary — no any remains 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.tsx SSR 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 as any. New any in 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.json and removed the four where the dependency was clearly bigger than the surface we used:

    • d3-scale-chromaticsrc/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 effective Rgb#toString() shape (#rrggbb for 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 of GeoTileRenderer.ts. The tile() 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." tileWrap is a one-line antimeridian wrap. ~20KB unpacked saved.
    • d3-formatsrc/components/charts/shared/numberFormat.ts. Implements the chart-axis-relevant subset of d3's spec syntax: [,][.precision][~][type] with types f, %, e, d, s, r, g, plus the unparseable-spec fallback to Intl.NumberFormat. 19 round-trip tests lock in parity with d3 for the format strings semiotic emits internally and the realistic spec subset consumers pass via xFormat/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-formatsrc/components/charts/shared/timeFormat.ts. strftime token parser backed by Intl.DateTimeFormat pinned to en-US for %b/%B/%a/%A (matches d3-time-format's default-locale behavior — timeFormatDefaultLocale is 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, %Y form and each token. Dropped %U/%W/%Z/%c/%x/%X/%f — none appear in chart axis labels. Also drops the transitive d3-time (~40KB) since d3-time-format was 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/realtime bundles. No public API change; the four removed packages plus @types/d3-scale-chromatic/@types/d3-format/@types/d3-time-format come out of package.json. Companion analysis identified d3-brush/d3-selection/d3-zoom as 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 via check:pack.

  • check:jsdoc-coverage gate — new scripts/check-jsdoc-coverage.mjs enforces a minimum agent-visible documentation surface on every HOC registered in chartSpecs.ts: a top-line one-sentence summary (TypeDoc renders this as the component blurb on /api/charts) and at least 2 @example blocks (examples drive /api/typedoc, generated AI docs, and the MCP getSchema prompt 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: AreaChart and StackedAreaChart (a single @example apiece — both got a second covering gradients/normalization), and all 5 realtime charts (1 @example each, plus RealtimeHistogram had 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 @deprecated block stays honest. Wired into release:check, prepublishOnly, and the CI workflow alongside check:test-quality. Closes the API Reference Documentation P0 next-work bullet.

  • check:ai-examples-coverage gate — new scripts/check-ai-examples-coverage.mjs catches drift in ai/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 in chartSpecs.ts but its section in examples.md survived, 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 like ThemeProvider / LinkedCharts / Tooltip so they don't false-positive); (2) coverage gaps — a renderable chart was added to chartSpecs.ts but examples.md was never updated, so MCP / --doctor agents 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-way COVERAGE_BASELINE set inline in the script — mirrors check: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 @example blocks are still enforced by check:jsdoc-coverage, and MCP / --doctor agents still discover each chart through chartSpecs.ts + getSchema, so the baseline charts aren't agent-invisible — just narrative-light in the canonical copy-paste file. Wired into release:check, prepublishOnly, and the CI workflow alongside check: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 by check: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 across bar/area/line/pointCanvasRenderer.ts in byte-identical form: resolveCurveFactory(curve) (the d3-shape token switch was duplicated identically between area and line; adding a curve token previously required two lockstep edits), resolveCanvasFill(ctx, fill, fallback) (replaces the (typeof X === "string" ? resolveCSSColor(ctx, X) : X) || fallback form that appeared 5+ times across the four renderers and silently fell back to #000000 when consumers passed CSS-variable strings), buildLinearFillGradient(ctx, fillGradient, baseFill, x0, y0, x1, y1) (replaces barCanvasRenderer's buildBarGradient and areaCanvasRenderer's inline gradient block — both implemented the same colorStops / topOpacity two-shape switch with the same offset clamping and the same parseCanvasColor opacity-form normalization; bar already filtered NaN offsets before the 2-stop minimum check, area filtered them inline and could silently render a 1-stop transparent fill — the helper unifies on bar's stricter behavior, with the renderer falling back to flat fill when the helper returns null), and buildColorStopGradient(ctx, strokeGradient, x0, y0, x1, y1) (replaces the stroke-side colorStops gradient construction in area's top-stroke branch and line's stroke branch). Renderer-specific path tracing (rounded-corner bar paths, area decay strips, line threshold-color crossings, area traceAreaPath with 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). New canvasRenderHelpers.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 a findNearestSceneNode hit-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-quality baseline (156) unchanged.

  • setupCanvasMock adoption sweep — four rAF stubbing flavors + leak-tight cleanupsetupCanvasMock now accepts stubRaf: 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, false for 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 → pipelineConfig regression specs in StreamGeoFrame.test.tsx and StreamNetworkFrame.test.tsx now use this), and "microtask" for tests where sync fire would recurse scheduleRender but 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 previous teardownMocks only restored rAF/cAF and leaked getContext + Path2D into later test files via the shared HTMLCanvasElement.prototype and globalThis, which the helper's symmetric capture-then-restore now closes. Sync-fire flavor returns rAF id 0 deliberately: useFrame.scheduleRender treats rafRef.current as a truthy "pending" flag (if (rafRef.current) return), and the assignment sequence rafRef.current = requestAnimationFrame(cb) lets a non-zero return overwrite the renderer's own rafRef.current = 0 reset and silently coalesce the next scheduleRender into a phantom pending rAF. Caught during the migration when the previously-inlined sync stubs in DotPlot.streaming-order / LikertChart.streaming-order / StreamOrdinalFrame / StreamXYFrame push-API specs started returning getScales() === undefined after 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. noop and microtask flavors return monotonically-increasing ids because their callers never relied on the truthy-flag invariant. The paired cancelAnimationFrame mock now honors cancellation for the "microtask" flavor (tracks ids in a cancelled set; the deferred callback no-ops on fire) so production cleanup paths — useFrame unmount, DataSourceAdapter chunk timers, MinimapChart polling — 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-quality baseline (156 mount-only candidates) unchanged.

  • Shared AI/MCP component metadataai/componentMetadata.cjs is now the shared source for component category, import, renderability, and registry metadata across the CLI, MCP resources, and surface-parity checks. check:surface is wired into release gates so schema, semiotic/ai, MCP renderability, and server-renderer support cannot drift silently.

  • Shared chart recommendation engineai/chartSuggestions.cjs powers both MCP suggestChart and npx semiotic-ai --suggest, with recommendations for network, hierarchy, geographic, temporal, categorical, and magnitude data shapes. The CLI supports stdin on all platforms via fd 0.

  • MCP protocol smoke coveragesrc/__tests__/scenarios/mcp-protocol.test.ts exercises stdio JSON-RPC and Streamable HTTP initialization/tools-list flows, including robust parsing for JSON and SSE responses.

  • API docs extraction coverageapi-docs-extraction.test.js locks down TypeDoc re-export resolution, props alias handling, function-signature formatting, examples, inherited-prop labels, and component summaries.

  • Docs route smoke checkcheck:docs-routes validates prerendered homepage/API routes, route metadata, sitemap entries, and generated API JSON assets; website:build now runs it after prerender.

  • AI behavior contractsai/behaviorContracts.cjs is 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 --doctor and MCP diagnoseConfig now accept usageMode: "static" | "push" so static/renderChart configs still require data while ref-push React HOCs can intentionally omit it. The MCP semiotic://behavior-contracts resource and generated AI docs consume the same rule metadata; check:ai-contracts is wired into release gates.

  • Test quality gatecheck:test-quality baselines 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 subscriptionsStreamOrdinalFrame and StreamXYFrame expose legendCategoryAccessor and onCategoriesChange; pushed legends now populate, shrink, relabel, and clear after insert/remove/update/clear. LinkedCharts has 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, and barColors changes without data ingest.

  • HOC JSDoc coverage — every public HOC (38/38) now ships with at least 2 @example blocks, a top-line summary that cross-references sibling charts via {@link}, and @default annotations 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 helperssrc/components/charts/shared/streamPropsHelpers.ts exposes three pure helpers that replace the spreads recurring in every XY/ordinal HOC's streamProps = { ... } literal: buildBaseMetadataProps (the ...(title && { title }) chain across title/description/summary/accessibleTable/className/animate, with per-field truthy-vs-defined gates that match the inline form), buildTooltipProps (the tooltip === false ? () => null : (normalizeTooltip(tooltip) || defaultTooltipContent) ternary), and buildCustomBehaviorProps (the conditional customHoverBehavior / customClickBehavior spread, with a linkedHoverInClickPredicate flag so geo / CandlestickChart-style HOCs that exclude linkedHover from 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-state tooltip === "multi" branch, ChoroplethMap's 4-state tooltip === true branch, the geo HOCs' resolved.X metadata 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. Unused normalizeTooltip imports 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 playbookserver.json at the repo root refreshed for the official MCP Registry publish flow (version + npm identifier sync'd to current package.json, title/websiteUrl/registryBaseUrl fields added that the registry validator wants). README gained an mcp-name: io.github.nteract/semiotic literal 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). New check:mcp-registry gate (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 into release:check, prepublishOnly, and the CI workflow alongside check:context7. New top-level DISCOVERABILITY.md documents 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.md is the maintenance playbook.

  • Context7 manifest + freshness gatecontext7.json lives 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/) with excludeFolders for dist/build/node_modules/snapshot dirs, plus a rules array distilled from behaviorContracts.cjs (sub-path imports, push-mode data semantics, ID-accessor requirements for remove/update, categorical color precedence, geo-import discipline, required prop combinations, server-rendering boundaries). New check:context7 gate (scripts/check-context7.mjs) validates JSON syntax, the 255-char-per-rule limit Context7 silently rejects on, that every folders entry resolves on disk, and that the sub-path rule's import names line up with package.json's exports keys. Wired into release:check, prepublishOnly, and the CI workflow. behaviorContracts.cjs carries an inline maintenance note pointing edits at context7.json so 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? }) at src/components/charts/shared/useFrameImperativeHandle.ts — extracts the 7-method RealtimeFrameHandle bridge (push / pushMany / remove / update / clear / getData / getScales) every HOC implemented inline. Three variants: "xy" (vanilla pass-through to frameRef.current), "network" (topology-walking removeNode / updateNode), and "geo-points" (removePoint-based with emulated update). HOCs with bespoke wrappers — BubbleChart's wrapped push that tracks the streaming size domain, SankeyDiagram / ChordDiagram's edge-shaped getData — pass overrides to selectively replace methods while keeping the variant defaults for the rest. MultiAxisLineChart and LikertChart keep 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-useChartMode alias bundle into a 9-to-11-line block of const X = resolved.X lines. 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 ?? false fallback case that ForceDirectedGraph uses (translates cleanly to the destructure-default showLabels = false form because the ChartModeResult shape allows only boolean | undefined).
  • useChartSetup unification closed — the standing question of whether useChartSetup should grow optional inputs for dual-axis (MultiAxisLineChart) and projection (geo) cases is resolved with no API change. MultiAxisLineChart already integrates by passing colorBy: SERIES_FIELD (a synthetic series field) and bumping marginDefaults.left/right to 70 for the dual-axis layout — the unitization step that makes the chart dual-axis lives above setup, not inside it. ProportionalSymbolMap, FlowMap, and DistanceCartogram integrate cleanly today with the existing setup surface; the d3-geo projection lives inside StreamGeoFrame, not in any HOC-reachable seam, so there is no "pre-projected coordinates" path that wants exposing. ChoroplethMap is a separate shape (sequential gradient legend driven by a value scale, not categorical) and is intentionally outside useChartSetup's scope. Outcome: no new optional inputs added, no new consumers in the queue — the unification work is complete.

  • LineChart drops standalone useStreamingLegenduseChartSetup owns the full legend pipeline — LineChart used to layer a separate useStreamingLegend call on top of useChartSetup: that hook re-tracked discovered categories via wrapPush/wrapPushMany, registered them with the parent LinkedCharts via useLinkedChartCategories, and produced a streamingLegend element + streamingMarginAdjust that the HOC merged on top of setup.legend / setup.margin. After this change, useChartSetup already does the equivalent end-to-end: useChartLegendAndMargin (called inside setup) registers the live category domain with LinkedCharts, and the synthesized legendColorScale + setup.legend carry the same provider → scheme → theme → STREAMING_PALETTE precedence the marks use. LineChart now spreads setup.legendBehaviorProps (which includes legendCategoryAccessor + onCategoriesChange for the frame, plus the legend slot and legend-interaction handlers) and reads setup.legend / setup.margin directly. useStreamingLegend stays 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 at setup.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), and GroupedBarChart.test.tsx (1) replaced their expect(frame).toBeTruthy() mount-only assertions with semantic checks against the props the HOC actually forwards to StreamOrdinalFrame: chartType, data, oAccessor/rAccessor, projection, oSort (plus a sortedness check on the pre-sorted data when sort is "asc"/"desc"), barPadding, enableHover, showGrid, size, stackBy, groupBy, normalize, legend.legendGroups[0].items (distinct colors per category for colorScheme checks), and frameProps escape-hatch overrides. Caught and locked in a real distinction along the way: GroupedBarChart routes through chartType: "clusterbar", not "bar". The intentional sparse-array hardening test gets the documented // test-quality-gate: allow-mount-only opt-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 across accessibility.spec.ts, brush-selection.spec.ts, coordinated-views.spec.ts, geo-charts.spec.ts, hoc-legend.spec.ts, realtime-charts.spec.ts, and streaming-regression.spec.ts replaced with semantic aria-label regex matches (/\d+/). The data canvas's aria-label is set by computeCanvasAriaLabel only 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 past toBeVisible() but trip the regex. The coordinated-views hover test now asserts a .stream-frame-tooltip element 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.
  • 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 inside useChartSetup, useColorScale, inferNodesFromEdges, the choropleth geometry validator, and the StreamFrame ingestion path. A new shared identity-preserving helper at src/components/charts/shared/sparseArray.ts#filterSparseArray returns the original reference when nothing is dropped (preserving useMemo cache hits in the clean case) and is wired through every ingestion seam:

    • useChartSetup filters its data input and exposes the sanitized array as setup.data; the empty-state check filters rawData too, so [null, undefined] lands on the empty-state UI rather than rendering blank.
    • DataSourceAdapter filters setBoundedData, setReplacementData, push, and pushMany. Push-mode ref.pushMany([null, valid]) now silently drops the null and lands the valid row instead of crashing extent reads inside the pipeline store.
    • StreamGeoFrame.pushPoint/pushMany and StreamNetworkFrame.pushEdge/pushManyEdges mirror the same filtering at the geo/network frame boundaries (those frames don't route through DataSourceAdapter).
    • 26 HOCs replaced their safeData = data || [] with the identity-preserving filter; MultiAxisLineChart's series prop, the network family's nodes/edges, and ChoroplethMap's resolved areas got the same treatment.
    • ForceDirectedGraph/SankeyDiagram/ChordDiagram empty-state checks route through their filtered arrays so sparse-only input triggers empty UI.
    • ProportionalSymbolMap and FlowMap were migrated to drop their own pre-setup filter and read setup.data directly (eliminates the double-scan when useChartSetup would re-filter the same array).
    • A new EMPTY_ARRAY singleton (frozen) is used as the stable push-mode default in HOCs that need an array literal; per-render churn through the sparse-filter useMemo is gone.
    • New regression test src/__tests__/scenarios/sparse-array-hardening.test.tsx covers 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.tsx exercises the legend-color synthesis precedence (CategoryColorProvider → explicit colorScheme array → string scheme name → ThemeProvider categorical → default theme) against LineChart (xy / useStreamingLegend path) and ProportionalSymbolMap (geo / useChartSetup path) mounted in push mode for each tier. 10 tests total. The "bare push" tier's behavior is documented inline: it resolves to LIGHT_THEME.colors.categorical because the theme store seeds a non-empty default categorical palette, leaving STREAMING_PALETTE as a defense-in-depth fallback rather than a reachable production path. Negative-anti-source assertions catch any regression that surfaces STREAMING_PALETTE from a wrongly-ordered precedence chain.

  • StreamGeoFrame push-mode legend category emissionStreamGeoFrame now reads legendCategoryAccessor + onCategoriesChange from props and emits the live category domain after every scene rebuild, mirroring StreamXYFrame / StreamOrdinalFrame. Push-mode geo HOCs (ProportionalSymbolMap, FlowMap) now propagate their discovered categories back through useChartSetup's frameCategories state, 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 in StreamGeoFrame.test.tsx exercises the emission across pushMany / removePoint / clear so 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.ts is 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 in scripts/lib/chart-specs-generators.mjs (generateSchemaToolEntry, generateValidationMapEntry, generateMetadataEntry) produce ai/schema.json, validationMap.ts, and componentMetadata.cjs entries from each ChartSpec; scripts/regenerate-schema.ts re-baselines ai/schema.json from the registry. A 130-test round-trip suite (chart-specs-round-trip.test.ts) iterates over CHART_SPECS and 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 that CHART_SPECS keys exactly match the canonical name sets in ai/schema.json, validationMap.ts, and ai/componentMetadata.cjs. check:chart-specs is wired into release/prepublish gates and the CI workflow. Adding a new chart is now one edit (a ChartSpec entry) plus regeneration; previously it required three coordinated hand-edits across schema/validation/metadata files.

Changed

  • 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/typedoc now 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 bridgeuseFrame owns 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 LinkedCharts legends agree.
  • HOC shared setup unificationLineChart, AreaChart, StackedAreaChart, QuadrantChart, ConnectedScatterplot, BubbleChart, ProportionalSymbolMap, FlowMap, and DistanceCartogram now use useChartSetup for 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. useChartSetup itself now synthesizes a push-mode legend color scale from discovered categories using the same precedence as useColorScale (provider → explicit scheme → theme → STREAMING_PALETTE), so legend swatches and rendered marks agree on every converted chart without each one needing to layer useStreamingLegend separately.
  • StackedAreaChart AI contractareaBy is 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.

Removed

  • 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 inside check:schema is preserved as a slim, focused gate at scripts/check-claude-md-coverage.js (npm run check:claude-md-coverage). check:surface no longer asserts schema↔validation name parity (also covered by registry round-trip) and instead focuses on semiotic/ai exports, MCP renderable registry entries, AI component metadata, and server renderChart configs. CI workflow and release/prepublish gate scripts are updated accordingly.

Fixed

  • Production builds preserve "use client" after tersernpm run dist:prod was failing the post-build directive-placement gate: every client bundle's minified output came out missing the directive. Root cause: useClientPlugin.renderChunk was prepending "use client"; to the output, then terser's renderChunk ran 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: split useClientPlugin's registration so its transform hook still scans modules during the parse phase but its renderChunk is 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.ingest is idempotent on identical bounded data refs — the SSR branch in every Stream Frame calls store.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 returns false immediately 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 used code.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 with hasLeadingUseClientDirective() 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 that clientOnly: true bundles 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).
  • cancelIntroAnimation now clears per-node _introClipFractionsynthesizeIntroPositions sets _introClipFraction = 0 on line and area scene nodes (the canvas renderers consume it directly to clip the path from the left). The earlier Phase 4 cancelIntroAnimation cleared prevPositionMap / prevPathMap / activeTransition but 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 to undefined for line / area nodes. Regression test in PipelineStore.cancelIntro.test.ts asserts 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 to if (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.
  • createStore no longer calls createContext at module load — even with the "use client" directive removed from semiotic/server, the bundle still pulled in createStore.tsx, which called React.createContext eagerly when each store factory ran. React Server Components ship a build of react that omits createContext entirely, so importing semiotic/server from a Server Component threw (0, p.createContext) is not a function before renderChart ever ran. Refactored createStore to defer the createContext call until Provider / useSelector actually executes — both of which only run on the client. Added an RSC import safety regression test that mocks react to throw on createContext and asserts the factory call doesn't trip it. Caught by the SSR demo's manual-placeholder route at request time.
  • ForceDirectedGraph accepts nodeIdAccessor (camelCase) — the historical prop name was nodeIDAccessor (uppercase ID), which was inconsistent with the rest of the network HOCs (SankeyDiagram, ChordDiagram, TreeDiagram, OrbitDiagram all use camelCase nodeIdAccessor). The casing inconsistency surfaced during the SSR demo's verification matrix. Both prop names are accepted now; nodeIdAccessor is canonical and nodeIDAccessor is a @deprecated alias slated for removal in 4.0. When both are passed, nodeIdAccessor wins. Codemod follow-up tracked for the external semiotic-codemod repo: a force-directed-graph-node-id transform that renames the JSX attribute on existing <ForceDirectedGraph> usages.
  • semiotic/server no 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. Calling renderChart (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-bundle serverOnly flag 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 from semiotic/server.
  • HOC JSDoc accuracy pass — 13 doc inaccuracies caught and corrected against source: ForceDirectedGraph nodeSize / nodeSizeRange defaults, push-mode opt-in for nodes/edges, and the fabricated frameProps.initialPositions; PieChart.startAngle units (degrees, not radians) and the matching example value; PieChart.valueAccessor aggregation by Math.abs for negatives; QuadrantChart example commentary; ChoroplethMap.areas shape (Feature[], not FeatureCollection) and the async resolveReferenceGeography example pattern; MinimapChart examples that referenced non-existent chart, minimapHeight, initialExtent, onBrushChange props; ScatterplotMatrix example that wrapped the matrix in an outer <LinkedCharts> even though the component already creates its own provider internally; ProportionalSymbolMap push example missing the id field required by pointIdAccessor. 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:url metadata are normalized together, including the homepage URL shape.
  • SSR alignment test isolationssr-alignment.test.ts checks a temporary SceneToSVG copy instead of mutating tracked source files during parallel Vitest runs.
  • GaugeChart LLM docs — machine-readable docs now correctly list thresholds as optional, matching the TypeScript API.
  • validateProps data shape handling — the "none" data shape is handled explicitly so no-data components do not accidentally fall through realtime validation assumptions.

Tooling

  • 3324 unit tests pass after the latest backlog cleanup and cache regression additions.
v3.4.2 Breaking risk
⚠ Upgrade required
  • Removed `react-router`, `marked-gfm-heading-id`, and `tslib` from runtime dependencies; they are now dev‑dependencies only.
  • Explicitly added `@testing-library/dom` as a devDependency to prevent missing peer issues.
Notable features
  • Added `gradientFill` to `BarChart` with opacity fade, explicit stops, and multi‑color gradients; supports both canvas and SVG/SSR via new helpers.
  • Migrated JSX transform to automatic runtime (`jsx: react-jsx`) aligning with React 17+ guidance.
Full changelog

Added

  • gradientFill on BarChart — same API as AreaChart.gradientFill: true for 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 with roundedEdge unconditionally (previously only when roundedTop > 0) so gradient direction resolves without requiring rounded corners. New buildBarGradient helper in barCanvasRenderer.ts builds the CanvasGradient per bar; StackedBarChart / GroupedBarChart get it for free via the shared scene + renderer.
  • SVG / SSR rendering for bar gradientsordinalSceneNodeToSVG in SceneToSVG.tsx emits <defs><linearGradient> + fill="url(#id)" for rect nodes carrying fillGradient. Works through both renderToStaticSVG (server) and animatedGif (GIF export) automatically since both delegate to the shared scene-to-SVG converter. Uses gradientUnits="userSpaceOnUse" with absolute coords so each bar's gradient tracks its own rect. New safeSvgId helper coerces category names containing spaces/punctuation to a legal SVG id charset before embedding them in the gradient's id and url(#...) reference.
  • renderers/colorUtils.ts — shared parseCanvasColor(ctx, color) used by both barCanvasRenderer and areaCanvasRenderer. Resolves any valid CSS color (named like "steelblue", hsl(), rgb(), hex short/long) to an [r, g, b] tuple via a ctx.fillStyle round-trip that the browser normalizes. Uses a sentinel-probe pattern so silently-rejected invalid colors (canvas ignores them and leaves fillStyle at the previous value) fall back safely instead of being mis-parsed as the prior color. Unified the two previously-duplicated local parseColor helpers. Bar-gradient dev demos on /charts/bar-chart cover three shapes: opacity fade, multi-color stops, and horizontal bars.

Changed

  • JSX transform flipped to the automatic runtime. tsconfig.json and tsconfig.mcp.json now use "jsx": "react-jsx" instead of the classic "jsx": "react". JSX compiles to imports from react/jsx-runtime rather than React.createElement(...), matching React 17+ guidance and removing the "outdated JSX transform" runtime warning that was peppering test output. ESLint flat config layers reactPlugin.configs["jsx-runtime"].rules on top of recommended to disable react/react-in-jsx-scope and react/jsx-uses-react (both obsolete under the new runtime). 17 test files lost their now-redundant import React from "react" imports; the 51 files that reference React.X types kept theirs. Rollup's external predicate in scripts/build.mjs extended to cover the react/jsx-runtime and react/jsx-dev-runtime subpaths alongside the existing react-dom/server entry (auto-external marks package roots external but not subpaths).
  • Empty legends no longer reserve margin. useChartLegendAndMargin previously returned a truthy legend object with legendGroups: [{ items: [], label: "" }] when mounted with no data (push-API pattern) plus colorBy — that reserved 110px of right margin and rendered only the legend's header neatline. Zero-item legends now resolve to undefined, 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-router and marked-gfm-heading-id moved from dependenciesdevDependencies (both are docs-only — never imported by anything under src/). Removed tslib as an explicit dev dep (no importHelpers: true in either tsconfig, so TypeScript never emits tslib references). @testing-library/dom now an explicit devDependency (was a transitive peer of @testing-library/react@16+; missing on npm install --legacy-peer-deps and 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.js returns 0.

Fixed

  • Publish workflow OOM in prepublishOnly. Four of the five node scripts/build.mjs invocations across package.json scripts were missing the --max-old-space-size=8192 flag that dist and dist:prod carry; 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 through npm run dist:prod so there's one source of truth for how to invoke the build.
  • parseCanvasColor detects silently-rejected invalid colors. The browser ignores invalid CSS color assignments without throwing — fillStyle stays 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 #010203 before 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-string fillStyle case (CanvasGradient / CanvasPattern).
  • buildBarGradient / buildRectSVGGradient: < 2 valid stops falls back to solid. Both previously checked fg.colorStops.length >= 2 but then filtered out NaN offsets inside the loop — a configured list of 2 stops with one NaN produced a single-stop gradient (flat color) or, on the SVG path, emitted offset="NaN" which invalidates the whole gradient. Now both filter for finite offsets first, clamp, then require ≥2 valid survivors before building.
  • Bar renderer preserves CanvasPattern fills when fillGradient is set. The opacity branch of buildBarGradient used a hardcoded "#4e79a7" fallback when the resolved fill wasn't a string, silently replacing a CanvasPattern fill 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. snapshotPositions now captures node.bodyWidth into prev.w so the candlestick exit node reads the pre-transition width instead of falling through to the 6px default on the final frame. getNodeIdentity prefers an existing _transitionKey over the datum-derived key so exit stubs stay stable across overlapping transitions (affects all exit-node types, not just candlestick).
  • CandlestickChart OHLC validation gap. When the user asks for OHLC mode but the data is missing open/close fields, warnMissingField and validateArrayData now cover all four accessors. Previously the scene builder silently dropped bars and the chart rendered blank with no feedback.

Tooling

  • noUnusedLocals / ESLint cleanup pass. 17 test files under src/components/ and src/__tests__/scenarios/ had bare import React from "react" imports that existed only to satisfy the classic JSX transform. Removed with the transform flip. The 51 files that use React.SomeType / React.ComponentProps<> kept their imports.
  • 3246 unit tests pass (was 3216 in 3.4.1). Net adds: 9 buildBarGradient tests + 6 SVG gradientFill tests in SceneToSVG.test.tsx + 1 BarChart empty-legend suppression test + 8 parseCanvasColor tests covering hex normalization, named colors, invalid-with-string-prev, invalid-with-non-string-prev, and the sentinel-self edge case + 2 ordinalSceneBuilders tests for roundedEdge tagging and fillGradient attachment + 2 NaN-stop fallback tests.

Docs

  • /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 (no Math.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 onCategoriesChange callback on StreamOrdinalFrame + StreamXYFrame threading through useChartSetup state into useChartLegendAndMargin's existing categories param), known landmines (first-ingest timing, sorted-array dedupe, XY's groupAccessor variant, LinkedCharts + CategoryColorProvider interaction), and an estimated ~150 LOC surface across 5 files.
v3.4.1 Breaking risk
Notable features
  • CandlestickChart HOC with primary/context/sparkline modes, required high/low accessors, optional open/close for full candlesticks or fallback range/dumbbell visualization
  • Server‑side rendering support for CandlestickChart via `renderChart` entry and passthrough accessor mapping
  • `compactMode` boolean added to `useChartMode` return, consumed by GaugeChart to collapse conditional renders
Full changelog

Added

  • CandlestickChart HOC (semiotic/xy) — wraps chartType="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). Honors mode="primary" | "context" | "sparkline": scalePadding scales from width (12 / 10 / 3) to keep leftmost/rightmost bars from clipping, extentPadding drops to 2% at widths ≤200 so the y-domain isn't padded into uselessness, and sparkline zeroes top/bottom margin (axes are stripped, so the 2px defaults were dead space). Full docs page at /charts/candlestick-chart with 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.ts gained full enter/update/exit branches for type: "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 carries bodyWidth too so exits don't jump to a 6px fallback on the final frame. Renderer now composites decayOpacity * style.opacity so decay and transition fades stack. getNodeIdentity prefers an existing _transitionKey over 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 candlestickrenderChart("CandlestickChart", ...) works through a new entry in serverChartConfigs.ts. Passthrough config: HOC-level accessors map 1:1 to frame-level ones; openAccessor/closeAccessor are forwarded without defaults so PipelineStore can auto-detect range mode.
  • compactMode: boolean on useChartMode return — the context∨sparkline union now lives on the hook instead of being recomputed in each HOC. GaugeChart consumes it (replaces the local modeIsContext || modeIsSparkline flag 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).

Changed

  • 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 < 60 with 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/2 and caps at layout.height * 0.12 (was hardcoded max(wickWidth * 2, 4) — ≥4px always, marble-sized on a 24px row). Scales up for primary/context.
    • Scene builder now computes the same gap-derived bodyWidth in OHLC and range modes so the renderer has a scale-aware basis for dot sizing.
  • GaugeChart needle formula simplificationinnerRadius > 20 ? innerRadius - 8 : radius - 1. The Math.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 any types 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/sdk 1.27.1 → 1.29.0, @types/node aligned to Node 22.19.17 (matches the Volta-pinned runtime). .node-version corrected from 1822.22.1.

Fixed

  • RealtimeHistogram.showLegend dead pass-throughshowLegend was being fed into useChartMode but the resolved value was never consumed (the HOC doesn't construct a legend prop for StreamXYFrame). Removed the feed-in and updated the comment to explain the absence.
  • arrowOfTime wrongly exposed on StreamOrdinalFrame — 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-colors slugged to the same React key. Renamed to "When to reach for which role" and "When to reach for which primitive"; PageLayout additionally de-dupes TOC keys defensively so a transient DOM overlap during route transitions can't re-surface the warning. item.id still carries the real heading id for anchor navigation; item.key is a separate React-only identifier.
  • Shadowed cookbook import in App.jsimport 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 to CandlestickCookbookPage.

Tooling

  • ai/schema.json and validationMap.ts gained CandlestickChart entries; check-schema-freshness.js and check-ssr-alignment.js both pass.
v3.4.0 Breaking risk
Security fixes
  • Authflow session‑scoped OTP cooldowns close an abuse vector where users changed phone/email mid‑flow to reset OTP cooldowns.
Notable features
  • Usage alerts with email and `usage.alert.triggered` webhook before hard caps hit.
  • Non‑ASCII sender names supported in custom SMTP (e.g., Chinese, Japanese).
  • Portal: Endpoint field now shown for OIDC/SAML app types.
Full changelog

Added

  • Tooltip format cascadevalueFormat on ordinal HOCs and xFormat/yFormat on XY HOCs now flow through to the default tooltip automatically, so a BarChart with valueFormat: 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 to oSort on the frame and to sort on BarChart / StackedBarChart / GroupedBarChart / DotPlot. DotPlot's default changed from sort: true to sort: "auto" — fixes categories shuffling during streaming in the quick-start docs demo and any push-API usage.
  • replace() method on StreamOrdinalFrameHandle — atomically swaps the dataset while preserving the store's category insertion-order memory and the transition position snapshot. Routes through a new DataSourceAdapter.setReplacementData() (emits {bounded: true, preserveCategoryOrder: true}); falls through to progressive chunking for large replacements. LikertChart's re-aggregation now uses replace() instead of clear() + 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 shared RealtimeFrameHandle (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. Extends RealtimeFrameHandle and types getScales() as returning OrdinalScales, so ref.current?.getScales()?.o.domain() works without casts.
  • useFrame composition 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 supportrenderChart("FlowMap", ...) now works server-side via a new flowMap entry in serverChartConfigs.ts. Expands {flows, nodes} into the line-shape StreamGeoFrame expects, with value-proportional edge widths, edgeColorBy / edgeWidthRange / edgeOpacity / edgeLinecap honored. Function-valued edgeColorBy returning literal CSS colors passes through unchanged (via shared getColor).
  • LikertChart added to the server-side ChartName union — was registered in CHART_CONFIGS but absent from the TS union, so renderChart("LikertChart", ...) would type-error despite working at runtime.
  • animate prop on every HOC chartanimate?: boolean | { duration?, easing?, intro? } wired across all XY, ordinal, network, and geo HOCs. Stream Frames resolve animatetransition internally, with synthesized intro animations: bars from baseline, wedges from collapsed arc, lines/areas clipped from left, points from r=0, network nodes from chart center, geo points from center. Wedge angle interpolation for pie/donut data changes. Respects prefers-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 maxPointRadius so the hit tester widens its query for variable-size points (BubbleChart, proportional symbols). Shared findHitPointInQuadtree (src/components/stream/quadtreeHitTest.ts) uses quadtree.visit() to enumerate every candidate within the search region, eliminating the nearest-only miss that quadtree.find() had on heterogeneous-radius scenes.
  • Path2D cache on network edges (NetworkBezierEdge / NetworkRibbonEdge / NetworkCurvedEdge) — _cachedPath2D + _cachedPath2DSource fields invalidate when pathD changes. Shared between NetworkCanvasHitTester and networkEdgeRenderer.
  • waitForChartReady / waitForAllChartsReady / waitForRafs / waitForStreamingUpdate in integration-tests/helpers.ts — event-driven Playwright waits replacing the per-spec waitForVisualization + waitForTimeout(N) pattern.
  • HoverPointerCoords type in hoverUtils.ts — narrower hover-handler signature replacing the as unknown as React.MouseEvent cast that the rAF-coalesced path used to need.
  • ordinalFixtures.ts + recordCanvasOps test utilities — shared sample datasets for bar-chart tests; behavior-level draw-op recorder that replaces brittle toHaveBeenCalledTimes assertions in canvas-renderer tests.
  • describe.each combinatorial coverage for lineCanvasRenderer over (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, findHitPointInQuadtree variable-radius, resolveCSSColor version counter, swimlane bandwidth clamp.
  • Theme-driven selection opacitytheme.colors.selectionOpacity (already defined on SemioticTheme; built-in presets set it to 0.1–0.15) is now wired into the dimming applied by hoverHighlight, legend isolate, and linked selections. Previously the value was emitted as the --semiotic-selection-opacity CSS variable but never read. A new useResolvedSelection(selection) hook merges the theme value into the selection config; every HOC plus Treemap now passes through it. Resolution order is selection.unselectedOpacity (per-chart) → theme.colors.selectionOpacityDEFAULT_SELECTION_OPACITY (library fallback). Clients that previously reached into the package to change DEFAULT_SELECTION_OPACITY can now do <ThemeProvider theme={{ colors: { selectionOpacity: 0.5 } }}> instead.

Changed

  • Function comparator on ordinal sort / oSort is now a category-key comparator — prior types said (row, row) => number but 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) => number on BarChart, DotPlot, GroupedBarChart, StackedBarChart, and the frame's oSort type. useSortedData treats function-valued sort (and "auto") as pass-through since the frame owns category ordering. No usages in src/, docs/src/, or integration-tests/ passed a function comparator.
  • Stream Frame perf passOrdinalPipelineStore decay/pulse no longer rebuild a Map<datum, index> every frame (cached against _dataVersion); pulse wedge inner loop went from O(wedges × data) to O(matches per category) via getCategoryIndexMap. PipelineStore stacked-area extent fused into a single pass; resolveColorMap short-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. evaluateBezier rewritten as evaluateBezierInto(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). onMouseLeave cancels any pending move; latest coords always processed.
  • CSS-var color cache (resolveCSSColor) — version-counter design plus a singleton MutationObserver on document.documentElement and a prefers-color-scheme matchMedia listener. Themes/class toggles/media-query swaps that bypass React still invalidate; per-frame getComputedStyle thrashing is gone.
  • DEFAULT_SELECTION_OPACITY: 0.2 → 0.5 — unselected (dimmed) elements stay readable when a selection is active. Override via selection.unselectedOpacity (per-chart) or theme.colors.selectionOpacity (via ThemeProvider, applies to every chart). Built-in theme presets set this to 0.1–0.15.
  • barPadding ratio clamped to ≤ 0.9 in OrdinalPipelineStore — degenerate layouts (e.g. horizontal swimlane where showCategoryTicks: false shrinks the left margin and the vertical content area is less than barPadding * 2) no longer paint zero-bandwidth bands.

Fixed

  • 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 new preserveCategoryOrder ingest 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: absolute overlays no longer hides the base layer — StreamXYFrame and StreamOrdinalFrame used to paint --semiotic-bg across the full canvas regardless of whether the chart was on top of another. Pass frameProps={{ 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 rExtents not cleared on bounded ingest — when rAccessor is an array, the per-axis rExtents[i] instances are distinct from this.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 categoryresolveCategories retains its insertion-order memory for FIFO stability on re-appearance, but only the undefined/"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 statescheduleNext early returns (completed dataset, superseded data) didn't reset chunkTimer, so setBoundedData / clearLastData could call cancelAnimationFrame on a stale token. Every exit path now resets chunkTimer = 0, preserving the "chunkTimer === 0 iff no rAF scheduled" invariant. Fixed in both setBoundedData and the new setReplacementData.
  • setReplacementData microtask race — a push() / pushMany() buffered just before a replace() could flush after the replacement and append stale points onto the fresh dataset. setReplacementData now clears the pushBuffer + flushScheduled state before emitting the changeset.
  • react-dom/server stripped to (void 0)(...) in the server bundlerollup-plugin-auto-external marks package roots external but not subpaths, so renderToStaticMarkup was being tree-shaken to an undefined binding. Added id === "react-dom/server" to the rollup external predicate. Verified no other production subpath imports hit the same trap.
  • StreamOrdinalFrameHandle.replace() JSDoc and atomicity — routes through setBoundedData-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 cleanupStreamXYFrame and StreamOrdinalFrame now call adapter.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 requestAnimationFrame poll that kept calling setOverviewScales on unmounted components.
  • Cache invalidation completenessPipelineStore._stackExtentCache now invalidates on timeAccessor / valueAccessor / runtimeMode changes; OrdinalPipelineStore._colorSchemeMap on themeCategorical / colorAccessor; OrdinalPipelineStore._categoryIndexCache on categoryAccessor / oAccessor.
  • Accessor re-resolution gatesupdateConfig blocks for x/y/time/value (PipelineStore) and category/o/value/r (OrdinalPipelineStore) used config.X !== undefined, which silently skipped re-resolution when a caller explicitly cleared an accessor ({xAccessor: undefined} — valid React pattern). Switched to "X" in config so 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 reads canvasRef.current instead so it works under the rAF-coalesced path that passes a synthetic {clientX, clientY} payload.
  • _resetCSSColorCacheForTest observer leak — disconnects the global MutationObserver and matchMedia listener it installed; bumps currentVersion rather than resetting to 0 so any surviving WeakMap entries can't be re-validated.

Security

  • Bumped hono 4.12.8 → 4.12.14 and @hono/node-server to 1.19.14 (transitive via scripts/og-server.mjs). Resolves seven advisories — six in hono (cookie validation, IPv4-mapped IPv6 mismatch, path traversal in toSSG, serveStatic slash bypass, hono/jsx HTML injection) and one in @hono/node-server (serveStatic middleware bypass via repeated slashes). All moderate; reachable only from the OG-image build script, not from the published library.

Tooling

  • Dev deps: @modelcontextprotocol/sdk 1.27.1 → 1.29.0, esbuild 0.27.4 → 0.28.0, @types/node aligned to Node 22.19.17 (matches the Volta-pinned runtime).
  • Realtime encoding docs — new "Tuning for streaming cadence" subsection on /features/realtime-encoding with guidance on duration-vs-push-interval tradeoffs (fast / pulsed / slow streams) and the replace() requirement for aggregator HOCs to participate in the transition system.
  • CLAUDE.md / AI docs — "Composing overlays" pitfall note added.
  • scripts/create-release-branch.sh now (a) syncs ai/schema.json version to the bumped package version, (b) verifies CHANGELOG.md has an entry for the new version, and (c) gates on npm audit --audit-level=moderate. Override the audit floor with AUDIT_LEVEL=... if a release is intentionally shipping with known low-severity transitives.
  • prettier 3.8.1 → 3.8.3 (dev-only patch).

Removed

  • The per-spec waitForVisualization helpers in 9 Playwright spec files (consolidated into integration-tests/helpers.ts).
v3.3.1 Breaking risk
Breaking changes
  • Network `customHoverBehavior`/`tooltipContent` callbacks no longer receive `d.type`; use `d.nodeOrEdge` instead.
Notable features
  • `sort` prop on StackedBarChart and GroupedBarChart (supports asc/desc, boolean, or custom comparator)
  • `edgeIdAccessor` on NetworkPipelineConfig enabling single-ID edge removal
  • Transition exits on `remove()` for fade‑out animations across XY, Ordinal, and Network frames
Full changelog

Added

  • sort prop on StackedBarChart and GroupedBarChart — Default: false (data insertion order). Accepts "asc", "desc", boolean, or custom (a, b) => number comparator. Maps to frame oSort. Previously categories were always sorted by total value.
  • edgeIdAccessor on NetworkPipelineConfig — Enables removeEdge(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 calls snapshotPositions() 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 — Extracted renderChart() 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 existing pipelineDecay.ts utility. Single source of truth.
  • HoverData unified type — All four stream frames now construct typed HoverData objects instead of ad-hoc shapes. Network frames use nodeOrEdge field (replaces untyped type); geo frames use properties field. Fixed GeoFrame mismatch where tooltip and customHoverBehavior received different shapes. Breaking: Network customHoverBehavior/tooltipContent callbacks no longer receive d.type — use d.nodeOrEdge instead.
  • SSR angle convention fix — SVG wedge/arc rendering adds π/2 to 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 colorByDepth now uses config.colorScheme (from theme) instead of hardcoded DEPTH_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 on gMax === gMin.
  • SSR sweepAngle passthroughsweepAngle was on the props but missing from the pipelineConfig builder. Gauge arcs now render with correct sweep.
  • SSR hierarchySum string resolution — String valueAccessor (e.g., "value") now resolved to a function before passing to d3-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 _idPrefix in multi-chart documents. renderDashboard passes 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.

Changed

  • as any reduced: 240 → 164 — Hover data types, renderer arrays, pipeline config, accessor utils, SSR prop threading.
  • SSR frameProps override priorityframeProps spread first, explicit top-level props override. margin/colorScheme/legendPosition only override when defined (not undefined).
  • SSR gallery — All 15 charts use renderChart with explicit themes (11 different presets). Dark-themed charts have dark card backgrounds.

Fixed

  • getColor() / getSize() null datum guard — Optional chaining prevents crash when datum is undefined.
  • ProportionalSymbolMap sizeDomain crashfilter(Boolean) + optional chaining in accessor.
  • resolveCSSColor cache — Restored per-canvas WeakMap cache with has() check (handles falsy values). clearCSSColorCache() invalidates on theme change.
  • pieceStyle merge null guard — User frameProps.pieceStyle returning undefined/null no longer crashes spread.
  • GeoFrame hover/click shape mismatchcustomHoverBehavior and tooltipContent now receive the same HoverData object.
  • Bottom legend overlap — Positioned below axes area in reserved margin.
v3.3.0 New feature
Notable features
  • Server API `renderChart(component, props)` for standalone SVG rendering of 27+ HOC chart types with full theme, legend, grid, annotations, and accessibility support.
  • Server API `renderDashboard(charts, options)` enabling multi‑chart layouts with configurable columns and column spans.
  • Server API `renderToAnimatedGif(chartType, data, props, options)` for generating animated GIFs from streaming data windows.
Full changelog

Added

  • semiotic/server production APIrenderChart(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 have id attributes 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 supports colSpan for wide charts.
  • renderToImage(component, props, options) — PNG/JPEG rasterization via sharp (peer dependency). Configurable scale for 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 independent renderChart call.
  • SVG hatch patternscreateSVGHatchPattern() for server-rendered diagonal hatch fills. Used by FunnelChart vertical mode for dropoff bars.
  • Push API remove() and update() — Selective data removal and in-place update across all stores (RingBuffer, PipelineStore, OrdinalPipelineStore, NetworkPipelineStore) and all HOC/frame handles. remove(id) or remove([ids]) by ID (requires pointIdAccessor/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 for remove() and update() 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 renderingrenderChart("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.

Changed

  • hoverHighlight simplified — Changed from boolean | "series" to just boolean. Any truthy value triggers series-based dimming (requires colorBy).
  • CSS variable resolution in canvasresolveCSSColor() resolves var(--name, fallback) via getComputedStyle at paint time. Per-canvas cache avoids repeated calls within a paint cycle; clearCSSColorCache() invalidates on theme change. All 9 canvas renderers updated.
  • extentPadding nullish coalescing — Changed || 0.05 to ?? 0.05 so extentPadding: 0 is respected.
  • Swimlane skipMaxPad — Prevents trailing gap in swimlane charts by skipping max-side extent padding.
  • frameProps.pieceStyle merging — Ordinal HOCs now merge user's pieceStyle with computed base style instead of excluding it. Enables stroke overrides.
  • resolveGroupColor() in server rendering — XY line/area style fallbacks call resolveGroupColor(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.

Fixed

  • Server legend margin — Legend position expands margin before width/height calculation (right:100, left:100, bottom:70, top:40).
  • Server frameProps passthroughframeProps spread into renderer common object so pieceStyle, lineStyle flow through.
  • Server effectiveColorScheme — Falls back to theme.colors.categorical when colorScheme prop 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.
  • buildRealtimeNodes preserving positions — Uses x: d.x ?? 0, y: d.y ?? 0 instead of hardcoded zeros.
  • Dark mode CSS var strokes — Docs site sets --semiotic-bg in both dark/light blocks. LiveExample uses MutationObserver for chart remount on theme toggle.
v3.2.3 Breaking risk
⚠ Upgrade required
  • Build system updated: `rollup-plugin-typescript2` replaced with `@rollup/plugin-typescript`
  • Playwright CI script now skips redundant `npm run dist` and has a timeout of 120 s
Breaking changes
  • Renamed `baselineStyle` to `gridStyle` in XY frame props
Notable features
  • GaugeChart HOC for single‑value gauges with threshold zones and configurable sweep angle
  • Time scale (`xScaleType="time"`) using d3.scaleTime for Date‑aware tick generation
  • Multi‑point tooltip (`tooltip="multi"`) on LineChart showing all series values at hover
Full changelog

Added

  • 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 from semiotic and semiotic/ordinal.
  • Range/dumbbell plot — Candlestick chart type now supports range mode: omit openAccessor/closeAccessor and provide only highAccessor/lowAccessor to render vertical lines with endpoint dots. Single rangeColor via candlestickStyle. No new HOC — demonstrates StreamXYFrame flexibility.
  • scalePadding — Pixel inset on XY scale ranges to prevent glyph clipping at chart edges. Available on StreamXYFrameProps; HOCs pass via frameProps={{ scalePadding: 12 }}. Domain and tick values unchanged.
  • xScaleType="time" — New scale type creates d3.scaleTime for Date-aware tick generation. Required for landmark ticks with timestamp data.
  • sweepAngle — New prop on StreamOrdinalFrameProps limiting pie/donut arc to less than 360° (used internally by GaugeChart).
  • Multi-point tooltiptooltip="multi" on LineChart shows all series values at hovered X with color swatches. Custom functions receive datum.allSeries with {group, value, valuePx, color, datum}.
  • Click-to-lock crosshair — In linkedHover x-position mode, click locks the crosshair. Escape or click again to unlock. Source-aware unlock prevents multi-chart interference.
  • Hover-based sibling dimminghoverHighlight on all HOCs dims non-hovered series on data mark hover (requires colorBy).
  • Per-series fillAreafillArea={["A","B"]} on LineChart fills named series as areas, others stay as lines. New "mixed" chart type with dedicated scene builder.
  • Multi-color gradient fillsgradientFill={{ colorStops: [{offset, color}] }} on AreaChart for semantic color bands. Supports transparent.
  • Line stroke gradientslineGradient={{ colorStops }} on LineChart/AreaChart for horizontal gradient strokes.
  • Axis config extensionsincludeMax forces domain-max tick, autoRotate rotates labels 45° when crowded, gridStyle ("dashed"|"dotted"|string) for grid lines, landmarkTicks bolds month/year boundaries.
  • baselinePadding — Boolean prop on bar chart HOCs. Default false makes bars flush with 0 baseline.
  • hoverRadius — Configurable hit-test distance (default 30px) on all XY HOCs and StreamXYFrameProps.
  • ReactNode tick labelsxFormat, yFormat, categoryFormat accept => string | ReactNode with <foreignObject> fallback.
  • Tick deduplication — Adjacent identical tick labels automatically removed.
  • getHitRadius and MultiPointTooltip exported from semiotic/utils.
  • isTimeLandmark and toDate exported from hitTestUtils.ts (shared across SVGOverlay and tests).

Fixed

  • 30px default hit radius — All 4 hit testers (XY, Network, Geo, Ordinal) now use getHitRadius() from shared hitTestUtils.ts. Previous 12px Fitts's law cap was too small for comfortable interaction.
  • lineDataAccessor data flattening — StreamXYFrame now flattens line-object data before pipeline ingestion. Previously the pipeline read xAccessor on line objects (which lack that field), producing NaN extents.
  • scaleTime domain comparisonvalueOf() comparison for Date objects prevents stale scales from blocking updates.
  • Annotation dark modeAnnotation.tsx text uses var(--semiotic-text), connectors use var(--semiotic-text-secondary) instead of hardcoded black.
  • SwimlaneChart showCategoryTicks={false} — Now suppresses both tick labels and axis title.
  • Floating point tooltip precisionformatValue rounds via toPrecision(6).
  • Default tick format Date-awaredefaultTickFormat handles Date objects (formats as "Jan 7" style).
  • bodyWidth: 0 on 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 and candlestickRangeMode recomputed on prop changes.

Changed

  • baselineStyle renamed to gridStyle — Applies to grid lines (not axis baselines, which stay solid).
  • Build systemrollup-plugin-typescript2 replaced with @rollup/plugin-typescript (fixes TS compilation).
  • Playwright CIserve-examples:ci script skips redundant npm run dist. Timeout bumped to 120s.
v3.2.2 Breaking risk
⚠ Upgrade required
  • `@types/d3-quadtree` moved to devDependencies — no runtime impact.
  • Dev-mode warning added: frame callbacks now warn when accessing `d.data?.field` incorrectly.
Breaking changes
  • Removed `@modelcontextprotocol/sdk` from production dependencies; now bundled via esbuild in MCP CLI.
Notable features
  • 346 new scene builder tests covering XY and ordinal builders
  • 60 tests added for FunnelChart and LikertChart HOCs
  • Render pipeline benchmarks for scene builder throughput, RingBuffer ops, and end‑to‑end ingest‑to‑render
Full changelog

[3.2.2] - 2026-03-30

Added

  • 346 new scene builder tests — Exhaustive coverage for all XY and ordinal scene builders with actual coordinate/position assertions.
  • FunnelChart and LikertChart HOC tests — 60 tests for the two previously untested HOCs.
  • Render pipeline benchmarks — Scene builder throughput, RingBuffer ops, end-to-end ingest-to-render.
  • Dev-mode d.data access warning — Frame callbacks warn when accessing wrapper properties instead of d.data?.field. Zero production overhead.
  • Streaming-first docs — Landing and Getting Started pages restructured to lead with the streaming engine.

Fixed

  • @modelcontextprotocol/sdk removed from production dependencies — MCP CLI now bundles the SDK via esbuild. npm install semiotic no longer pulls in the 4MB+ SDK.
  • @types/d3-quadtree moved to devDependencies
  • Stacked area points at wrong Y position — Fixed cumulative Y computation.
  • Null Y datums assigned stacked Y — Added null/NaN guard.
  • Stale forecast overlays on prop removal — Clears overlays when both forecast and anomaly become falsy.
  • GeoCanvasHitTester inconsistent hit radius — Unified to Math.max((r||4)+5, 12).
  • backgroundGraphics not honoring margins — Fixed in StreamXYFrame and StreamGeoFrame.

See CHANGELOG.md for full details.

v3.2.1 Breaking risk
Notable features
  • LikertChart ordinal HOC for survey data (diverging horizontal / stacked vertical)
  • `onClick` prop added to all HOCs, receiving (datum, {x, y})
  • IBM Carbon color palettes and theme presets (`CARBON_CATEGORICAL_14`, `CARBON_ALERT`, "carbon", "carbon-dark")
Full changelog

[3.2.1] - 2026-03-30

Added

  • LikertChart — new ordinal HOC for Likert scale survey data (diverging horizontal / stacked vertical).
  • onClick prop on all HOCs — Direct click handler receiving (datum, { x, y }).
  • categoryFormat prop — Custom tick label formatting on ordinal HOCs.
  • category-highlight annotation type — Highlight category columns/rows.
  • labelPosition on threshold annotations — Position labels left/center/right (y) or top/center/bottom (x).
  • Coordinate-based linked crosshairlinkedHover with mode: "x-position" for synced vertical crosshairs.
  • Tooltip viewport-aware flip — Auto-flip near container edges.
  • Data-driven histogram bin snapping — Snaps to actual computed bin boundaries.
  • IBM Carbon color palettesCARBON_CATEGORICAL_14, CARBON_ALERT, "carbon"/"carbon-dark" theme presets.
  • Legend line wrapping — Horizontal legends wrap to multiple rows.
  • showPoints on AreaChart and StackedAreaChart

Changed

  • ThemeProvider categorical colors flow to all HOCs automatically.
  • 12px minimum hit target (Fitts's law) on all canvas hit testers.

Fixed

  • Legend styleFn contract, LikertChart tooltip, category-highlight annotation positioning, crosshair cleanup on unmount, FlippingTooltip dependency array, removed dead slicePadding prop.

See CHANGELOG.md for full details.

v3.2.0 New feature
Notable features
  • Adaptive time tick formatting to auto-space hierarchical axis labels and prevent overlap.
  • Forecast configuration adds `trainStroke`, `trainLinecap`, `trainUnderline`, `trainOpacity`, and `forecastOpacity` for training line styling.
  • Per-datum anomaly styling in Forecast with functional accessors for `anomalyColor`, `anomalyRadius`, and `anomalyStyle`.
Full changelog

[3.2.0] - 2026-03-25

Added

  • 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.
  • Adaptive time tick formatting — hierarchical axis labels that auto-space to prevent overlap.
  • Forecast: training line stylingtrainStroke, trainLinecap, trainUnderline, trainOpacity, forecastOpacity on ForecastConfig.
  • Forecast: per-datum anomaly stylinganomalyColor, anomalyRadius, and anomalyStyle now accept functions for data-driven rendering.
  • 128 new unit tests

Fixed

  • SVGOverlay left axis label missing in dual-axis mode
  • ThemeStore mode: "dark" merged onto wrong base
  • Tick label overlap on time axes
  • Function accessors with forecast/anomaly now bake resolved values for annotation rendering
  • Geo hover ring color, tick color dark mode fallback, annotation accessor fallback

See CHANGELOG.md for full details.

v3.1.2 Breaking risk
⚠ Upgrade required
  • v3.1.1 was yanked from npm; upgrade directly from 3.1.0 to 3.1.2
  • MCP server now exposes exactly five tools: `getSchema`, `suggestChart`, `renderChart`, `diagnoseConfig`, and `reportIssue`
Notable features
  • Added `getSchema` tool for on‑demand component prop schemas
  • Enhanced `suggestChart` with confidence levels, reasons, and JSX examples supporting an `intent` parameter
  • Added geo chart components (ChoroplethMap, ProportionalSymbolMap, FlowMap, DistanceCartogram) to the render registry
Full changelog

Semiotic v3.1.2

Critical MCP server fix — all 5 tools now receive arguments correctly. Geo chart rendering support added. HTTP transport mode for remote MCP inspectors.

Note: v3.1.1 was yanked from npm. Upgrade directly from 3.1.0 to 3.1.2.

Fixed

  • 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.
  • MCP geo chart renderingrenderHOCToSVG rejected geo components not in the validateProps validation map. Geo components now skip validation gracefully and render correctly.
  • MCP --port parsing--http without --port no longer produces NaN (falls back to 3001).
  • MCP "top-level fields" dead code — removed unreachable spread logic from renderChart/diagnoseConfig handlers; updated Zod descriptions to match actual schema behavior.
  • suggestChart Histogram heuristic — removed unreachable data.length >= 10 check (suggestChart accepts 1–5 samples per its Zod schema).
  • renderHOCToSVG validation fragility — tightened unknown-component skip check to require exactly one "Unknown component" error.

Added

  • MCP getSchema tool — returns the prop schema for any component on demand, or lists all 30 components with [renderable] markers. Reduces token overhead vs loading full schema.
  • MCP suggestChart tool — analyzes 1–5 sample data objects and recommends chart types with confidence levels, reasons, and example JSX. Supports intent parameter (comparison, trend, distribution, relationship, composition, geographic, network, hierarchy).
  • MCP geo chart support — ChoroplethMap, ProportionalSymbolMap, FlowMap, and DistanceCartogram added to the render registry (25 renderable components total).
  • MCP HTTP transportnpx semiotic-mcp --http --port 3001 starts 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.

Changed

  • MCP server now has 5 tools total: getSchema, suggestChart, renderChart, diagnoseConfig, reportIssue.
  • npm keywords added for MCP directory discoverability (mcp, model-context-protocol, mcp-server).
  • prepublishOnly cleans dist/ to prevent stale dynamic import chunks in published tarball.

Install

npm install [email protected]

MCP Setup

{
  "mcpServers": {
    "semiotic": {
      "command": "npx",
      "args": ["semiotic-mcp"]
    }
  }
}
v3.1.0 Breaking risk
Notable features
  • ChoroplethMap sequential color encoding on GeoJSON features
  • ProportionalSymbolMap sized/colored point symbols with configurable scales
  • FlowMap animated origin-destination flow lines with particle system
Full changelog

Semiotic v3.1.0

Geographic visualization, accessibility foundation, performance optimizations, and comprehensive bug fixes.

Added

Geographic Visualization (semiotic/geo)

  • ChoroplethMap — sequential color encoding on GeoJSON features with valueAccessor, colorScheme, and reference geography strings ("world-110m", "world-50m")
  • ProportionalSymbolMap — sized/colored point symbols on a geographic basemap with sizeBy, sizeRange, and colorBy
  • FlowMap — origin-destination flow lines with width encoding, animated particles (showParticles, particleStyle), and lineType ("geo"|"line")
  • DistanceCartogram — ORBIS-style projection distortion based on travel cost with concentric ring overlay, north indicator, configurable strength and line mode
  • StreamGeoFrame — low-level geo frame with full control over areas, points, lines, canvas rendering, and push API for streaming
  • Zoom/Pan — all geo charts accept zoomable, zoomExtent, onZoom with imperative getZoom()/resetZoom()
  • Drag Rotate — globe spinning for orthographic projections, latitude clamped to [-90, 90]
  • Tile MapstileURL, tileAttribution, tileCacheSize for slippy-map basemaps (Mercator only)
  • Reference geographyresolveReferenceGeography() returns Natural Earth GeoJSON features; mergeData() joins external data into features
  • GeoParticlePool — object-pool polyline particle system for animated flow particles
  • GeoCanvasHitTester — spatial indexing for hover/click hit detection on canvas-rendered geo marks
  • 6 documentation pages, 2 playground pages, 1 recipe page
  • Comprehensive test suites: FlowMap (25), ChoroplethMap (16), DistanceCartogram (19), colorUtils (+6), hooks (+3)

Accessibility (WCAG 2.1 AA ~70%)

  • Canvas aria-label on all Stream Frames describing chart type and data shape
  • Legend keyboard navigation — roving tabindex, Enter/Space activate, arrow keys navigate
  • aria-live="polite" region mirroring tooltip text for screen readers
  • SVG <title> and <desc> on all overlay components
  • aria-label on ChartContainer toolbar buttons
  • 35 Playwright accessibility integration tests

Streaming

  • Streaming legend support — useStreamingLegend hook discovers categories from pushed data dynamically
  • 20+ Playwright streaming regression tests (canvas pixel sampling, legend appearance, tooltip content, stability)

Performance

  • Color map cache — skips rebuild when categories unchanged
  • Stacked area cache — skips cumulative sum when data unchanged
  • Buffer array cache — dirty flag prevents re-materialization on hover/transition ticks
  • Canvas buffer reuse — skips GPU reallocation when dimensions unchanged
  • Path2D caching on geo nodes — eliminates per-hover allocations
  • Streaming push backpressure — microtask batching in DataSourceAdapter

AI Tooling

  • MCP server consolidated from 19 tools to 3 (renderChart, diagnoseConfig, reportIssue)
  • Self-healing error boundaries — SafeRender runs diagnoseConfig on chart failures (dev mode)
  • 61 new unit tests for withChartWrapper, network utilities, and ordinal push API

Fixed

  • Grey fills on push API charts — end-to-end fix for color pipeline when colorScale is unavailable during streaming
  • LineChart infinite re-render loop — circular dependency between statistical overlay effect and line style
  • tooltip={false} now correctly disables tooltips on all 22 remaining HOCs
  • Network HOC tooltip double-chrome wrapping (7 charts)
  • Math.min/max(...spread) stack overflow on >100K datasets
  • Unbounded memory growth in "growing" window mode (capped at 1M default)
  • Geo hooks crash — "Rendered more hooks than during previous render" in FlowMap and ChoroplethMap
  • ChoroplethMap colorScale crash with null areas during async loading
  • Function colorBy produced undefined colorsuseColorScale derives categories from data when colorBy is a function
  • Area/StackedArea tooltips showing "-" instead of values
  • Force graph centering and streaming refresh
  • FIFO category ordering for streaming ordinal charts
  • Edge hit areas expanded to 5px minimum tolerance
  • Sankey crossing reduction via barycenter ordering
  • Anti-meridian line handling for geo projections
  • And 20+ more fixes — see CHANGELOG.md

Improved

  • @ts-nocheck removed from Legend.tsx — full type safety restored
  • 38 as any casts eliminated from PipelineStore
  • useSyncExternalStore replaces custom shim (concurrent mode safe)
  • Shared useChartSetup hook — 10 HOCs refactored, ~170 lines removed

Tests

  • 1944 total tests, 106 test files
  • Direct tests for StreamXYFrame (39) and StreamOrdinalFrame (44)
  • Legend tests (33), tooltipUtils tests (35), ordinal scene builder tests (67)
v3.0.1 Bug fix
Notable features
  • `emphasis` prop added (primary/secondary) for charts and `ChartGrid` column spanning
  • `directLabel` rendering via new "text" annotation type
  • `gapStrategy` fixes: break splits lines at null boundaries, interpolate no longer coerces null to 0
Full changelog

Semiotic v3.0.1

Bug fixes, emphasis prop, direct labels, gap strategy fixes, and export improvements.

Added

  • emphasis prop — all charts accept emphasis="primary" | "secondary". ChartGrid spans primary charts across two columns
  • directLabel rendering — labels now actually render via new "text" annotation type
  • gapStrategy fixes"break" correctly splits lines at null boundaries; "interpolate" no longer coerces null to 0

Fixed

  • PNG export now composites canvas + SVG layers (previously captured only axes)
  • directLabel annotations no longer silently dropped
  • gapStrategy="break" no longer draws lines through gaps
  • colorBy type mismatch in network/hierarchy charts
  • Duplicate amplitude property in StreamOrdinalFrameProps

See CHANGELOG.md for details.

v3.0.0 Breaking risk
⚠ Upgrade required
  • Migrate to the new sub-package entry points (`semiotic`, `semiotic/xy`, etc.) as the monolithic import structure has changed.
  • Update build configuration for TypeScript strict mode; resolve any type errors introduced by generic component parameters.
Breaking changes
  • Complete rewrite changes internal rendering model from SVG‑first to canvas‑first with SVG overlays; all existing chart configurations must be updated for the new HOC components and TypeScript types.
  • Minimum supported Node.js version is now 18.0 due to full TypeScript strict mode usage.
Notable features
  • 37 HOC chart components covering XY, Ordinal, Network, Realtime, Geo categories
  • Full TypeScript support with generic type parameters on all components
  • Server‑side rendering via component‑level SSR and standalone `semiotic/server` package
Full changelog

Semiotic v3.0.0

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.

Highlights

  • Stream-first rendering — all frames are canvas-first with SVG overlays for labels, axes, and annotations
  • 37 HOC chart components — XY (LineChart, Scatterplot, Heatmap, ...), Ordinal (BarChart, BoxPlot, ...), Network (ForceDirectedGraph, SankeyDiagram, ...), Realtime (RealtimeLineChart, ...)
  • Full TypeScript strict mode with generic type parameters on all components
  • Server-side rendering — component-level SSR (automatic in Next.js/Remix/Astro) + standalone semiotic/server
  • Realtime visual encoding — decay, pulse, transition, staleness effects
  • Coordinated views — LinkedCharts, CategoryColorProvider, cross-filtering, linked hover/brush
  • AI toolingsemiotic/ai schema, MCP server, CLI, validateProps, diagnoseConfig
  • 9 sub-package entry pointssemiotic, semiotic/xy, semiotic/ordinal, semiotic/network, semiotic/realtime, semiotic/geo, semiotic/ai, semiotic/data, semiotic/server

See CHANGELOG.md for the full list.

Beta — feedback welcome: [email protected]