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
- 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/yExtentpass-through.
Fixed
- Fixed
yExtenthandling so explicit user bounds continue to control the rendered domain instead of being overridden by envelope-derived extents. - Fixed realtime heatmap tooltip metadata so bin-center values are available and
agg="sum"tooltips report summed values.
- `tooltip="multi"` hover-anywhere tooltips for LineChart, AreaChart, and StackedAreaChart — 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 withinhoverRadiusof an explicit data point. This uses the sharedStreamXYFramemulti-tooltip path, so multi hovers are cursor-anchored within the data range. Interpolation remains generous between sparse path samples but is range-bounded so explicitxExtentpadding does not clamp to first/last values. Stacked areas report per-series band height instead of cumulative stack top, and synthetic no-hit hovers carry data-spacexValue/xAccessordata for linked crosshair and observations. SSR is unchanged because tooltips are gated on pointer-drivenhoverPoint. -
useHydrationLifecyclehook — extracts the post-hydration paint pattern that was previously duplicated as a 12-lineuseEffectacross all four Stream Frames (StreamXYFrame,StreamOrdinalFrame,StreamNetworkFrame,StreamGeoFrame). Each frame now has a 7-lineuseHydrationLifecycle({ ... })call. Three things happen on every commit-after-hydration: cancel the intro animation if we just rehydrated from SSR, mark the scene dirty, paint synchronously viarenderFnRef.current(). Frame-specific cleanup (XY/Ordinal clearing the streaming adapter, Geo clearing tile cache) is supplied via thecleanupoption. Adding hydration support to a hypothetical fifth frame is now a single hook call instead of a copy-paste-and-modify exercise. -
HYDRATION.mdintegration recipe —src/components/stream/HYDRATION.mdcodifies the six-step pattern for adding hydration support to a new Stream Frame: import hooks, gate SSR branch onisServerEnvironment || (!hydrated && wasHydratingFromSSR), attachresponsiveRefon the SVG branch, wireuseHydrationLifecycle, implementcancelIntroAnimation()on the store, mark the bundleclientOnly: true. Codifying this in source (not just memory) means the next contributor doesn't have to reverse-engineer the pattern from four examples — Phase 3.5's geo backfill happened because I miscounted frame families and missed Stream Geo from Phase 3 scope; doc-as-code prevents the same kind of miss. -
SSR-vs-CSR pixel-level Playwright gate —
integration-tests/ssr-parity.spec.ts+ the newssr-parity-examples/fixture snapshot both server-rendered SVG (viarenderChart) and client-rendered canvas for the same chart matrix (LineChart, BarChart, PieChart, SankeyDiagram, Treemap). The SSR side renders into apage.setContentpayload — no fixture file needed — while the CSR side renders through the live HOC components. 30 darwin baselines committed (5 charts × 2 sides × 3 browsers). Both sides baseline independently rather than direct pixel-comparing each other (SVG and canvas rendering pipelines differ in subtle ways and won't match byte-for-byte), so any drift in either pipeline lands as a snapshot diff for a maintainer to review. -
SSR-vs-CSR structural parity test —
src/components/server/ssr-csr-parity.test.tsxexercises the two SSR code paths (renderChartfromsemiotic/serverand the in-frame SSR branch viarenderToString(<Component />)) for the same chart matrix. Asserts both paths emit the dominant data-mark primitive with counts within a 3× ratio. Catches the regression class where one pipeline silently emits zero or wildly different data marks than the other — manual-placeholder users would otherwise see a different rendering than auto-hydrating users without anyone noticing until a real consumer hit it. Surfaces an expected divergence (renderChartis bare data marks, the in-frame path includes SVGOverlay chrome) which the assertions are tuned to permit. -
Interaction-state visual snapshots — 4 new states across XY + LinkedCharts — Closes the bulk of the P1 "Interaction-State Visual Snapshots" item. Added pixel-stable Playwright snapshots for four user-driven states the existing structural tests didn't pin visually:
hoverHighlightdim (LineChart multi-series withhoverHighlight: true): pointer over series A, series B should dim. Fixture inxy-examples/index.js+ test inxy-frame.spec.ts"Interaction states" describe.- Brush selection rect (Scatterplot with
linkedBrush): drag from (0.2w, 0.2h) to (0.8w, 0.8h), capture the resulting brush rect + selection-dim state. - Legend isolate (LineChart multi-series with
legendInteraction: "isolate"): click.legend-item:firstfor series A, capture series B dim. - Linked-hover cross-highlight (existing
linked-hoverfixture incoordinated-examples/index.js): pointer over the scatter half of the LinkedCharts dashboard, capture the matching-category bars staying lit while non-matching dim. Test inbrush-selection.spec.ts"Visual snapshots" describe.
All four use the same
page.mouse.move/page.mouse.down|up/locator.click+waitForRafs(page, 4)settling pattern as the existingxy-scatter-hover-statesnapshot.maxDiffPixels: 200because pointer-driven states have more anti-aliased motion edges than default-state renders. 12 darwin baselines committed (4 interactions × 3 browsers); ran clean twice consecutively for pixel-stability. Remaining gap recorded in OUTSTANDING_WORK: click-locked crosshair (2-step click-to-lock + click-to-unlock interaction) — lower-priority follow-up since the linked-hover snapshot already covers most of the regression surface. -
HOC-level visual snapshots — animated-HOC backfill closes the 43/43 coverage matrix — Added pixel-stable default-theme Playwright snapshots for the 5 HOCs whose canvases were intentionally never visually stable: 4 realtime charts (
RealtimeHistogram,RealtimeSwarmChart,RealtimeWaterfallChart,RealtimeHeatmap) andOrbitDiagram. The technique that made it work: rather than freezing rAF or pinning a frame count (the approach the prior OUTSTANDING_WORK note pre-emptively flagged), simply pass staticdataarrays + omit the continuous-animation props (decay/pulse/transition/stalenessfor realtime,animated: falsefor Orbit). Without those props, the canvas stabilizes after initial paint and ordinarywaitForChartReady(defaultstable: true) succeeds — no rAF instrumentation needed. Fixtures live alongside the streaming-mode siblings inrealtime-examples/index.js(withseedHistData/seedWaterfallData/seedSwarmData/seedHeatmapDataconstants) andnetwork-examples/index.js. Tests are scoped to a "HOC default coverage (static)" describe in each spec so the streaming-mode tests stay separate. 15 darwin baselines committed (5 charts × 3 browsers); ran clean twice in a row to confirm pixel-stability. After this pass, every HOC inchartSpecs.ts(43/43) has a default-theme visual snapshot, closing the P1 HOC-Level Visual Snapshots item. Remaining infrastructure work — bootstrap Linux baselines from the CIplaywright-snapshotsartifact — stays open as a one-time CI/user dance. -
HOC-level visual snapshots — static-mode coverage complete (ordinal + geo backfill) — Added default-theme Playwright snapshots for the 4 remaining static-mode HOCs that didn't have any:
DotPlotandRidgelinePlotinordinal-frame.spec.ts(fixtures land inordinal-examples/index.js),FlowMapandDistanceCartogramingeo-charts.spec.ts(fixtures ingeo-examples/index.js). Each follows the XY-family pattern from the prior pass — single "HOC default coverage" describe per spec, looping the testIds through a sharedtoHaveScreenshotbody. 12 darwin baselines committed (4 charts × 3 browsers). FlowMap fixture pins 5-node + 5-edge synthetic flow data with asimpleAreasbackground; DistanceCartogram pins a 5-node hub+spokes layout withcostaccessor for the cartogram distortion. After this pass every static-mode HOC in the registry has at least one default-theme visual snapshot. 99/99 ordinal + geo Playwright tests pass on darwin (was 87 before; +12); vitest 3574/3574 unchanged. -
HOC-level visual snapshots — XY family fully covered + suite-wide color-drift re-baseline — Added default-theme Playwright snapshots for the 6 XY HOCs that didn't have any:
StackedAreaChart,ConnectedScatterplot,QuadrantChart,MultiAxisLineChart,ScatterplotMatrix,MinimapChart. Each gets a fixture entry inintegration-tests/xy-examples/index.js(deterministic small data,colorspalette where categorical) + atoHaveScreenshottest inxy-frame.spec.ts's new "HOC default coverage" describe. 18 darwin baselines committed (6 charts × 3 browsers). The Linux baselines auto-generate on the next CI push via the existing smoke-fallback workflow (HAVE_LINUXcheck in.github/workflows/node.js.yml); commit them from theplaywright-snapshotsartifact to flip the regression gate on for Linux runners.Same pass re-baselined ~50 pre-existing snapshots that had drifted by ~0.02% pixel diff — sub-perceptual color shift introduced by the 2026-04-28 d3-scale-chromatic →
colorPalettes.tsswap (linear RGB interpolation between 9-11 stops vs d3's basis-spline 256-stop LUTs; ΔE < 1 across every gradient as documented in the helper, but enough to crossmaxDiffPixels: 100on charts with anti-aliased edges over 50k+ pixels). Affected:xy-range-plot,ord-gauge-180/240/needle,histogram-stroke-{light,dark,scoped},funnel-{light,dark},tree-{light,dark},choropleth-{light,dark}under primitive-theme-matrix,waterfall-{light,dark},heatmap-{tufte,bi-tool},likert-{light,dark}under status-scale-theme-matrix. Many of these previously had chromium-darwin only — firefox-darwin and webkit-darwin baselines now committed alongside, extending the regression gate to all three darwin browsers for those tests.Remaining HOC visual coverage gaps (recorded in OUTSTANDING_WORK P1):
RealtimeHistogram/RealtimeSwarmChart/RealtimeWaterfallChart/RealtimeHeatmap(continuous animation — needs rAF freeze),DotPlot/RidgelinePlot/OrbitDiagram/FlowMap/DistanceCartogram. These ship as follow-on PRs since each requires its own fixture page entry.Total: 732/732 Playwright tests pass on darwin after re-baseline; vitest 3574/3574 unchanged.
-
Realtime HOCs auto-fit
windowSizeto bounded data — closes the Consumer Workaround Audit P0 item — newresolveRealtimeWindowSize(windowSizeProp, data)helper atsrc/components/charts/realtime/resolveWindowSize.tsis wired into all 5 realtime HOCs (RealtimeLineChart,RealtimeHistogram,RealtimeSwarmChart,RealtimeWaterfallChart,RealtimeHeatmap) where the priorwindowSize = 200destructure default lived. Resolution rule: explicit userwindowSizealways wins (a 10-point window over a 100-point archive is a legitimate "show the last 10" view); otherwise auto-fit toMath.max(data?.length ?? 0, 200). Closes the historicalwindowSize={data.length}workaround that consumers had to write to keep staticdataarrays larger than 200 points from silently truncating against the sliding-window cap during bulk ingest. Our owndocs/src/pages/theming/SemanticColorsPage.jshad the workaround on the histogram example — removed in the same diff. Streaming-only consumers see no change (the 200 floor preserves the prior default whendatais absent or empty); push API behavior is identical (the buffer can still grow viawindowMode="growing"or be consumed viaref.current.push()regardless of the resolved size). 7 round-trip tests cover the resolution rules including the explicit-zero edge case. The helper is intentionally not exported from any public sub-path — it's an HOC-internal seam, not a consumer API. Closes the Consumer Workaround Audit P0 next-work bullet ("Audit realtime chart usage forwindowSize={data.length}or other bounded-mode workarounds; consider first-class bounded mode on realtime HOCs where static-data use is common"). -
OUTSTANDING_WORK P0 cleared (second sweep) — TypeScript Surface Cleanup, Consumer Workaround Audit, and
validationMapComposition all closed; P0 is empty again. The validationMap entry was a self-marked "skip if Chart Spec Registry ships" — registry shipped, file is generated, item retires automatically. Re-open P0 only when a new release-confidence or doc-correctness gap surfaces. -
TypeScript
anycleanup pass (321 → 274; -47, ~15%) — closes the long-running OUTSTANDING_WORK item — final sweep across the highest-leverage hotspots called out in the original entry, then the item retires:colorUtils.ts—getColor(dataPoint: any, …, colorScale?: (v: any) => string)andgetSize(dataPoint: any, …)re-typed withDatumand(v: string) => string. The internaldataPoint[colorBy]access (which is genuinelyunknown-shaped because Datum is loose) now stringifies once at the seam, matching what d3 ordinal scales do internally and threadingstring[]cleanly throughscaleOrdinal<string, string>without the prioras (v: any) => stringcasts on the return type.SceneToSVG.tsx— replaced 3({} as any)arc-noop calls with a typedARC_NOOP: DefaultArcObjectconstant; replaced thedominantBaselinecast-to-anywith a cast to React's typedSVGAttributes["dominantBaseline"]union (the cast stays — it's the boundary between our free-formNetworkLabel.baselinestring and React's strict SVG-spec union — but it's no longer a type-safety bypass).hierarchyLayoutPlugin.ts— typed everyroot: anyparameter asHierarchyNode<Datum>, everyd: anyposition-setter parameter as the layout-specificHierarchyPointNode<Datum>/HierarchyRectangularNode<Datum>/HierarchyCircularNode<Datum>(matching d3's per-layout node-type contract), and the descendantnodeMapfromMap<any, RealtimeNode>toMap<HierarchyNode<Datum>, RealtimeNode>. The 3 layout-specific subtype casts at the dispatch site are the genuine boundary —layoutTypediscriminant carries the type information, but TS can't see it without an explicit cast — and they're typed-cast notany-cast.orbitLayoutPlugin.ts— same treatment for the orbit-specific layout (buildOrbitLayout(root: Datum),buildTree(parentDatum: Datum),pieGen.value((kid) => …)); typed the__orbitStatecache via the existingunknownfield onNetworkPipelineConfig; replaced the revolution-style callback's(n: any) => numberwith a structuralDepthLikeshape ({ depth?: number }) that names exactly what the function reads; dropped anull as anySceneDatum that was just a forgotten cast (SceneDatumis alreadyDatum | null).chordLayoutPlugin.ts— the per-nodearcDataand per-edgechordDataextension fields are now declared as typedunknown-bag fields onRealtimeNode/RealtimeEdge(the same pattern__hierarchyNode/__radiususe), so callsites narrow at the read site instead of(node as any).arcData. Thearc.centroid()argument now constructs an explicitDefaultArcObjectfrom theChordGrouprather than casting; the one remaining cross-type cast (Chord→ribbonGenerator's expectedRibbonparameter, where d3's configured-radius generator never reads the missingradiusfield) is now a typedas unknown as Parameters<typeof ribbonGenerator>[0]boundary cast with a comment instead ofas any.XYBrushOverlay.tsx+OrdinalBrushOverlay.tsx— typed the d3-brush ref asBrushBehavior<unknown>, the brush event callbacks asD3BrushEvent<unknown>, and the brush-group selection asSelection<SVGGElement, …>so all 12as anycasts ong.call(brushFn)/g.call(brushFn.move, …)resolve cleanly through d3-brush's typed call signatures. Brushes are now fully typed at the d3 boundary — noanyremains in either overlay.
This closes the OUTSTANDING_WORK "TypeScript Surface Cleanup" P0 item per the rationale called out in earlier passes: the remaining ~274 anys cluster in places where the cost-benefit doesn't pencil — vendored sankey-plus type stubs (16, intentional),
renderToStaticSVG.tsxSSR boilerplate (27, hits many type seams across the server bundle),fromVegaLite.ts(11, dynamic Vega-Lite spec shape),canvasMock.ts(10, test utility deliberately loose), plus many small per-component sites that would each save 1–3 lines for non-trivial structural work. The principle stays: prefer modeling real shapes over mechanical replacement, and accept boundary casts that are legibly named (as DefaultArcObject,as HierarchyPointNode<Datum>) overas any. Newanyin PRs is now opportunistic-flag territory, not a tracked backlog item. -
Drop four d3 micro-deps via inlined replacements — audited every d3-* dependency in
package.jsonand removed the four where the dependency was clearly bigger than the surface we used:d3-scale-chromatic→src/components/charts/shared/colorPalettes.ts. We use 3 categorical schemes (schemeCategory10,schemeTableau10,schemeSet3) and 19 sequential/diverging interpolators (interpolate{Blues,Reds,Greens,Oranges,Purples,Greys,Viridis,Plasma,Inferno,Magma,Cividis,Turbo,RdBu,PiYG,PRGn,BrBG,RdYlBu,RdYlGn,Spectral}). Categorical schemes are byte-identical hex arrays from the original ColorBrewer/Tableau/Vega palettes. Interpolators sample the canonical palette at 9–11 stops (vs d3's 256-stop precomputed LUTs) and use linear RGB interpolation rather than d3's Catmull-Rom basis splines — the ΔE across the gradient is sub-perceptual (< 1) for every palette, and binned legend output (the dominant consumer pattern) is identical at typical N=5–9. Output format matches d3's effectiveRgb#toString()shape (#rrggbbfor opaque colors), caught in transit by an SSR snapshot test that asserted on the hex form. ~76KB unpacked saved.d3-tile→ inlined ~30 lines of slippy-tile math at the top ofGeoTileRenderer.ts. Thetile()chained-setter API was over-abstracted for a single call site; the math itself is just "given a viewport size + Mercator scale + translate, which tile triples cover it and what's the per-tile pixel offset."tileWrapis a one-line antimeridian wrap. ~20KB unpacked saved.d3-format→src/components/charts/shared/numberFormat.ts. Implements the chart-axis-relevant subset of d3's spec syntax:[,][.precision][~][type]with typesf,%,e,d,s,r,g, plus the unparseable-spec fallback toIntl.NumberFormat. 19 round-trip tests lock in parity with d3 for the format strings semiotic emits internally and the realistic spec subset consumers pass viaxFormat/yFormat/valueFormat. Dropped fill/align/sign/symbol/width and the binary/octal/hex/code-point/comma-rounded types — none of those appear in chart axis labels. ~42KB unpacked saved.d3-time-format→src/components/charts/shared/timeFormat.ts. strftime token parser backed byIntl.DateTimeFormatpinned toen-USfor%b/%B/%a/%A(matches d3-time-format's default-locale behavior —timeFormatDefaultLocaleis opt-in over there too — and keeps tick labels stable across CI runners, browser system locales, and SSR snapshot baselines). Handles%Y/%y/%m/%d/%e/%H/%I/%M/%S/%L/%p/%b/%B/%a/%A/%j/%%(every token semiotic emits internally + the realistic chart-axis subset). Pre-tokenizes the spec so per-tick formatting walks a resolved program rather than re-scanning the spec string.dayOfYear(%j) reads local calendar components and projects onto a UTC integer day count to avoid 23h/25h DST days skewing the integer division. 13 round-trip tests cover semiotic's default%b %d, %Yform and each token. Dropped%U/%W/%Z/%c/%x/%X/%f— none appear in chart axis labels. Also drops the transitived3-time(~40KB) sinced3-time-formatwas its only consumer in the tree. ~120KB unpacked saved (incl. d3-time).
Cumulative: ~258KB unpacked install-size reduction, ~40KB gzipped on the
xy/ordinal/network/geo/realtimebundles. No public API change; the four removed packages plus@types/d3-scale-chromatic/@types/d3-format/@types/d3-time-formatcome out ofpackage.json. Companion analysis identifiedd3-brush/d3-selection/d3-zoomas theoretically droppable but the bundle savings (~150KB) don't justify the touch-event correctness risk concentrated in the resulting custom pointer model — left those alone. Full suite (3560 tests across 192 files; +32 from the new format-shim suites) green; all 9 release gates clean; rebuilt bundles smoke-tested viacheck:pack. -
check:jsdoc-coveragegate — newscripts/check-jsdoc-coverage.mjsenforces a minimum agent-visible documentation surface on every HOC registered inchartSpecs.ts: a top-line one-sentence summary (TypeDoc renders this as the component blurb on/api/charts) and at least 2@exampleblocks (examples drive/api/typedoc, generated AI docs, and the MCPgetSchemaprompt context — a single example doesn't show variation). Replaces the previous baseline of "TypeDoc resolves the type" — that only proved the JSDoc parsed, not that any of it was useful, so a new HOC could ship with zero examples and nothing in CI would notice. The 2026-04-26 38/38 audit was hand-checked at the time but had no regression detection; today's gate locks it in. Initial run flagged 8 gaps the previous audit missed:AreaChartandStackedAreaChart(a single@exampleapiece — both got a second covering gradients/normalization), and all 5 realtime charts (1@exampleeach, plusRealtimeHistogramhad a multi-scenario block flattened into a single@example— split into proper separate blocks). Backwards-compat aliases (export const RealtimeHistogram = RealtimeTemporalHistogram) are followed once: the gate audits the canonical declaration so the alias's intentionally-minimal@deprecatedblock stays honest. Wired intorelease:check,prepublishOnly, and the CI workflow alongsidecheck:test-quality. Closes the API Reference Documentation P0 next-work bullet. -
check:ai-examples-coveragegate — newscripts/check-ai-examples-coverage.mjscatches drift inai/examples.md, the canonical copy-paste reference that ships in the npm tarball. Two failure modes: (1) stale chart references — a chart was renamed or removed inchartSpecs.tsbut its section inexamples.mdsurvived, so agents that follow the example produce code that fails type-check (heuristic identifier match against the registry, with an explicit allowlist for non-chart surface APIs likeThemeProvider/LinkedCharts/Tooltipso they don't false-positive); (2) coverage gaps — a renderable chart was added tochartSpecs.tsbutexamples.mdwas never updated, so MCP /--doctoragents that read the file as the canonical example reference can't find a starting point. The 22-chart copy-paste backlog at gate-shipping time (SwarmPlot/BoxPlot/RidgelinePlot/DotPlot/PieChart/DonutChart/GaugeChart/FunnelChart/SwimlaneChart/LikertChart/BubbleChart/QuadrantChart/MultiAxisLineChart/CandlestickChart/ScatterplotMatrix/MinimapChart/ChoroplethMap/ProportionalSymbolMap/FlowMap/DistanceCartogram/RealtimeSwarmChart/RealtimeWaterfallChart) is captured in a one-wayCOVERAGE_BASELINEset inline in the script — mirrorscheck:test-quality's burn-down approach: any baseline name that becomes covered in a future PR must be removed from the set in the same diff (otherwise a regression that drops the example would silently pass), and adding a NEW name to the baseline requires diff justification. The HOC's source-level@exampleblocks are still enforced bycheck:jsdoc-coverage, and MCP /--doctoragents still discover each chart throughchartSpecs.ts+getSchema, so the baseline charts aren't agent-invisible — just narrative-light in the canonical copy-paste file. Wired intorelease:check,prepublishOnly, and the CI workflow alongsidecheck:ai-contracts. Closes the AI Surface Behavior Contracts P0 next-work bullet (the "regenerate examples from runtime fixtures and diff" half — the rule-section regeneration half was already covered bycheck:ai-contracts). -
OUTSTANDING_WORK P0 cleared — all four previously-tracked P0 items are closed (API Reference Documentation, Chart Spec Registry, AI Surface Behavior Contracts, Test Quality Gate). The doc now points at the gates that hold the line for each, so future drift surfaces in CI rather than as backlog text. Re-open P0 only when a new release-confidence or doc-correctness gap surfaces — it should be the place that catches "we haven't written the gate yet," not "we keep meaning to come back to this."
-
Canvas render-helper module (
canvasRenderHelpers.ts) — extracted four primitives that recurred acrossbar/area/line/pointCanvasRenderer.tsin byte-identical form:resolveCurveFactory(curve)(the d3-shape token switch was duplicated identically betweenareaandline; adding a curve token previously required two lockstep edits),resolveCanvasFill(ctx, fill, fallback)(replaces the(typeof X === "string" ? resolveCSSColor(ctx, X) : X) || fallbackform that appeared 5+ times across the four renderers and silently fell back to#000000when consumers passed CSS-variable strings),buildLinearFillGradient(ctx, fillGradient, baseFill, x0, y0, x1, y1)(replacesbarCanvasRenderer'sbuildBarGradientandareaCanvasRenderer's inline gradient block — both implemented the samecolorStops/topOpacitytwo-shape switch with the same offset clamping and the sameparseCanvasColoropacity-form normalization;baralready filtered NaN offsets before the 2-stop minimum check,areafiltered them inline and could silently render a 1-stop transparent fill — the helper unifies onbar's stricter behavior, with the renderer falling back to flat fill when the helper returnsnull), andbuildColorStopGradient(ctx, strokeGradient, x0, y0, x1, y1)(replaces the stroke-side colorStops gradient construction inarea's top-stroke branch andline's stroke branch). Renderer-specific path tracing (rounded-corner bar paths, area decay strips, line threshold-color crossings, areatraceAreaPathwith curve-vs-linear branching) stays in the renderer that owns the mark — this is an extraction of mechanical seam boilerplate, not an abstraction over what each mark draws. Net renderer surface: 784 → 686 lines (-98), with 163 lines of helpers added (net +65, but every duplication is gone — adding a new curve token, gradient form, or fill resolution case is one edit instead of four). NewcanvasRenderHelpers.test.ts(11 tests) locks in null-return semantics for pathological inputs (1-stop colorStops, NaN offsets), fallback behavior on null/undefined fills, and the linear-fallback sentinel. Companion P3 plan for afindNearestSceneNodehit-tester factory was audited and skipped — only XY + Ordinal share the quadtree-fast-path + closest-wins shape (~10 line overlap), Network and Geo have genuinely different two-loop / three-phase structures, and the per-mark hit functions that make up the bulk of each tester don't share. Full suite (3515 tests) green;check:test-qualitybaseline (156) unchanged. -
setupCanvasMockadoption sweep — four rAF stubbing flavors + leak-tight cleanup —setupCanvasMocknow acceptsstubRaf: boolean | "noop" | "microtask"so the four test-side rAF cadences live in one place:true(default, synchronous fire) for assertions that just want a paint,falsefor force-simulation specs that need jsdom's setTimeout cadence,"noop"for "observe initial mount-time state" regression suites that would recurse under sync fire (the per-frame*Style → pipelineConfigregression specs inStreamGeoFrame.test.tsxandStreamNetworkFrame.test.tsxnow use this), and"microtask"for tests where sync fire would recursescheduleRenderbut jsdom's setTimeout latency is too costly (the StrictMode HOC suite). Migration cleared the last three test files reimplementing canvas + Path2D + rAF/cAF spies inline (StreamGeoFrame.test.tsx,StreamNetworkFrame.test.tsx,StrictMode.test.tsx);StrictMode.test.tsx's previousteardownMocksonly restored rAF/cAF and leakedgetContext+Path2Dinto later test files via the sharedHTMLCanvasElement.prototypeandglobalThis, which the helper's symmetric capture-then-restore now closes. Sync-fire flavor returns rAF id0deliberately:useFrame.scheduleRendertreatsrafRef.currentas a truthy "pending" flag (if (rafRef.current) return), and the assignment sequencerafRef.current = requestAnimationFrame(cb)lets a non-zero return overwrite the renderer's ownrafRef.current = 0reset and silently coalesce the nextscheduleRenderinto a phantom pending rAF. Caught during the migration when the previously-inlined sync stubs inDotPlot.streaming-order/LikertChart.streaming-order/StreamOrdinalFrame/StreamXYFramepush-API specs started returninggetScales() === undefinedafter the second push; the docstring on the sync branch now spells out the invariant so a future "let's number the ids properly" change won't regress it.noopandmicrotaskflavors return monotonically-increasing ids because their callers never relied on the truthy-flag invariant. The pairedcancelAnimationFramemock now honors cancellation for the"microtask"flavor (tracks ids in acancelledset; the deferred callback no-ops on fire) so production cleanup paths —useFrameunmount,DataSourceAdapterchunk timers,MinimapChartpolling — don't leak into post-unmount state updates or runaway loops in StrictMode-style tests; sync-fire and noop flavors don't need it (already-fired and never-fires respectively). Full suite (3515 tests across 189 files) green;check:test-qualitybaseline (156 mount-only candidates) unchanged. -
Shared AI/MCP component metadata —
ai/componentMetadata.cjsis now the shared source for component category, import, renderability, and registry metadata across the CLI, MCP resources, and surface-parity checks.check:surfaceis wired into release gates so schema,semiotic/ai, MCP renderability, and server-renderer support cannot drift silently. -
Shared chart recommendation engine —
ai/chartSuggestions.cjspowers both MCPsuggestChartandnpx semiotic-ai --suggest, with recommendations for network, hierarchy, geographic, temporal, categorical, and magnitude data shapes. The CLI supports stdin on all platforms via fd0. -
MCP protocol smoke coverage —
src/__tests__/scenarios/mcp-protocol.test.tsexercises stdio JSON-RPC and Streamable HTTP initialization/tools-list flows, including robust parsing for JSON and SSE responses. -
API docs extraction coverage —
api-docs-extraction.test.jslocks down TypeDoc re-export resolution, props alias handling, function-signature formatting, examples, inherited-prop labels, and component summaries. -
Docs route smoke check —
check:docs-routesvalidates prerendered homepage/API routes, route metadata, sitemap entries, and generated API JSON assets;website:buildnow runs it after prerender. -
AI behavior contracts —
ai/behaviorContracts.cjsis now the structured source for agent-visible semantic rules that schema parity cannot express: categorical color precedence, required prop combinations, push/ref behavior, ID-accessor mutation requirements, and renderChart/static-data boundaries.semiotic-ai --doctorand MCPdiagnoseConfignow acceptusageMode: "static" | "push"so static/renderChart configs still require data while ref-push React HOCs can intentionally omit it. The MCPsemiotic://behavior-contractsresource and generated AI docs consume the same rule metadata;check:ai-contractsis wired into release gates. -
Test quality gate —
check:test-qualitybaselines existing frame/canvas mount-only assertion candidates and fails when new candidates are introduced without updating the baseline. The release and prepublish gates now run it so new tests must prefer semantic assertions against scene summaries, rendered output, callbacks, or user-visible behavior. -
Streaming legend frame-domain subscriptions —
StreamOrdinalFrameandStreamXYFrameexposelegendCategoryAccessorandonCategoriesChange; pushed legends now populate, shrink, relabel, and clear after insert/remove/update/clear.LinkedChartshas a live category registry so unified legends and shared category colors update from child chart domains. -
ThemeProvider first-render coverage — new tests prove preset/object themes, CSS custom properties, prop changes, and forced-colors initialization are visible to children on the first render.
-
PipelineStore config-only cache regressions — cache tests now cover scene rebuilds from
themeSemantic.primary,themeSequential, andbarColorschanges without data ingest. -
HOC JSDoc coverage — every public HOC (38/38) now ships with at least 2
@exampleblocks, a top-line summary that cross-references sibling charts via{@link}, and@defaultannotations on the most-used optional props. Previously only 5 HOCs had any examples; the rest relied on schema summaries. Format is consistent across families: simple usage → encoded variants → push-API or advanced cases. Surfaced through TypeDoc (/api/typedoc) and TypeScript hover-help. -
streamProps construction helpers —
src/components/charts/shared/streamPropsHelpers.tsexposes three pure helpers that replace the spreads recurring in every XY/ordinal HOC'sstreamProps = { ... }literal:buildBaseMetadataProps(the...(title && { title })chain acrosstitle/description/summary/accessibleTable/className/animate, with per-field truthy-vs-defined gates that match the inline form),buildTooltipProps(thetooltip === false ? () => null : (normalizeTooltip(tooltip) || defaultTooltipContent)ternary), andbuildCustomBehaviorProps(the conditionalcustomHoverBehavior/customClickBehaviorspread, with alinkedHoverInClickPredicateflag so geo /CandlestickChart-style HOCs that excludelinkedHoverfrom the click predicate get the right semantics from the same helper). 19 HOCs migrated across the four families; the 9 holdouts have non-standard variants (LineChart's 4-statetooltip === "multi"branch, ChoroplethMap's 4-statetooltip === truebranch, the geo HOCs'resolved.Xmetadata destructure, LikertChart and FunnelChart's chart-specific tooltip flow, MultiAxisLineChart's series unitization, QuadrantChart's overlay graphics) and stay inline because the helper would have to grow more knobs than the boilerplate it's saving. Net 19 files changed, 228 deletions vs 171 insertions (-57 lines on the HOC surface; the helper module adds ~135). The bigger win is centralizing the three predicates so a future change to the linked-hover-vs-click wiring rule edits one spot instead of 19. UnusednormalizeTooltipimports in the migrated files dropped during the same pass; typecheck + 3515 unit tests + chart-specs / context7 / mcp-registry / test-quality gates all green. -
AI-discoverability prep + DISCOVERABILITY.md playbook —
server.jsonat the repo root refreshed for the official MCP Registry publish flow (version + npm identifier sync'd to currentpackage.json, title/websiteUrl/registryBaseUrl fields added that the registry validator wants). README gained anmcp-name: io.github.nteract/semioticliteral string in the MCP Server section (registry validator substring-matches it as proof the npm package and the registry entry are the same artifact) and a "Where to find Semiotic for AI assistants" section linking the discovery surfaces (Context7, DeepWiki, GitMCP, MCP Registry, Smithery) plus the agent-facing files that ship inside the package (CLAUDE.md,llms.txt,llms-full.txt,ai/schema.json,ai/behaviorContracts.cjs). Newcheck:mcp-registrygate (scripts/check-mcp-registry.mjs) cross-references all three sources —server.json,package.json#mcpName/name/version, and the README literal — and fails the build if any of them drift, since drift only surfaces at publish time otherwise. Wired intorelease:check,prepublishOnly, and the CI workflow alongsidecheck:context7. New top-levelDISCOVERABILITY.mddocuments the 9 places worth listing Semiotic with copy-pasteable submission text and CLI commands; the README's discovery section is the consumer-facing summary,DISCOVERABILITY.mdis the maintenance playbook. -
Context7 manifest + freshness gate —
context7.jsonlives at the repo root and points the Context7 indexer at the agent-facing surface (CLAUDE.md,docs/public/llms-full.txt,docs/src/pages/charts,ai/) withexcludeFoldersfordist/build/node_modules/snapshot dirs, plus arulesarray distilled frombehaviorContracts.cjs(sub-path imports, push-modedatasemantics, ID-accessor requirements forremove/update, categorical color precedence, geo-import discipline, required prop combinations, server-rendering boundaries). Newcheck:context7gate (scripts/check-context7.mjs) validates JSON syntax, the 255-char-per-rule limit Context7 silently rejects on, that everyfoldersentry resolves on disk, and that the sub-path rule's import names line up withpackage.json'sexportskeys. Wired intorelease:check,prepublishOnly, and the CI workflow.behaviorContracts.cjscarries an inline maintenance note pointing edits atcontext7.jsonso content drift between the two stays visible during code review (the gate catches format drift but not semantic drift).useFrameImperativeHandle(ref, { variant, frameRef, overrides?, deps? })atsrc/components/charts/shared/useFrameImperativeHandle.ts— extracts the 7-methodRealtimeFrameHandlebridge (push/pushMany/remove/update/clear/getData/getScales) every HOC implemented inline. Three variants:"xy"(vanilla pass-through toframeRef.current),"network"(topology-walkingremoveNode/updateNode), and"geo-points"(removePoint-based with emulatedupdate). HOCs with bespoke wrappers —BubbleChart's wrapped push that tracks the streaming size domain,SankeyDiagram/ChordDiagram's edge-shapedgetData— passoverridesto selectively replace methods while keeping the variant defaults for the rest.MultiAxisLineChartandLikertChartkeep their inline handles (per-series unitization and pre-aggregation diverge enough that the helper would fight rather than help). Migrated 22 HOCs across the four families.const { width, height, enableHover, showGrid, showLegend, title, description, summary, accessibleTable, ...rest } = resolved— every HOC unpacked the post-useChartModealias bundle into a 9-to-11-line block ofconst X = resolved.Xlines. Replaced with a single destructure across 32 HOCs. Plain JS object destructuring (no helper, no API surface, no test) handles the same job, including the?? falsefallback case that ForceDirectedGraph uses (translates cleanly to the destructure-defaultshowLabels = falseform because theChartModeResultshape allows onlyboolean | undefined).
-
useChartSetupunification closed — the standing question of whetheruseChartSetupshould grow optional inputs for dual-axis (MultiAxisLineChart) and projection (geo) cases is resolved with no API change.MultiAxisLineChartalready integrates by passingcolorBy: SERIES_FIELD(a synthetic series field) and bumpingmarginDefaults.left/rightto 70 for the dual-axis layout — the unitization step that makes the chart dual-axis lives above setup, not inside it.ProportionalSymbolMap,FlowMap, andDistanceCartogramintegrate cleanly today with the existing setup surface; the d3-geo projection lives insideStreamGeoFrame, not in any HOC-reachable seam, so there is no "pre-projected coordinates" path that wants exposing.ChoroplethMapis a separate shape (sequential gradient legend driven by a value scale, not categorical) and is intentionally outsideuseChartSetup's scope. Outcome: no new optional inputs added, no new consumers in the queue — the unification work is complete. -
LineChart drops standalone
useStreamingLegend—useChartSetupowns the full legend pipeline — LineChart used to layer a separateuseStreamingLegendcall on top ofuseChartSetup: that hook re-tracked discovered categories viawrapPush/wrapPushMany, registered them with the parentLinkedChartsviauseLinkedChartCategories, and produced astreamingLegendelement +streamingMarginAdjustthat the HOC merged on top ofsetup.legend/setup.margin. After this change,useChartSetupalready does the equivalent end-to-end:useChartLegendAndMargin(called inside setup) registers the live category domain withLinkedCharts, and the synthesizedlegendColorScale+setup.legendcarry the same provider → scheme → theme → STREAMING_PALETTE precedence the marks use. LineChart now spreadssetup.legendBehaviorProps(which includeslegendCategoryAccessor+onCategoriesChangefor the frame, plus the legend slot and legend-interaction handlers) and readssetup.legend/setup.margindirectly.useStreamingLegendstays in the codebase as a low-level escape hatch for aggregator HOCs that need to intercept push calls BEFORE ingest (LikertChart re-aggregating streamed rows into level × count); its docstring now flags this as a niche case and points new code atsetup.legend/setup.margin. No behavior change in either bounded or push mode for LineChart consumers; 3507 unit tests including the dedicated push-mode legend color regression suite continue to pass. -
Test quality gate burn-down: ordinal bar family + Playwright — Two passes against the mount-only assertion baseline:
- Ordinal bar family unit tests:
BarChart.test.tsx(16 mount-only checks),StackedBarChart.test.tsx(8), andGroupedBarChart.test.tsx(1) replaced theirexpect(frame).toBeTruthy()mount-only assertions with semantic checks against the props the HOC actually forwards toStreamOrdinalFrame:chartType,data,oAccessor/rAccessor,projection,oSort(plus a sortedness check on the pre-sorted data whensortis"asc"/"desc"),barPadding,enableHover,showGrid,size,stackBy,groupBy,normalize,legend.legendGroups[0].items(distinct colors per category forcolorSchemechecks), andframePropsescape-hatch overrides. Caught and locked in a real distinction along the way:GroupedBarChartroutes throughchartType: "clusterbar", not"bar". The intentional sparse-array hardening test gets the documented// test-quality-gate: allow-mount-onlyopt-out so the gate doesn't fight a test whose whole purpose is to prove no-crash-on-sparse-input. - Playwright integration specs: 9
canvas.toBeVisible()/svg.toBeVisible()mount-only checks acrossaccessibility.spec.ts,brush-selection.spec.ts,coordinated-views.spec.ts,geo-charts.spec.ts,hoc-legend.spec.ts,realtime-charts.spec.ts, andstreaming-regression.spec.tsreplaced with semanticaria-labelregex matches (/\d+/). The data canvas'saria-labelis set bycomputeCanvasAriaLabelonly after the scene populates (e.g.scatter, 50 points), so the new check requires real paint — a regression that wires the canvas up but never draws would slip pasttoBeVisible()but trip the regex. The coordinated-views hover test now asserts a.stream-frame-tooltipelement appears after hover (proving the interaction-canvas hit-test resolved a point) instead of just checking the overlay canvas exists. - Net baseline change: 190 → 156 mount-only candidates (~18% reduction across both passes). The Playwright sweep is the higher-leverage half: those tests run in a real browser, where mount-only checks are blind to drawn-pixels regressions that semantic assertions catch.
- Ordinal bar family unit tests:
-
Sparse-array prop hardening across every ingestion path — public chart HOCs now filter
null/non-object entries from array props (data,points,nodes,edges,flows,series,areas) before any iteration. CSV/loader pipelines commonly emit[null, validRow, undefined]-shaped input, which previously crashed insideuseChartSetup,useColorScale,inferNodesFromEdges, the choropleth geometry validator, and the StreamFrame ingestion path. A new shared identity-preserving helper atsrc/components/charts/shared/sparseArray.ts#filterSparseArrayreturns the original reference when nothing is dropped (preservinguseMemocache hits in the clean case) and is wired through every ingestion seam:useChartSetupfilters itsdatainput and exposes the sanitized array assetup.data; the empty-state check filtersrawDatatoo, so[null, undefined]lands on the empty-state UI rather than rendering blank.DataSourceAdapterfilterssetBoundedData,setReplacementData,push, andpushMany. Push-moderef.pushMany([null, valid])now silently drops the null and lands the valid row instead of crashing extent reads inside the pipeline store.StreamGeoFrame.pushPoint/pushManyandStreamNetworkFrame.pushEdge/pushManyEdgesmirror the same filtering at the geo/network frame boundaries (those frames don't route throughDataSourceAdapter).- 26 HOCs replaced their
safeData = data || []with the identity-preserving filter;MultiAxisLineChart'sseriesprop, the network family'snodes/edges, andChoroplethMap's resolvedareasgot the same treatment. ForceDirectedGraph/SankeyDiagram/ChordDiagramempty-state checks route through their filtered arrays so sparse-only input triggers empty UI.ProportionalSymbolMapandFlowMapwere migrated to drop their own pre-setup filter and readsetup.datadirectly (eliminates the double-scan whenuseChartSetupwould re-filter the same array).- A new
EMPTY_ARRAYsingleton (frozen) is used as the stable push-mode default in HOCs that need an array literal; per-render churn through the sparse-filteruseMemois gone. - New regression test
src/__tests__/scenarios/sparse-array-hardening.test.tsxcovers one HOC per data-iteration shape, plus push-mode ingestion drops and empty-state routing — 17 cases, all green.
-
Push-mode legend color regression suite (XY + geo) —
src/__tests__/scenarios/push-mode-legend-colors.test.tsxexercises the legend-color synthesis precedence (CategoryColorProvider→ explicitcolorSchemearray → string scheme name →ThemeProvidercategorical → default theme) againstLineChart(xy /useStreamingLegendpath) andProportionalSymbolMap(geo /useChartSetuppath) mounted in push mode for each tier. 10 tests total. The "bare push" tier's behavior is documented inline: it resolves toLIGHT_THEME.colors.categoricalbecause the theme store seeds a non-empty default categorical palette, leavingSTREAMING_PALETTEas a defense-in-depth fallback rather than a reachable production path. Negative-anti-source assertions catch any regression that surfacesSTREAMING_PALETTEfrom a wrongly-ordered precedence chain. -
StreamGeoFrame push-mode legend category emission —
StreamGeoFramenow readslegendCategoryAccessor+onCategoriesChangefrom props and emits the live category domain after every scene rebuild, mirroringStreamXYFrame/StreamOrdinalFrame. Push-mode geo HOCs (ProportionalSymbolMap,FlowMap) now propagate their discovered categories back throughuseChartSetup'sframeCategoriesstate, so the synthesized legend renders with the correct swatches the moment data starts flowing. Previously the props were accepted on the HOC side but unread by the frame, so push-mode geo legends never populated. Frame-level coverage inStreamGeoFrame.test.tsxexercises the emission acrosspushMany/removePoint/clearso a regression in the wiring is caught at the frame level, not just through HOC scenarios. -
Chart Spec Registry — Phases 1–4 (all 43 chart families) —
src/components/charts/shared/chartSpecs.tsis the single source of truth for the prop specifications of every Semiotic HOC: 15 ordinal + 12 XY + 7 network + 4 geo + 5 realtime = 43 charts. Three pure generators inscripts/lib/chart-specs-generators.mjs(generateSchemaToolEntry,generateValidationMapEntry,generateMetadataEntry) produceai/schema.json,validationMap.ts, andcomponentMetadata.cjsentries from eachChartSpec;scripts/regenerate-schema.tsre-baselinesai/schema.jsonfrom the registry. A 130-test round-trip suite (chart-specs-round-trip.test.ts) iterates overCHART_SPECSand asserts deep structural equality on the parsed schema tool entry and validationMap entry per chart, plus componentMetadata category correctness and a top-level set-parity check thatCHART_SPECSkeys exactly match the canonical name sets inai/schema.json,validationMap.ts, andai/componentMetadata.cjs.check:chart-specsis wired into release/prepublish gates and the CI workflow. Adding a new chart is now one edit (aChartSpecentry) plus regeneration; previously it required three coordinated hand-edits across schema/validation/metadata files.
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/typedocnow covers the full chart HOC surface, resolves re-exported props declarations instead of stopping at reference stubs, shows component summaries/examples, formats callback prop signatures, and labels inherited props from shared interfaces. - ThemeProvider initialization — the scoped ThemeStore is seeded with the resolved initial theme before children render. This removes the previous light-theme-first path for
useTheme(), chart color defaults, and CSS variables. - Canvas theme bridge —
useFrameowns theme-change invalidation from a layout-timed effect: clear CSS-var color cache, mark the frame dirty, and schedule a repaint whenever the ThemeStore theme changes. - Streaming legends and linked colors — push-mode legends now source swatch colors from the same provider/theme/color-scale path as rendered marks, so child legends and unified
LinkedChartslegends agree. - HOC shared setup unification —
LineChart,AreaChart,StackedAreaChart,QuadrantChart,ConnectedScatterplot,BubbleChart,ProportionalSymbolMap,FlowMap, andDistanceCartogramnow useuseChartSetupfor categorical color scales, legends, selection/hover/click behavior, loading/empty states, and margins, while keeping chart-specific logic (statistical overlays, gap handling, direct-label margins, line/area/stack transforms, size domains, quadrant overlays, projection, flow point→edge hover translation, and cartogram layout) local.useChartSetupitself now synthesizes a push-mode legend color scale from discovered categories using the same precedence asuseColorScale(provider → explicit scheme → theme → STREAMING_PALETTE), so legend swatches and rendered marks agree on every converted chart without each one needing to layeruseStreamingLegendseparately. - StackedAreaChart AI contract —
areaByis now marked required in the AI schema and validation map, matching the existing LLM guidance that StackedAreaChart needs an explicit grouping field for stacked series. - HOC rendering scenario tests — high-value public wrapper smoke tests now assert scene summaries, legend labels, annotation labels, and explicit empty/loading states instead of only proving that a canvas mounted.
- OUTSTANDING_WORK.md — collapsed into an active priority backlog only; completed dependency migrations, theming milestones, AI/MCP work, prerendering, streaming legend work, quadtree work, and cache/theme fixes were removed from the backlog and recorded here instead.
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 insidecheck:schemais preserved as a slim, focused gate atscripts/check-claude-md-coverage.js(npm run check:claude-md-coverage).check:surfaceno longer asserts schema↔validation name parity (also covered by registry round-trip) and instead focuses onsemiotic/aiexports, MCP renderable registry entries, AI component metadata, and server renderChart configs. CI workflow and release/prepublish gate scripts are updated accordingly.
Fixed
- Production builds preserve
"use client"after terser —npm run dist:prodwas failing the post-build directive-placement gate: every client bundle's minified output came out missing the directive. Root cause:useClientPlugin.renderChunkwas prepending"use client";to the output, then terser'srenderChunkran AFTER it and silently dropped the top-level string expression through its parse → compress → emit pipeline (default terser doesn't preserve"use client"the way it preserves"use strict"). Fix: splituseClientPlugin's registration so itstransformhook still scans modules during the parse phase but itsrenderChunkis appended LAST in the plugin array — after terser. The directive is the absolute final write to the chunk; terser never sees it. Caught by the inverse-direction post-build assertion that gates client-only bundles must carry the directive (added in the previous round); without that gate the regression would have shipped silently. PipelineStore.ingestis idempotent on identical bounded data refs — the SSR branch in every Stream Frame callsstore.ingest({ inserts: data, bounded: true })from inside render. React StrictMode renders components twice in dev mode, so the second render's ingest would clear the buffer and re-fill it from the same data array — wasted work, and a real correctness risk if a future change made the second pass non-trivially different. Now: the first call records_lastBoundedInsertsRef; a subsequent call with the same reference returnsfalseimmediately as a no-op.clear()resets the dedupe ref so post-clear ingests re-run normally. Streaming (non-bounded) ingests are unaffected — each push is meaningful and shouldn't dedupe.- Build's
"use client"detection accepts leading comments — the rollup plugin usedcode.startsWith('"use client"'), which silently missed every source file that opened with a JSDoc block (StreamOrdinalFrame, useFrame, useHydration, useChartSetup, several others — 7 files total). Bundles still ended up directive-tagged in practice because each chunk pulled in some other module that opened with the bare directive, but a future change isolating one of those JSDoc-headered files into its own chunk would silently lose the directive on its bundle. Replaced withhasLeadingUseClientDirective()that skips leading whitespace + line comments + block comments before checking — the same way Next.js / React's parsers interpret the directive. Added: (1) a 14-case unit test covering positive (with/without leading comments, single vs double quotes) and negative (directive in a string, after an import, no directive at all) inputs; (2) a complementary post-build assertion thatclientOnly: truebundles MUST carry the directive on output (catches the inverse regression where a detection bug silently drops the tag from a chart-family bundle, which would crash Next.js Server Components importing from that sub-path with browser-API errors at runtime). cancelIntroAnimationnow clears per-node_introClipFraction—synthesizeIntroPositionssets_introClipFraction = 0on line and area scene nodes (the canvas renderers consume it directly to clip the path from the left). The earlier Phase 4cancelIntroAnimationclearedprevPositionMap/prevPathMap/activeTransitionbut left the per-node flag, which would silently produce a fully-clipped (blank) line / area chart on the first canvas paint after SSR hydration. Now walks the scene and resets the flag toundefinedfor line / area nodes. Regression test inPipelineStore.cancelIntro.test.tsasserts every line node's clipFrac is undefined after cancel.- CSR mounts skip the wasted SVG render — the SSR/hydration gate on every Stream Frame was
if (isServerEnvironment || !hydrated), which forced a full SVG render on the first render of every client-side mount, even when there was no SSR HTML to match. Tightened toif (isServerEnvironment || (!hydrated && wasHydratingFromSSR))— the SSR signal we already use for intro-animation cancellation now also gates this branch. CSR mounts skip the SVG render entirely and go straight to canvas; SSR rehydration still gets the byte-identical SVG output it needs. Net effect: one less synchronous scene-build + SVG conversion per client-only chart mount, with no behavior change for SSR consumers. createStoreno longer callscreateContextat module load — even with the"use client"directive removed fromsemiotic/server, the bundle still pulled increateStore.tsx, which calledReact.createContexteagerly when each store factory ran. React Server Components ship a build ofreactthat omitscreateContextentirely, so importingsemiotic/serverfrom a Server Component threw(0, p.createContext) is not a functionbeforerenderChartever ran. RefactoredcreateStoreto defer thecreateContextcall untilProvider/useSelectoractually executes — both of which only run on the client. Added anRSC import safetyregression test that mocksreactto throw oncreateContextand asserts the factory call doesn't trip it. Caught by the SSR demo'smanual-placeholderroute at request time.ForceDirectedGraphacceptsnodeIdAccessor(camelCase) — the historical prop name wasnodeIDAccessor(uppercase ID), which was inconsistent with the rest of the network HOCs (SankeyDiagram,ChordDiagram,TreeDiagram,OrbitDiagramall use camelCasenodeIdAccessor). The casing inconsistency surfaced during the SSR demo's verification matrix. Both prop names are accepted now;nodeIdAccessoris canonical andnodeIDAccessoris a@deprecatedalias slated for removal in 4.0. When both are passed,nodeIdAccessorwins. Codemod follow-up tracked for the externalsemiotic-codemodrepo: aforce-directed-graph-node-idtransform that renames the JSX attribute on existing<ForceDirectedGraph>usages.semiotic/serverno longer carries"use client"— the build's directive plugin tagged any chunk that contained a transitively-imported client module, which leaked the directive into the server-only entry point. CallingrenderChart(or any other server export) from a Next.js Server Component threw at runtime: "Attempted to call X() from the server but X is on the client." The fix is a per-bundleserverOnlyflag on the build config that opts the server bundle out of the directive unconditionally, plus a post-build assertion (assertDirectivePlacement) that fails the build if the regression ever returns. Caught by an SSR demo that exercised the manual-placeholder pattern; the auto-hydrating XY/ordinal/network HOCs were unaffected because they don't import fromsemiotic/server.- HOC JSDoc accuracy pass — 13 doc inaccuracies caught and corrected against source:
ForceDirectedGraphnodeSize/nodeSizeRangedefaults, push-mode opt-in fornodes/edges, and the fabricatedframeProps.initialPositions;PieChart.startAngleunits (degrees, not radians) and the matching example value;PieChart.valueAccessoraggregation byMath.absfor negatives;QuadrantChartexample commentary;ChoroplethMap.areasshape (Feature[], notFeatureCollection) and the asyncresolveReferenceGeographyexample pattern;MinimapChartexamples that referenced non-existentchart,minimapHeight,initialExtent,onBrushChangeprops;ScatterplotMatrixexample that wrapped the matrix in an outer<LinkedCharts>even though the component already creates its own provider internally;ProportionalSymbolMappush example missing theidfield required bypointIdAccessor. All examples now match the real prop surface and copy-paste cleanly. - AI chart suggestions copy/paste correctness — ForceDirectedGraph suggestions no longer imply nodes are optional, hierarchy suggestions reference the provided data shape, heatmap recommendations require two dimensions plus a value, sample inputs are capped, and generated JSX string props are escaped safely with
JSON.stringify. - MCP HTTP test robustness — startup retries wait for child-process exit before rebinding; response parsing handles plain JSON and SSE data events instead of assuming a single
data:line. - Prerender structured-data duplication — injected JSON-LD now carries the same
data-jsonld="semiotic"marker the client hook checks, preventing duplicate runtime scripts. De-duplication is scoped to Semiotic-owned JSON-LD rather than all SoftwareApplication schemas. - Prerender metadata consistency — canonical and
og:urlmetadata are normalized together, including the homepage URL shape. - SSR alignment test isolation —
ssr-alignment.test.tschecks a temporary SceneToSVG copy instead of mutating tracked source files during parallel Vitest runs. - GaugeChart LLM docs — machine-readable docs now correctly list
thresholdsas optional, matching the TypeScript API. validatePropsdata shape handling — the"none"data shape is handled explicitly so no-data components do not accidentally fall through realtime validation assumptions.
Tooling
- 3324 unit tests pass after the latest backlog cleanup and cache regression additions.
- 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.
- 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
gradientFillonBarChart— same API asAreaChart.gradientFill:truefor a default 80%→5% opacity fade on the resolved bar color,{ topOpacity, bottomOpacity }for explicit opacity stops, or{ colorStops: [{offset, color}, ...] }for arbitrary multi-color gradients. Direction always runs from each bar's tip (opposite the baseline) toward its base, so positive/negative and vertical/horizontal orientations all do the right thing. The scene builder now tags every rect withroundedEdgeunconditionally (previously only whenroundedTop > 0) so gradient direction resolves without requiring rounded corners. NewbuildBarGradienthelper inbarCanvasRenderer.tsbuilds theCanvasGradientper bar;StackedBarChart/GroupedBarChartget it for free via the shared scene + renderer.- SVG / SSR rendering for bar gradients —
ordinalSceneNodeToSVGinSceneToSVG.tsxemits<defs><linearGradient>+fill="url(#id)"for rect nodes carryingfillGradient. Works through bothrenderToStaticSVG(server) andanimatedGif(GIF export) automatically since both delegate to the shared scene-to-SVG converter. UsesgradientUnits="userSpaceOnUse"with absolute coords so each bar's gradient tracks its own rect. NewsafeSvgIdhelper coerces category names containing spaces/punctuation to a legal SVG id charset before embedding them in the gradient'sidandurl(#...)reference. renderers/colorUtils.ts— sharedparseCanvasColor(ctx, color)used by bothbarCanvasRendererandareaCanvasRenderer. Resolves any valid CSS color (named like"steelblue",hsl(),rgb(), hex short/long) to an[r, g, b]tuple via actx.fillStyleround-trip that the browser normalizes. Uses a sentinel-probe pattern so silently-rejected invalid colors (canvas ignores them and leavesfillStyleat the previous value) fall back safely instead of being mis-parsed as the prior color. Unified the two previously-duplicated localparseColorhelpers. Bar-gradient dev demos on/charts/bar-chartcover three shapes: opacity fade, multi-color stops, and horizontal bars.
Changed
- JSX transform flipped to the automatic runtime.
tsconfig.jsonandtsconfig.mcp.jsonnow use"jsx": "react-jsx"instead of the classic"jsx": "react". JSX compiles to imports fromreact/jsx-runtimerather thanReact.createElement(...), matching React 17+ guidance and removing the "outdated JSX transform" runtime warning that was peppering test output. ESLint flat config layersreactPlugin.configs["jsx-runtime"].ruleson top ofrecommendedto disablereact/react-in-jsx-scopeandreact/jsx-uses-react(both obsolete under the new runtime). 17 test files lost their now-redundantimport React from "react"imports; the 51 files that referenceReact.Xtypes kept theirs. Rollup'sexternalpredicate inscripts/build.mjsextended to cover thereact/jsx-runtimeandreact/jsx-dev-runtimesubpaths alongside the existingreact-dom/serverentry (auto-external marks package roots external but not subpaths). - Empty legends no longer reserve margin.
useChartLegendAndMarginpreviously returned a truthy legend object withlegendGroups: [{ items: [], label: "" }]when mounted with nodata(push-API pattern) pluscolorBy— that reserved 110px of right margin and rendered only the legend's header neatline. Zero-item legends now resolve toundefined, so no margin is reserved and the chart uses the full width until categories arrive. Surfaced by the "Update: Bar Chart" demo on/features/push-api. - Dependency hygiene.
react-routerandmarked-gfm-heading-idmoved fromdependencies→devDependencies(both are docs-only — never imported by anything undersrc/). Removedtslibas an explicit dev dep (noimportHelpers: truein either tsconfig, so TypeScript never emits tslib references).@testing-library/domnow an explicitdevDependency(was a transitive peer of@testing-library/react@16+; missing onnpm install --legacy-peer-depsand broke 79 test files on the publish workflow). Net: Cloud consumers installing the library get 19 runtime deps instead of the 22 they were getting before.grep -c "react-router\|marked-gfm-heading-id\|tslib" dist/semiotic.module.min.jsreturns 0.
Fixed
- Publish workflow OOM in
prepublishOnly. Four of the fivenode scripts/build.mjsinvocations across package.json scripts were missing the--max-old-space-size=8192flag thatdistanddist:prodcarry; the one that ran on publish hit Node's 4GB default heap ceiling and died during rollup minification with a mark-compact GC failure (exit 134). All five invocations (build:analyze,build:prod,pretest:dist,release:check,prepublishOnly) now share the heap bump, and the last two route throughnpm run dist:prodso there's one source of truth for how to invoke the build. parseCanvasColordetects silently-rejected invalid colors. The browser ignores invalid CSS color assignments without throwing —fillStylestays at whatever was set before. Without a probe, feeding an invalid color to the parser would mis-read the previous color as the caller's input. Added a sentinel-set-first pattern: assign#010203before the user's color, then compare; if the sentinel is still there (and the input wasn't literally the sentinel), return the fallback tuple. Also guards the non-stringfillStylecase (CanvasGradient/CanvasPattern).buildBarGradient/buildRectSVGGradient: < 2 valid stops falls back to solid. Both previously checkedfg.colorStops.length >= 2but then filtered outNaNoffsets inside the loop — a configured list of 2 stops with one NaN produced a single-stop gradient (flat color) or, on the SVG path, emittedoffset="NaN"which invalidates the whole gradient. Now both filter for finite offsets first, clamp, then require ≥2 valid survivors before building.- Bar renderer preserves
CanvasPatternfills whenfillGradientis set. The opacity branch ofbuildBarGradientused a hardcoded"#4e79a7"fallback when the resolved fill wasn't a string, silently replacing aCanvasPatternfill with a grey gradient. Now guards: if the resolved fill isn't a string, the gradient is skipped entirely and the pattern fill renders as intended. - Candlestick transition exit stubs preserve bodyWidth.
snapshotPositionsnow capturesnode.bodyWidthintoprev.wso the candlestick exit node reads the pre-transition width instead of falling through to the 6px default on the final frame.getNodeIdentityprefers an existing_transitionKeyover the datum-derived key so exit stubs stay stable across overlapping transitions (affects all exit-node types, not just candlestick). CandlestickChartOHLC validation gap. When the user asks for OHLC mode but the data is missingopen/closefields,warnMissingFieldandvalidateArrayDatanow cover all four accessors. Previously the scene builder silently dropped bars and the chart rendered blank with no feedback.
Tooling
noUnusedLocals/ ESLint cleanup pass. 17 test files undersrc/components/andsrc/__tests__/scenarios/had bareimport React from "react"imports that existed only to satisfy the classic JSX transform. Removed with the transform flip. The 51 files that useReact.SomeType/React.ComponentProps<>kept their imports.- 3246 unit tests pass (was 3216 in 3.4.1). Net adds: 9
buildBarGradienttests + 6 SVGgradientFilltests inSceneToSVG.test.tsx+ 1BarChartempty-legend suppression test + 8parseCanvasColortests covering hex normalization, named colors, invalid-with-string-prev, invalid-with-non-string-prev, and the sentinel-self edge case + 2ordinalSceneBuilderstests for roundedEdge tagging and fillGradient attachment + 2 NaN-stop fallback tests.
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 (noMath.random()at module scope).- OUTSTANDING_WORK.md — added "Legend auto-population from pushed categories [YELLOW]" under the Push API section. Captures the 3.4.2 short-circuit (empty legends don't reserve margin), the real design (an
onCategoriesChangecallback on StreamOrdinalFrame + StreamXYFrame threading throughuseChartSetupstate intouseChartLegendAndMargin's existingcategoriesparam), known landmines (first-ingest timing, sorted-array dedupe, XY'sgroupAccessorvariant,LinkedCharts+CategoryColorProviderinteraction), and an estimated ~150 LOC surface across 5 files.
- 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
CandlestickChartHOC (semiotic/xy) — wrapschartType="candlestick"with the same mode-aware, animated, push-API conventions as the other XY HOCs. Required:highAccessor,lowAccessor. Optional:openAccessor+closeAccessor— omit both and the chart degrades to a range/dumbbell visualization (endpoint dots + wick, no body). Honorsmode="primary" | "context" | "sparkline":scalePaddingscales from width (12 / 10 / 3) to keep leftmost/rightmost bars from clipping,extentPaddingdrops to 2% at widths ≤200 so the y-domain isn't padded into uselessness, and sparkline zeroestop/bottommargin (axes are stripped, so the 2px defaults were dead space). Full docs page at/charts/candlestick-chartwith static ↔ streaming toggle, range-chart demo, compact-mode grid for OHLC + Range, and an Animation section demoing data-morph (seeded regenerate button) and a sliding push/remove window.- Candlestick animation support — the transition pipeline in
pipelineTransitions.tsgained full enter/update/exit branches fortype: "candlestick"nodes. Bars matching by x-identity smoothly interpolate all four y-coords (openY,closeY,highY,lowY) when data updates; new bars fade in; scrolled-off bars fade out with a held-in-place gray stub. Snapshot carriesbodyWidthtoo so exits don't jump to a 6px fallback on the final frame. Renderer now compositesdecayOpacity * style.opacityso decay and transition fades stack.getNodeIdentityprefers an existing_transitionKeyover the datum-derived key so exit stubs stay stable across overlapping transitions (fixes a latent reshuffle risk for all exit-node types, not just candlestick). - Server-side rendering for candlestick —
renderChart("CandlestickChart", ...)works through a new entry inserverChartConfigs.ts. Passthrough config: HOC-level accessors map 1:1 to frame-level ones;openAccessor/closeAccessorare forwarded without defaults soPipelineStorecan auto-detect range mode. compactMode: booleanonuseChartModereturn — the context∨sparkline union now lives on the hook instead of being recomputed in each HOC.GaugeChartconsumes it (replaces the localmodeIsContext || modeIsSparklineflag and collapses three conditional-render branches into one).candlestick-range-*visual regression fixtures — 3 new modes × 3 browsers = 9 baselines added to the chart-modes matrix specifically covering range-mode rendering (the path that motivated the dot-radius cap).
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 < 60with a 2px minimum stroke. At sparkline heights the protrusion above/below a tiny body is often <2px and lands on subpixel boundaries, antialiasing to ~11% alpha (invisible). Drawing the wick last shows the full high-low range as a continuous line through the body. - Range-mode dot radius scales with
bodyWidth/2and caps atlayout.height * 0.12(was hardcodedmax(wickWidth * 2, 4)— ≥4px always, marble-sized on a 24px row). Scales up for primary/context. - Scene builder now computes the same gap-derived
bodyWidthin OHLC and range modes so the renderer has a scale-aware basis for dot sizing.
- Wick is drawn on top of the body at
GaugeChartneedle formula simplification —innerRadius > 20 ? innerRadius - 8 : radius - 1. TheMath.max(1, ...)/Math.max(2, ...)floors in the previous formula were dead: the guarded expression is always well above the floor in either branch.- Type safety sweep — ~216
anytypes eliminated across the codebase. Scene-node interfaces, scale helpers, hook returns, and accessor resolution gained concrete types. No behavior change; catches more regressions at compile time. - Major dependency updates —
@playwright/test+playwright-chromium^1.17.1→^1.59.1(regenerated 9 darwin baselines for chromium font-rendering shifts on label-heavy charts),vitest+@vitest/coverage-v8+@vitest/ui^4.0.18→^4.1.4,typedoc^0.28.17→^0.28.19,@axe-core/playwright^4.11.1→^4.11.2,@modelcontextprotocol/sdk1.27.1 → 1.29.0,@types/nodealigned to Node 22.19.17 (matches the Volta-pinned runtime)..node-versioncorrected from18→22.22.1.
Fixed
RealtimeHistogram.showLegenddead pass-through —showLegendwas being fed intouseChartModebut the resolved value was never consumed (the HOC doesn't construct alegendprop for StreamXYFrame). Removed the feed-in and updated the comment to explain the absence.arrowOfTimewrongly exposed onStreamOrdinalFrame— removed. The prop only applies to XY time-series layouts; its presence on the ordinal frame was a leftover from a shared-types refactor.- Doc TOC duplicate-key warning — two sections titled "When to reach for which" on
/theming/semantic-colorsslugged to the same React key. Renamed to "When to reach for which role" and "When to reach for which primitive";PageLayoutadditionally de-dupes TOC keys defensively so a transient DOM overlap during route transitions can't re-surface the warning.item.idstill carries the real heading id for anchor navigation;item.keyis a separate React-only identifier. - Shadowed cookbook import in
App.js—import CandlestickChartPage from "./pages/cookbook/..."was re-importing the same symbol used by the new/charts/route, so the charts-route fell through to the cookbook recipe. Renamed toCandlestickCookbookPage.
Tooling
ai/schema.jsonandvalidationMap.tsgainedCandlestickChartentries;check-schema-freshness.jsandcheck-ssr-alignment.jsboth pass.
- Authflow session‑scoped OTP cooldowns close an abuse vector where users changed phone/email mid‑flow to reset OTP cooldowns.
- 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 cascade —
valueFormaton ordinal HOCs andxFormat/yFormaton XY HOCs now flow through to the default tooltip automatically, so a BarChart withvalueFormat: d => \$${d/1000}k`shows "$450k" on both the axis and the tooltip. Wired into: BarChart, StackedBarChart, GroupedBarChart, DotPlot, SwarmPlot, SwimlaneChart, LineChart, AreaChart, StackedAreaChart, Scatterplot, BubbleChart, ConnectedScatterplot, QuadrantChart, Heatmap.buildOrdinalTooltipandbuildDefaultTooltipgained format params; a newapplyFormathelper wraps formatter calls in try/catch so a misbehaving formatter falls back to the built-informatValinstead of breaking the tooltip. Customtooltipprops still fully override the default (re-pass the formatter insideTooltip({format})/MultiLineTooltip({fields:[{format}]})if you want it to apply). New "Format Cascade" section on/features/tooltips`. sort: "auto"on ordinal HOCs — preserves insertion order while streaming and falls through to value-desc on static data. Applied tooSorton the frame and tosorton BarChart / StackedBarChart / GroupedBarChart / DotPlot. DotPlot's default changed fromsort: truetosort: "auto"— fixes categories shuffling during streaming in the quick-start docs demo and any push-API usage.replace()method onStreamOrdinalFrameHandle— atomically swaps the dataset while preserving the store's category insertion-order memory and the transition position snapshot. Routes through a newDataSourceAdapter.setReplacementData()(emits{bounded: true, preserveCategoryOrder: true}); falls through to progressive chunking for large replacements.LikertChart's re-aggregation now usesreplace()instead ofclear() + pushMany()so streaming question order stays stable across ticks.Changeset.preserveCategoryOrder— new flag on the ingest changeset. When true on a bounded changeset, the store replaces the buffer contents but does NOT clear its category insertion-order memory, and marks itself as having received streaming-sourced data. The machinery that makes aggregator HOCs (LikertChart, future density/bin charts) behave like live streams even though the transport is wholesale replacement.getScales()on the sharedRealtimeFrameHandle(optional) — routed through 8 ordinal HOCs + 9 XY HOCs + 5 realtime HOCs. Returns the frame's resolved scales ({o, r, projection}for ordinal,{x, y}for XY). Network/geo/hierarchy HOCs stay compliant by virtue of the method being optional.LikertChartHandle— narrowed ref handle type exported from the public entry point. ExtendsRealtimeFrameHandleand typesgetScales()as returningOrdinalScales, soref.current?.getScales()?.o.domain()works without casts.useFramecomposition hook (src/components/stream/useFrame.ts) — extracts shared Tier A concerns across all four Stream Frames (size + responsive sizing, margin merge, foreground/background graphics resolution, animate → transition, current theme subscription, stable accessible-table id, rAF-coalesced render scheduling with unmount cleanup, pointer-coalesced hover handlers, theme-change effect). ~300 lines of duplication removed. No behavioral change for consumers.- FlowMap SSR support —
renderChart("FlowMap", ...)now works server-side via a newflowMapentry inserverChartConfigs.ts. Expands{flows, nodes}into the line-shape StreamGeoFrame expects, with value-proportional edge widths,edgeColorBy/edgeWidthRange/edgeOpacity/edgeLinecaphonored. Function-valuededgeColorByreturning literal CSS colors passes through unchanged (via sharedgetColor). LikertChartadded to the server-sideChartNameunion — was registered inCHART_CONFIGSbut absent from the TS union, sorenderChart("LikertChart", ...)would type-error despite working at runtime.animateprop on every HOC chart —animate?: boolean | { duration?, easing?, intro? }wired across all XY, ordinal, network, and geo HOCs. Stream Frames resolveanimate→transitioninternally, with synthesized intro animations: bars from baseline, wedges from collapsed arc, lines/areas clipped from left, points fromr=0, network nodes from chart center, geo points from center. Wedge angle interpolation for pie/donut data changes. Respectsprefers-reduced-motion.- Quadtree spatial index for point hit testing on XY (scatter/bubble), Geo (proportional symbol maps), and Ordinal (swarm plots) when point count exceeds 500. Each store tracks
maxPointRadiusso the hit tester widens its query for variable-size points (BubbleChart, proportional symbols). SharedfindHitPointInQuadtree(src/components/stream/quadtreeHitTest.ts) usesquadtree.visit()to enumerate every candidate within the search region, eliminating the nearest-only miss thatquadtree.find()had on heterogeneous-radius scenes. Path2Dcache on network edges (NetworkBezierEdge/NetworkRibbonEdge/NetworkCurvedEdge) —_cachedPath2D+_cachedPath2DSourcefields invalidate whenpathDchanges. Shared betweenNetworkCanvasHitTesterandnetworkEdgeRenderer.waitForChartReady/waitForAllChartsReady/waitForRafs/waitForStreamingUpdateinintegration-tests/helpers.ts— event-driven Playwright waits replacing the per-specwaitForVisualization+waitForTimeout(N)pattern.HoverPointerCoordstype inhoverUtils.ts— narrower hover-handler signature replacing theas unknown as React.MouseEventcast that the rAF-coalesced path used to need.ordinalFixtures.ts+recordCanvasOpstest utilities — shared sample datasets for bar-chart tests; behavior-level draw-op recorder that replaces brittletoHaveBeenCalledTimesassertions in canvas-renderer tests.describe.eachcombinatorial coverage forlineCanvasRendererover (curve × decay × thresholds), exercising the path-selection invariants that previously had a single test.- 3000+ unit tests passing (was 2890 in 3.3.x). Added cache-invalidation regressions for
_colorMapCache,_colorSchemeMap,_categoryIndexCache,_stackExtentCache, accessor explicit-clear, ParticlePool free-list,findHitPointInQuadtreevariable-radius,resolveCSSColorversion counter, swimlane bandwidth clamp. - Theme-driven selection opacity —
theme.colors.selectionOpacity(already defined onSemioticTheme; built-in presets set it to 0.1–0.15) is now wired into the dimming applied byhoverHighlight, legend isolate, and linked selections. Previously the value was emitted as the--semiotic-selection-opacityCSS variable but never read. A newuseResolvedSelection(selection)hook merges the theme value into the selection config; every HOC plusTreemapnow passes through it. Resolution order isselection.unselectedOpacity(per-chart) →theme.colors.selectionOpacity→DEFAULT_SELECTION_OPACITY(library fallback). Clients that previously reached into the package to changeDEFAULT_SELECTION_OPACITYcan now do<ThemeProvider theme={{ colors: { selectionOpacity: 0.5 } }}>instead.
Changed
- Function comparator on ordinal
sort/oSortis now a category-key comparator — prior types said(row, row) => numberbut the frame always invoked it with category name strings, so any user passing a row-comparator was getting silently incorrect ordering. Tightened to(a: string, b: string) => numberon BarChart, DotPlot, GroupedBarChart, StackedBarChart, and the frame'soSorttype.useSortedDatatreats function-valued sort (and"auto") as pass-through since the frame owns category ordering. No usages insrc/,docs/src/, orintegration-tests/passed a function comparator. - Stream Frame perf pass —
OrdinalPipelineStoredecay/pulse no longer rebuild aMap<datum, index>every frame (cached against_dataVersion); pulse wedge inner loop went fromO(wedges × data)toO(matches per category)viagetCategoryIndexMap.PipelineStorestacked-area extent fused into a single pass;resolveColorMapshort-circuits on_ingestVersion. Geo line projection fused project + filter into one pass. ParticlePool.spawn()— O(1) free-list (stack of free indices) replaced the O(capacity) linear scan.evaluateBezierrewritten asevaluateBezierInto(out)so positions write into the particle directly — zero per-particle allocation per frame.- rAF-coalesced pointermove in all four Stream Frames — caps hit-testing + React re-renders at the display refresh rate (60 Hz) instead of the native pointer rate (often 120–240 Hz).
onMouseLeavecancels any pending move; latest coords always processed. - CSS-var color cache (
resolveCSSColor) — version-counter design plus a singletonMutationObserverondocument.documentElementand aprefers-color-schemematchMedialistener. Themes/class toggles/media-query swaps that bypass React still invalidate; per-framegetComputedStylethrashing is gone. DEFAULT_SELECTION_OPACITY: 0.2 → 0.5 — unselected (dimmed) elements stay readable when a selection is active. Override viaselection.unselectedOpacity(per-chart) ortheme.colors.selectionOpacity(viaThemeProvider, applies to every chart). Built-in theme presets set this to 0.1–0.15.barPaddingratio clamped to ≤ 0.9 inOrdinalPipelineStore— degenerate layouts (e.g. horizontal swimlane whereshowCategoryTicks: falseshrinks the left margin and the vertical content area is less thanbarPadding * 2) no longer paint zero-bandwidth bands.
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 newpreserveCategoryOrderingest path so the category Set isn't wiped on every re-aggregation; (2)sort: "auto"(DotPlot's new default) collapses to insertion-order while streaming instead of value-desc. - Composing charts as
position: absoluteoverlays no longer hides the base layer — StreamXYFrame and StreamOrdinalFrame used to paint--semiotic-bgacross the full canvas regardless of whether the chart was on top of another. PassframeProps={{ background: "transparent" }}on the overlay to short-circuit the fill; the built-in composed-brush demos (/charts/realtime-histogram) now use this pattern. Network/Geo frames already behaved correctly. - MultiAxis ordinal
rExtentsnot cleared on bounded ingest — whenrAccessoris an array, the per-axisrExtents[i]instances are distinct fromthis.rExtent, so clearing only the latter left stale min/max on subsequent bounded replacements. All per-accessor extents now clear together. - Streaming axis rendered ghost ticks after
replace()dropped a category —resolveCategoriesretains its insertion-order memory for FIFO stability on re-appearance, but only theundefined/"auto"branch was filtering to live categories. Explicit"desc"/"asc"/false/comparator branches rendered empty columns for evicted categories. Live-category filtering now happens once at the top of the function and every branch reads from it. - DataSourceAdapter progressive-chunk timer state —
scheduleNextearly returns (completed dataset, superseded data) didn't resetchunkTimer, sosetBoundedData/clearLastDatacould callcancelAnimationFrameon a stale token. Every exit path now resetschunkTimer = 0, preserving the "chunkTimer === 0iff no rAF scheduled" invariant. Fixed in bothsetBoundedDataand the newsetReplacementData. setReplacementDatamicrotask race — apush()/pushMany()buffered just before areplace()could flush after the replacement and append stale points onto the fresh dataset.setReplacementDatanow clears the pushBuffer +flushScheduledstate before emitting the changeset.react-dom/serverstripped to(void 0)(...)in the server bundle —rollup-plugin-auto-externalmarks package roots external but not subpaths, sorenderToStaticMarkupwas being tree-shaken to an undefined binding. Addedid === "react-dom/server"to the rollup external predicate. Verified no other production subpath imports hit the same trap.StreamOrdinalFrameHandle.replace()JSDoc and atomicity — routes throughsetBoundedData-style progressive chunking for large datasets, not a single synchronous change. Corrected the "Atomically replace" wording to describe the actual two-phase behavior (small datasets synchronous, large datasets chunked).- DataSourceAdapter unmount cleanup —
StreamXYFrameandStreamOrdinalFramenow calladapter.clear()in their lifecycle cleanup so in-flight progressive chunking and pending push microtasks can't fire after unmount. - MinimapChart polling rAF — tracks its handle and cancels on unmount + data change. Was leaking a recursive
requestAnimationFramepoll that kept callingsetOverviewScaleson unmounted components. - Cache invalidation completeness —
PipelineStore._stackExtentCachenow invalidates ontimeAccessor/valueAccessor/runtimeModechanges;OrdinalPipelineStore._colorSchemeMaponthemeCategorical/colorAccessor;OrdinalPipelineStore._categoryIndexCacheoncategoryAccessor/oAccessor. - Accessor re-resolution gates —
updateConfigblocks for x/y/time/value (PipelineStore) and category/o/value/r (OrdinalPipelineStore) usedconfig.X !== undefined, which silently skipped re-resolution when a caller explicitly cleared an accessor ({xAccessor: undefined}— valid React pattern). Switched to"X" in configso defined → undefined transitions revert to the fallback key. - GeoCanvasHitTester wasted fallback — when a quadtree is built, the linear scan after a quadtree miss is now skipped (the visit-based path is authoritative). Per-hit
.filter()array allocations for areas/lines also removed. - StreamGeoFrame hover via
e.currentTarget— handler readscanvasRef.currentinstead so it works under the rAF-coalesced path that passes a synthetic{clientX, clientY}payload. _resetCSSColorCacheForTestobserver leak — disconnects the globalMutationObserverandmatchMedialistener it installed; bumpscurrentVersionrather than resetting to 0 so any surviving WeakMap entries can't be re-validated.
Security
- Bumped
hono4.12.8 → 4.12.14 and@hono/node-serverto 1.19.14 (transitive viascripts/og-server.mjs). Resolves seven advisories — six inhono(cookie validation, IPv4-mapped IPv6 mismatch, path traversal intoSSG,serveStaticslash bypass,hono/jsxHTML injection) and one in@hono/node-server(serveStaticmiddleware bypass via repeated slashes). All moderate; reachable only from the OG-image build script, not from the published library.
Tooling
- Dev deps:
@modelcontextprotocol/sdk1.27.1 → 1.29.0,esbuild0.27.4 → 0.28.0,@types/nodealigned to Node 22.19.17 (matches the Volta-pinned runtime). - Realtime encoding docs — new "Tuning for streaming cadence" subsection on
/features/realtime-encodingwith guidance on duration-vs-push-interval tradeoffs (fast / pulsed / slow streams) and thereplace()requirement for aggregator HOCs to participate in the transition system. - CLAUDE.md / AI docs — "Composing overlays" pitfall note added.
scripts/create-release-branch.shnow (a) syncsai/schema.jsonversion to the bumped package version, (b) verifiesCHANGELOG.mdhas an entry for the new version, and (c) gates onnpm audit --audit-level=moderate. Override the audit floor withAUDIT_LEVEL=...if a release is intentionally shipping with known low-severity transitives.prettier3.8.1 → 3.8.3 (dev-only patch).
Removed
- The per-spec
waitForVisualizationhelpers in 9 Playwright spec files (consolidated intointegration-tests/helpers.ts).
- Network `customHoverBehavior`/`tooltipContent` callbacks no longer receive `d.type`; use `d.nodeOrEdge` instead.
- `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
sortprop on StackedBarChart and GroupedBarChart — Default:false(data insertion order). Accepts"asc","desc",boolean, or custom(a, b) => numbercomparator. Maps to frameoSort. Previously categories were always sorted by total value.edgeIdAccessoronNetworkPipelineConfig— EnablesremoveEdge(edgeId)single-ID edge removal. Accepts string or function accessor. Throws descriptive error if not configured when single-ID form is used.- Transition exits on
remove()—remove()now callssnapshotPositions()before buffer mutation in PipelineStore and OrdinalPipelineStore. Removed items get fade-out exit transitions instead of vanishing instantly. - Selection clearing on
remove()— All three stream frames (XY, Ordinal, Network) clear hover state when the removed datum matches the current hover. Prevents stale tooltips and ghost highlights. serverChartConfigs.ts— ExtractedrenderChart()dispatch from a 400-line switch statement into a lookup table of{ frameType, buildProps }entries. Each chart type is independently readable and testable.- Shared
computeDecayOpacity()— Decay algorithm consolidated from 4 inline implementations (OrdinalPipelineStore, NetworkPipelineStore, GeoPipelineStore) into the existingpipelineDecay.tsutility. Single source of truth. HoverDataunified type — All four stream frames now construct typedHoverDataobjects instead of ad-hoc shapes. Network frames usenodeOrEdgefield (replaces untypedtype); geo frames usepropertiesfield. Fixed GeoFrame mismatch where tooltip andcustomHoverBehaviorreceived different shapes. Breaking: NetworkcustomHoverBehavior/tooltipContentcallbacks no longer received.type— used.nodeOrEdgeinstead.- SSR angle convention fix — SVG wedge/arc rendering adds
π/2to convert from canvas convention (0 = 3 o'clock) to d3-shape convention (0 = 12 o'clock). Fixes -90° rotation on all SSR pie, donut, gauge, and chord charts. - SSR hierarchy theme colors — Treemap, CirclePack, and TreeDiagram
colorByDepthnow usesconfig.colorScheme(from theme) instead of hardcodedDEPTH_PALETTE. Default fill uses first scheme color instead of#4d430c. - SSR GaugeChart needle — Needle rendered via React elements (XSS-safe), positioned from inner (margin-adjusted) dimensions, uses
resolveTheme()for color, divide-by-zero guard ongMax === gMin. - SSR
sweepAnglepassthrough —sweepAnglewas on the props but missing from thepipelineConfigbuilder. Gauge arcs now render with correct sweep. - SSR
hierarchySumstring resolution — StringvalueAccessor(e.g.,"value") now resolved to a function before passing tod3-hierarchy.sum(). - SSR bottom legend positioning — Legend placed at
totalHeight - margin.bottom + 38(below axes) instead of hardcoded offset that overlapped chart area. - SSR ID uniqueness — All SVG element IDs (
data-area,axes,grid,legend,chart-title,annotations,semiotic-title,semiotic-desc, hatch patterns) prefixed with_idPrefixin multi-chart documents.renderDashboardpasses per-chart prefixes. - 88 new tests — Push API edge cases (17), server rendering coverage (27), HOC rendering integration (22), callback wiring + accessibility + bad data resilience (22). Plus 9 PipelineStore cache invalidation tests.
- Ordinal scene builder tests refactored — 14 exact-pixel assertions replaced with relationship/proportional assertions. Tests now survive layout constant changes.
Changed
as anyreduced: 240 → 164 — Hover data types, renderer arrays, pipeline config, accessor utils, SSR prop threading.- SSR
framePropsoverride priority —framePropsspread first, explicit top-level props override.margin/colorScheme/legendPositiononly override when defined (notundefined). - SSR gallery — All 15 charts use
renderChartwith explicit themes (11 different presets). Dark-themed charts have dark card backgrounds.
Fixed
getColor()/getSize()null datum guard — Optional chaining prevents crash when datum is undefined.ProportionalSymbolMapsizeDomain crash —filter(Boolean)+ optional chaining in accessor.resolveCSSColorcache — Restored per-canvasWeakMapcache withhas()check (handles falsy values).clearCSSColorCache()invalidates on theme change.pieceStylemerge null guard — UserframeProps.pieceStylereturningundefined/nullno longer crashes spread.- GeoFrame hover/click shape mismatch —
customHoverBehaviorandtooltipContentnow receive the sameHoverDataobject. - Bottom legend overlap — Positioned below axes area in reserved margin.
- 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/serverproduction API —renderChart(component, props)renders 27+ HOC chart types to standalone SVG strings. Supports all themes, legends (4 positions), grid, annotations (y-threshold, x-threshold, category-highlight, widget, enclose), and accessibility attributes (role="img",<title>,<desc>,aria-labelledby). SVG groups haveidattributes for Figma layer naming (data-area,axes,grid,annotations,legend,chart-title).renderDashboard(charts, options)— Multi-chart dashboard layout with title, theme, configurable columns. Each chart entry supportscolSpanfor wide charts.renderToImage(component, props, options)— PNG/JPEG rasterization via sharp (peer dependency). Configurablescalefor retina output.renderToAnimatedGif(chartType, data, props, options)— Animated GIF from streaming data windows. Options:fps,transitionFrames,easing,decay,windowSize,loop,scale.generateFrameSequence(frames)— Snapshot-based animation for topology changes (network failover, edge removal). Each frame is an independentrenderChartcall.- SVG hatch patterns —
createSVGHatchPattern()for server-rendered diagonal hatch fills. Used by FunnelChart vertical mode for dropoff bars. - Push API
remove()andupdate()— Selective data removal and in-place update across all stores (RingBuffer, PipelineStore, OrdinalPipelineStore, NetworkPipelineStore) and all HOC/frame handles.remove(id)orremove([ids])by ID (requirespointIdAccessor/dataIdAccessor).update(id, updater)for in-place mutation. Network:removeNode(id)cascades to edges,removeEdge(source, target)removes parallel edges. pointIdAccessor/dataIdAccessor— ID accessor props on BaseChartProps forremove()andupdate()targeting.- GaugeChart server rendering — Sweep angle, start angle, inner radius, threshold zone fills, needle indicator.
- FunnelChart server rendering — Horizontal and vertical modes with trapezoid connectors. Vertical mode supports hatch pattern dropoff bars.
- Sparkline server rendering —
renderChart("Sparkline", props)with no axes, 2px margins, no grid/legend/title. - 6 interactive docs pages — Render Studio, Theme Showcase, Dashboard Gallery, Email Preview, Export & Embed (with real GIF downloads), Push API demo.
Changed
hoverHighlightsimplified — Changed fromboolean | "series"to justboolean. Any truthy value triggers series-based dimming (requirescolorBy).- CSS variable resolution in canvas —
resolveCSSColor()resolvesvar(--name, fallback)viagetComputedStyleat paint time. Per-canvas cache avoids repeated calls within a paint cycle;clearCSSColorCache()invalidates on theme change. All 9 canvas renderers updated. extentPaddingnullish coalescing — Changed|| 0.05to?? 0.05soextentPadding: 0is respected.- Swimlane
skipMaxPad— Prevents trailing gap in swimlane charts by skipping max-side extent padding. frameProps.pieceStylemerging — Ordinal HOCs now merge user'spieceStylewith computed base style instead of excluding it. Enables stroke overrides.resolveGroupColor()in server rendering — XY line/area style fallbacks callresolveGroupColor(group)instead of hardcoding#007bff. Theme categorical colors flow through to server SVG.- Force layout
iterations: 0— Now skips simulation entirely for pinned node positions. Previously warm-start detection overrode to 40 iterations. - Background rect positioning — Server SVG background rect renders at SVG root, not inside translated group (fixes Figma import).
- Dependency bumps — vite 8.0.5, typedoc 0.28.18, vulnerable devdeps fixed.
Fixed
- Server legend margin — Legend position expands margin before width/height calculation (right:100, left:100, bottom:70, top:40).
- Server
framePropspassthrough —framePropsspread into renderer common object sopieceStyle,lineStyleflow through. - Server
effectiveColorScheme— Falls back totheme.colors.categoricalwhencolorSchemeprop not set. - Network
remove()return value — Returns node data before removal instead of empty array. - RingBuffer
update()snapshot safety — Proper type-aware cloning (array spread for arrays, object spread for objects). - Timestamp buffer desync on remove — Lockstep compaction removes matching indices from timestamp buffer.
buildRealtimeNodespreserving positions — Usesx: d.x ?? 0, y: d.y ?? 0instead of hardcoded zeros.- Dark mode CSS var strokes — Docs site sets
--semiotic-bgin both dark/light blocks.LiveExampleuses MutationObserver for chart remount on theme toggle.
- 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
- Renamed `baselineStyle` to `gridStyle` in XY frame props
- 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 fromsemioticandsemiotic/ordinal. - Range/dumbbell plot — Candlestick chart type now supports range mode: omit
openAccessor/closeAccessorand provide onlyhighAccessor/lowAccessorto render vertical lines with endpoint dots. SinglerangeColorviacandlestickStyle. No new HOC — demonstrates StreamXYFrame flexibility. scalePadding— Pixel inset on XY scale ranges to prevent glyph clipping at chart edges. Available onStreamXYFrameProps; HOCs pass viaframeProps={{ scalePadding: 12 }}. Domain and tick values unchanged.xScaleType="time"— New scale type createsd3.scaleTimefor Date-aware tick generation. Required for landmark ticks with timestamp data.sweepAngle— New prop onStreamOrdinalFramePropslimiting pie/donut arc to less than 360° (used internally by GaugeChart).- Multi-point tooltip —
tooltip="multi"on LineChart shows all series values at hovered X with color swatches. Custom functions receivedatum.allSerieswith{group, value, valuePx, color, datum}. - Click-to-lock crosshair — In
linkedHoverx-position mode, click locks the crosshair. Escape or click again to unlock. Source-aware unlock prevents multi-chart interference. - Hover-based sibling dimming —
hoverHighlighton all HOCs dims non-hovered series on data mark hover (requirescolorBy). - Per-series fillArea —
fillArea={["A","B"]}on LineChart fills named series as areas, others stay as lines. New"mixed"chart type with dedicated scene builder. - Multi-color gradient fills —
gradientFill={{ colorStops: [{offset, color}] }}on AreaChart for semantic color bands. Supportstransparent. - Line stroke gradients —
lineGradient={{ colorStops }}on LineChart/AreaChart for horizontal gradient strokes. - Axis config extensions —
includeMaxforces domain-max tick,autoRotaterotates labels 45° when crowded,gridStyle("dashed"|"dotted"|string) for grid lines,landmarkTicksbolds month/year boundaries. baselinePadding— Boolean prop on bar chart HOCs. Defaultfalsemakes bars flush with 0 baseline.hoverRadius— Configurable hit-test distance (default 30px) on all XY HOCs andStreamXYFrameProps.- ReactNode tick labels —
xFormat,yFormat,categoryFormataccept=> string | ReactNodewith<foreignObject>fallback. - Tick deduplication — Adjacent identical tick labels automatically removed.
getHitRadiusandMultiPointTooltipexported fromsemiotic/utils.isTimeLandmarkandtoDateexported fromhitTestUtils.ts(shared across SVGOverlay and tests).
Fixed
- 30px default hit radius — All 4 hit testers (XY, Network, Geo, Ordinal) now use
getHitRadius()from sharedhitTestUtils.ts. Previous 12px Fitts's law cap was too small for comfortable interaction. lineDataAccessordata flattening — StreamXYFrame now flattens line-object data before pipeline ingestion. Previously the pipeline readxAccessoron line objects (which lack that field), producing NaN extents.scaleTimedomain comparison —valueOf()comparison for Date objects prevents stale scales from blocking updates.- Annotation dark mode —
Annotation.tsxtext usesvar(--semiotic-text), connectors usevar(--semiotic-text-secondary)instead of hardcoded black. - SwimlaneChart
showCategoryTicks={false}— Now suppresses both tick labels and axis title. - Floating point tooltip precision —
formatValuerounds viatoPrecision(6). - Default tick format Date-aware —
defaultTickFormathandles Date objects (formats as "Jan 7" style). bodyWidth: 0on candlestick — Body rect skipped entirely, no invisible canvas elements.- Ordinal bar baseline — Value axis baseline draws at
rScale(0), not chart edge. Include-zero applied before padding. - Remap fast-path with
scalePadding— Disabled proportional remap when padding is set (forces full rebuild for correctness). - Candlestick
updateConfig— OHLC accessors andcandlestickRangeModerecomputed on prop changes.
Changed
baselineStylerenamed togridStyle— Applies to grid lines (not axis baselines, which stay solid).- Build system —
rollup-plugin-typescript2replaced with@rollup/plugin-typescript(fixes TS compilation). - Playwright CI —
serve-examples:ciscript skips redundantnpm run dist. Timeout bumped to 120s.
- `@types/d3-quadtree` moved to devDependencies — no runtime impact.
- Dev-mode warning added: frame callbacks now warn when accessing `d.data?.field` incorrectly.
- Removed `@modelcontextprotocol/sdk` from production dependencies; now bundled via esbuild in MCP CLI.
- 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.dataaccess warning — Frame callbacks warn when accessing wrapper properties instead ofd.data?.field. Zero production overhead. - Streaming-first docs — Landing and Getting Started pages restructured to lead with the streaming engine.
Fixed
@modelcontextprotocol/sdkremoved from production dependencies — MCP CLI now bundles the SDK via esbuild.npm install semioticno longer pulls in the 4MB+ SDK.@types/d3-quadtreemoved 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.
- 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).
onClickprop on all HOCs — Direct click handler receiving(datum, { x, y }).categoryFormatprop — Custom tick label formatting on ordinal HOCs.category-highlightannotation type — Highlight category columns/rows.labelPositionon threshold annotations — Position labels left/center/right (y) or top/center/bottom (x).- Coordinate-based linked crosshair —
linkedHoverwithmode: "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 palettes —
CARBON_CATEGORICAL_14,CARBON_ALERT,"carbon"/"carbon-dark"theme presets. - Legend line wrapping — Horizontal legends wrap to multiple rows.
showPointson 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
styleFncontract, LikertChart tooltip,category-highlightannotation positioning, crosshair cleanup on unmount,FlippingTooltipdependency array, removed deadslicePaddingprop.
See CHANGELOG.md for full details.
- 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 styling —
trainStroke,trainLinecap,trainUnderline,trainOpacity,forecastOpacityonForecastConfig. - Forecast: per-datum anomaly styling —
anomalyColor,anomalyRadius, andanomalyStylenow 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.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`
- 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 rendering —
renderHOCToSVGrejected geo components not in thevalidatePropsvalidation map. Geo components now skip validation gracefully and render correctly. - MCP
--portparsing —--httpwithout--portno longer produces NaN (falls back to 3001). - MCP "top-level fields" dead code — removed unreachable spread logic from
renderChart/diagnoseConfighandlers; updated Zod descriptions to match actual schema behavior. - suggestChart Histogram heuristic — removed unreachable
data.length >= 10check (suggestChart accepts 1–5 samples per its Zod schema). - renderHOCToSVG validation fragility — tightened unknown-component skip check to require exactly one "Unknown component" error.
Added
- MCP
getSchematool — 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
suggestCharttool — analyzes 1–5 sample data objects and recommends chart types with confidence levels, reasons, and example JSX. Supportsintentparameter (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 transport —
npx semiotic-mcp --http --port 3001starts a session-based HTTP server with CORS headers for browser-based MCP inspectors and remote access. - suggestChart input validation — Zod schema enforces
.min(1).max(5)on data array.
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). prepublishOnlycleansdist/to prevent stale dynamic import chunks in published tarball.
Install
npm install [email protected]
MCP Setup
{
"mcpServers": {
"semiotic": {
"command": "npx",
"args": ["semiotic-mcp"]
}
}
}
- 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, andcolorBy - FlowMap — origin-destination flow lines with width encoding, animated particles (
showParticles,particleStyle), andlineType("geo"|"line") - DistanceCartogram — ORBIS-style projection distortion based on travel cost 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,onZoomwith imperativegetZoom()/resetZoom() - Drag Rotate — globe spinning for orthographic projections, latitude clamped to [-90, 90]
- Tile Maps —
tileURL,tileAttribution,tileCacheSizefor slippy-map basemaps (Mercator only) - Reference geography —
resolveReferenceGeography()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-labelon 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-labelon ChartContainer toolbar buttons- 35 Playwright accessibility integration tests
Streaming
- Streaming legend support —
useStreamingLegendhook 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 —
SafeRenderrunsdiagnoseConfigon 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
colorByproduced undefined colors —useColorScalederives categories from data whencolorByis 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-nocheckremoved from Legend.tsx — full type safety restored- 38
as anycasts eliminated from PipelineStore useSyncExternalStorereplaces custom shim (concurrent mode safe)- Shared
useChartSetuphook — 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)
- `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
emphasisprop — all charts acceptemphasis="primary" | "secondary".ChartGridspans primary charts across two columnsdirectLabelrendering — labels now actually render via new"text"annotation typegapStrategyfixes —"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)
directLabelannotations no longer silently droppedgapStrategy="break"no longer draws lines through gapscolorBytype mismatch in network/hierarchy charts- Duplicate
amplitudeproperty inStreamOrdinalFrameProps
See CHANGELOG.md for details.
- 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.
- 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.
- 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 tooling —
semiotic/aischema, MCP server, CLI,validateProps,diagnoseConfig - 9 sub-package entry points —
semiotic,semiotic/xy,semiotic/ordinal,semiotic/network,semiotic/realtime,semiotic/geo,semiotic/ai,semiotic/data,semiotic/server
See CHANGELOG.md for the full list.