Active Foundation

Search documentation

Search all pages, headings and text, in the current package or topic or in all documentation.

Changelog

What changed for users of active-charts, per version.

@workspace/active-charts 6.0.0Status unknowncontent differs from the recorded release 6.0.0baseline ce081dc3

Release state

The version line at the top of this page is read from the package and the release register of Foundation. A recorded release counts only for exactly the content it recorded: when the package changed after it, this site does not call that version released. The section Unreleased lists what changed since, including the changes made since Foundation maintains the package.

Notable changes to @workspace/active-charts. Format based on Keep a Changelog; versioning per Semantic Versioning.

This file is the authoritative release history for @workspace/active-charts from v1.0.0 onward (the externally-publishable baseline).

A note on version-numbering#

The visible version-history may look non-monotonic at first glance. The package was internally iterated through 1.0.0 → … → 9.1.0 during pre-publication development, then reset to 1.0.0 on 2026-05-18 to mark the externally-publishable baseline with an API-stability promise. The internal 1.x → 9.x history is intentionally not preserved in this file — those versions never shipped externally and the reset was the cleanest way to start the public SemVer-contract from a meaningful 1.0.0. Git tags from the pre-reset period still exist for archeological purposes; consumers should treat the post-2026-05-18 1.x line as the only authoritative version-sequence.

[Unreleased]#

Maintained in Foundation (since 2026-10-21)#

  • Dependencies: @workspace/active-helpers replaces @workspace/active-utils. The export-font, delimited-text and menu helpers (./export-fonts, ./serialization, ./menu-items) now come from @workspace/active-helpers, the one package that owns them. Same functions, same behaviour; the public API is unchanged. A host that receives the charts receives active-helpers with it.

  • Browser suite without the platform appearance: a Playwright project standalone runs the suite with host: 'none' (the package defaults only), for a repository that has no platform stylesheet to read by path. The default project and its screenshot baselines are unchanged.

  • Documentation: references to documents of the source repository are text, marked (source repository: …), instead of links that resolve nowhere here.

Changed — BREAKING for imports (platform ADR-114)#

  • IsoDateString, toIsoDateString and the zone brand come from @workspace/active-format, which owns them since ADR-114; the package no longer depends on @workspace/active-types, and the managed package set of a charts consumer no longer contains it. The props are unchanged; a consumer imports the brand constructors from active-format.

Changed — BREAKING for the managed package set (ADR-124)#

  • Formatting comes from @workspace/active-format, not from the formatters entry of a translation package. The manifest declares @workspace/active-format instead of @workspace/active-i18n; the package imports nothing from a translation package any more. No prop, export or behaviour of active-charts changed: its presentation type is still the ValuePresentation union, now owned by @workspace/active-format.

  • A consumer that formats next to active-charts with the same functions imports them from @workspace/active-format.

[6.0.0] - 2026-10-18#

Changed — canonical transfer form (2026-10-18, platform ADR-112)#

  • The manifest is the transferable manifest (RPS-D7): no workspace package in devDependencies (what the package's own development takes from Datyx, such as the lint preset, is host tooling in the reusable package register); the declaration packages of what the source imports sit in the field of their module (@types/react and @types/react-dom as optional peers beside the react peers; the @types/d3-* packages as dependencies beside their modules). Nothing changes for a Datyx consumer.

  • tsconfig.json is self-contained: it no longer extends @workspace/typescript-config, with the same compiler options, because a bundler in a consumer reads it for every source file it compiles.

  • E2e harness: the host stylesheet it serves is the Datyx one, read from the host repository (active-ui's kit followed by @workspace/active-config/platform-appearance.css, fonts from packages/active-config/assets/fonts/v1); it stops with an error when they are missing, outside Datyx. Declared as host tooling. Rendered output is unchanged.

Changed — hierarchy node identity is a fixed-length structural key (ET-4B, ADR-122, CH-56, 2026-10-12)#

  • Treemap, sunburst and org-chart node keys are no longer the .-joined path of segments from the root but k plus a 14-character base-36 hash of the parent's key and the node's own segment (the host id, else the sibling position). The same place gives the same key across a data replacement; a hashed key that meets another node's falls back to that node's full path (detected per build, deterministic). Stored key characters are now 15 per node instead of growing with depth × id length (a 1,000-level chain: 15,015 instead of 2,471,098). Drill path, focus retention, React keys and DOM ids keep their behaviour; the DOM ids behind aria-activedescendant change form. No public API change.

  • Retained focus no longer reads the shape of a key. An org chart's focus kept across a collapse falls back to its nearest ancestor through the previous keyboard tree's parent links.

  • The org chart's provisional depth boundary (ADR-119) is unchanged; its development diagnostic no longer gives the old key growth as its reason.

Changed — timeline label placement is indexed (ET-4B B6, 2026-10-12)#

  • placeTimelineLabels reads only the blocks and markers near a candidate through an x-index instead of scanning all of them per event. The placement is identical (proved against the previous function on seeded layouts); 8,000 events place about 8× faster. Dense timelines still grow faster than linearly with the events per pixel column.

Added — ET-4 benchmark harness (ET-4B, ADR-123, 2026-10-12)#

  • benchmark:pe4 (the minified production bundle, the only profile with verdicts), benchmark:pe3, benchmark:sim and benchmark:selftest: a typed workload registry with deterministic generators, the sample policy (p95 only at n ≥ 20, three runs, nearest rank), the reproducibility block (sourceHash, bundleHash, machine, power, load, quiet), CDP memory and long-task metrics, dashboards, soak and export resource runs, and results.json plus summary.md per run. Test and benchmark code only.

  • CH-58: the selection/hover ratio of t8-selection is a diagnostic; two count guards (e2e/et4b-ch58-count-guards.spec.ts) and the absolute PE4 select budget replace it (QUALITY-BUDGETS § 28).

Added — hierarchy drill and return announced (ET-3B2, ADR-121, 2026-10-11)#

  • A treemap or sunburst drill and return are announced (CH-67). With drilldown, a drill says "Drilled into {name}" and a return "Returned to {name}" in the chart's live region, once per drill or return that actually happens, from the keyboard and the pointer alike; nothing on the first render, for Backspace at the top level, Enter on a leaf or a data update. The drill state commits a record of each accepted move with the level it produced, so a render that never commits says nothing.

  • Three new host keys: charts.accessibility.drilledInto, charts.accessibility.returnedTo, charts.accessibility.unnamedGroup. A host that does not provide them fails its coverage proof (ADR-028): migration note. No other public API change.

Fixed — pre-qualification (ET-3B, 2026-10-11)#

  • No keyboard item model without room (CH-63). A heatmap, radar, timeline or radial bar below its room draws no items, and now has no item model either: one image with its name, its description (with the no-room sentence) and its table; not a tab stop, no arrow stepping or position announcement, and no aria-activedescendant naming an element that is not drawn. With room the keyboard returns with no focused position. The radial bar's existing guard did not hold (undefined && … fell back to the hook default); useKbdBase now names no descendant while its model is off, also in the commit that turns it off.

  • A treemap leaf without a value keeps its keyboard element (CH-70). The keyboard reaches it and reads "(no value)"; its aria-activedescendant now names an element (0 × 0, nothing painted), as the sunburst's empty arc already did.

  • The unavailable sentence under forced colours (CH-69). The state's text takes CanvasText, as every chart text does (ADR-108).

  • No public API change. Docs: ACCESSIBILITY § 2 (the sankey switches layers with Enter, CH-68), § 5 (no room), and the ET-3 qualification protocol in BROWSER-AT-CHECKLIST and EVIDENCE (ADR-120).

Fixed — bounded safety correction (ET-2B2, ADR-119, 2026-10-10)#

  • Org chart depth (CH-59, ET2-OD-5, ADR-119). The org chart's data-table walk was still recursive: a valid chain of about 2,500 levels threw a RangeError. It is iterative now, and the org chart has a PROVISIONAL depth boundary of its own, 139 levels below the root, derived from the growth of its card identity keys (one segment per ancestor, CH-56): a deeper organisation, collapsed branches included, shows the workload-unavailable state with a warn diagnostic in every environment and offers no CSV.

  • No host-sized argument spread (CH-60). A collection whose length follows the data (heatmap categories, Sankey columns and a hub's neighbours, pie-like rows, sparkline points, radar values, histogram values, timeline lanes, legend entries, a series' hit-test points, linked hover rows, axis tick lines, bar rects of one category, compound series) was spread into Math.max / Math.min / push, which throws past the engine's argument limit (about 125,000). They go through maxOf / minOf (utils/math.ts) or a loop, with identical results; a sparse heatmap with 140,000 distinct rows and columns now draws. A source guard (et2b2-argument-spread-guard.test.ts) reviews every remaining spread.

Added — failure safety and resource boundaries (ET-2B, ADR-115 to ADR-118, 2026-10-10)#

  • Copy outcome (IR-004, ADR-116). Copy value, Copy values and Copy as table are disabled with a reason (charts.contextMenu.copyUnavailable) when the browser cannot copy at the moment the menu opens (no clipboard API, an insecure context); otherwise the outcome is announced in the chart's live region (charts.contextMenu.copied, charts.contextMenu.copyFailed). A failed copy reaches the host through the new contextMenu.onCopyError(error: ChartCopyError) or, without it, an error diagnostic in every environment; never both, never with the copied text. New public exports: ChartCopyError, ChartCopyErrorCode, ChartCopyAction. No toast, no execCommand fallback.

  • Workload-unavailable state (ADR-115). The unavailable placeholder says why: data-chart-unavailable="input" (charts.unavailable) or "workload" (new charts.unavailableWorkload). A treemap or sunburst deeper than the PROVISIONAL boundary of 139 levels below its root (derived: the largest depth whose chain writes at most 10,000 path segments) shows the workload state with a warn diagnostic in every environment.

  • Sparse heatmap table (IR-005, ADR-117). The data table is the matrix while it is no larger than the list of present cells, else that list (row, column, value) with a summary (charts.dataTable.row, charts.dataTable.column, charts.dataTable.cellsListed).

Fixed — failure safety and resource boundaries (ET-2B, 2026-10-10)#

  • Sankey (IR-006). The cycle check walks an explicit stack: a deep acyclic chain (100,000 nodes) validates instead of overflowing the call stack. Same cycle and duplicate-id diagnostics.

  • Heatmap (IR-005). No structure grows with rows × columns: the keyboard reads present cells through a lookup and the overlay is one node, the focused position's (it still walks every position and reads an absent one "(no value)").

  • Hierarchies (CH-48). Every walk of a host tree is iterative (table, CSV, node keys, colour measure, selection index, keyboard map, the org chart's walks and ancestor climbs; the org chart's table walk followed in ET-2B2, CH-59): depth alone never throws.

  • Hierarchy keyboard (CH-55). One sibling list per parent instead of a copy per node: a flat tree of 10,000 leaves holds 10,000 slots, not 100 million.

  • Item keys (CH-49, ADR-118). One reader for every family: NaN, an infinity, an invalid Date and any non-key type name no item (as null); a boolean is its text everywhere, so a boolean bar is now selectable.

  • Unknown series type (CH-44). A runtime series type without a plugin shows the unavailable state with one development diagnostic instead of throwing in the accessible name.

  • Heatmap order (CH-51). Linear matching through sets; a missing category named twice is counted once in the diagnostic.

  • <Chart> in its unavailable state offers no CSV: the handle rejects with EXPORT_NO_DATA and the menu lists no CSV item.

  • Tests (CH-54). The oracle import guard also reads import.meta.glob, fails closed on a loader handed on as a value or by bracket access, and compares module paths case-insensitively.

Fixed — hierarchy data is a prepared snapshot (ET-1B3, ADR-114, 2026-10-10)#

  • Hierarchy (CH-52, ADR-114). A treemap or sunburst that laid out again on its own (a new size, a drill) after a host changed a fallback parent's value, or a children array, in place under the SAME root drew the live tree while every reader stated the preparation (Summary 30 drawn as 90, Sector 40 drawn as 80). One preparation now owns the measures AND the structure they were resolved over: the layout, node keys, drill path, table, keyboard, CSV, selection index and colour measure follow the prepared children, and the layout weights come from the preparation. The contract (API § 3.3): hierarchy data is a snapshot; when it changes, pass a new root object (structural sharing is fine). Under the same root the chart stays on what it prepared, consistent on every surface. Still one pass, O(nodes); no new public type.

  • The ET-1 oracle import guards share one reader that also sees vi.importActual / vi.importMock / vi.mock, template-literal loads and type arguments, compares whole path segments, and fails closed on a load whose module it cannot read (CH-53).

Fixed — a measure per preparation, zoom options at the commit (ET-1B2, 2026-10-10)#

  • Hierarchy (CH-50, ADR-112). A host that changed a node in place and passed a new root got geometry from the new value and every reader (table, tooltip, keyboard, announcement, Copy value, CSV, second label, share) and the strict mismatch rule from the old one: the resolved measure was cached per node for the life of the page. Each treemap and sunburst now resolves its data once per data object, and the layout, the check and every reader read that one resolution; nothing is kept across roots or charts, and hierarchy input is not required to be immutable. Still one pass, O(nodes).

  • Zoom (CH-47). Drag-select and touch pinch/pan read their options (axis direction and x map, minVisibleItems, whether zoom is enabled, plot size and margins) from the last committed render, never from a host render that never commits: a held transition that reversed the axis no longer made the visible chart zoom the mirrored rows, and one that raised minVisibleItems no longer refused a valid drag. A committed change still applies to the next gesture.

  • The ET-1 oracle import guard now compares the module a specifier names (extension, /index, ./ and ../ spellings), not its text.

Fixed — one measure, one identity, committed state (ADR-112, ADR-113, ET-1B, 2026-10-10)#

  • Hierarchy (IR-001, ADR-112). Treemap and sunburst size, label, share, read, copy and export every node by ONE resolved measure: a leaf's value, a parent's derived sum, a summary's own value only where nothing below it is valued. A parent value beside valued children used to add to the area (150 drawn for a group of 100 over 30 + 20, read as 50, exported as 100); a matching value is now silent and a contradicting one makes the chart unavailable with a diagnostic. The hierarchy CSV value is the resolved measure; both families divide a share by the shown root. The table walk is O(nodes) instead of O(nodes · depth).

  • Heatmap (IR-002, ADR-113). A coordinate is its item key (ADR-106): 1 and "1" are two columns, a timestamp and its Date one. The cells, keyboard, selection, table, hit test and Copy value share that identity. Two rows on one cell make the chart unavailable (selection on or off); a row with a null coordinate is left out with a diagnostic. HeatmapAxisConfig.order widens to readonly ChartItemKey[].

  • Controlled selection (IR-003, ADR-105 restored). The selection a handler reads is published at the commit (useInsertionEffect), not in render: a suspended host transition no longer changes the next request.

  • An independent semantic oracle suite (et1b-oracle-*) and its import guard check geometry and every reader against literal expectations.

Documentation — the chart-family authoring guide (ADR-111, CF-1, 2026-10-09)#

  • docs/ADDING-A-CHART-FAMILY.md is the one authoring guide for a new chart family: whether it is a family at all, its archetype, the shared routes it must use, the internal family register, the UI Lab page, the evidence layers and a Definition of Done, with a table of what the repository checks. API § 7 ("Authoring a New Chart") is now a pointer to it; its old forwardRef sketch is gone.

  • API § 3.2 records why RadarChart has no data labels and why its props extend BaseChartPropsWithout rather than CategoricalChartProps.

  • No runtime and no public-API change.

Fixed — residual shared runtime truth (ADR-108, CF-1C3b, 2026-10-09)#

  • Print keeps the chart as drawn. The print stylesheet no longer forces a grey text colour, a light-grey grid or a white page: they broke the label language (an on-mark label printed at 3.9:1 in light and 1.7:1 in dark). A chart prints on its own mode's background with its colours exact.

  • Forced colours: labels on a mark. Chart text keeps CanvasText on a Canvas halo, so a label on a pie, funnel, treemap or sunburst mark that keeps its colour stays readable under any system scheme.

  • A deliberate press on <Chart> and the Cartesian wrappers. A pinch and the click after a long press that opened the menu no longer select or activate; the Cartesian overlay now uses the press rule of every family.

  • Radial scales show what they step. Radar rings and radial-bar ticks never repeat a label (small values, or money in whole units, step coarser).

  • Diagnostics per input or chart. A development diagnostic is once per condition and input object, or per chart where it concerns a value; a second chart is no longer silenced by the first for the session.

Changed — BREAKING (what a family accepts, it does, ADR-107, CF-1C3, 2026-10-09)#

  • No replay switch where there is no replay. The menu of <Chart> and the Cartesian wrappers takes features.copy, zoom, series and export; features.replay was accepted and ignored and is now a type error. LinearGauge and BulletChart no longer take animation.replay: they never replayed. GaugeChart keeps both.

Fixed — archetype and family truth (ADR-107, CF-1C3, 2026-10-09)#

  • Stagger on the profile. Funnel segments and the treemap and sunburst depths stagger on the chosen profile's step and cap, not on calm.

  • onAnimationComplete on time. A staggered entrance (funnel, treemap, sunburst) reports load once, when its last mark has ended, counted from the first frame it is drawn in: the treemap no longer reports up to 300 ms late, the sunburst's morph no longer early.

  • No room, no broken geometry. Below its room the timeline, sankey, heatmap, radar, linear gauge and bullet draw no marks and say so in a caption and in their description; the name, the table and a scalar's readout stay. Normal sizes are unchanged.

  • The organisation chart. The zoom presets are a real menu (focus moves in and back, arrows, Home, End, Escape, Tab; presets as checked radio items), the toolbar stays at full strength while focused and stops its hover transitions without motion; its shadows are the chrome tokens. Shift+F10 opens the focused card's menu on every card. A finger pans the view; a cancelled gesture ends the pan.

  • Items keep their element. A reordered slice, stage, sector, ring, radar series, treemap or sunburst node, sankey node or flow, or org card keeps its DOM element: React keys follow identity.

  • Radial scales. Radar rings and radial-bar ticks never step finer than the values' semantic quantum (a count at whole numbers).

  • <Chart> and <ChartDefaults>. <Chart> takes the provider's tooltip and accessibility under its own, as every wrapper does.

  • Copy value on a hierarchy node writes the value its tooltip and table write (the sum of its valued children, else its own).

  • Internal: the timeline's CSV rows live with the other families'; the treemap's leaf shading reads its sibling position in O(1); latent literal axis colours and series easing are tokens.

Changed — BREAKING (public types say what the package does, ADR-106, CF-1C2, 2026-10-09)#

  • A category is its value. A Cartesian category axis follows ChartItemKey: the number 1 and the text "1" are two categories with two bands, two marks, two table rows and two selectable items, even when both read "1"; before, the band axis merged them by text. A Date category is its time. A reference value on a category axis (cross line, annotation, tickValues, a linked range) is given in the type the data has. The tick formatter still receives the category's text.

  • <Chart> is generic and takes events. <Chart<TDatum>> (inferred from data): its context menu hands the host a typed CartesianPoint<TDatum> instead of never, and events bundles the chart and selection events, the individual prop winning.

  • SeriesType names only <Chart> series. It is SeriesOptions['type'], the thirteen Cartesian series; the standalone families and the scalars are gone from it.

  • No internal shape in a public type. HistogramSeriesOptions loses the internal _precomputedBins; HeatmapChartProps['label'] is the new exported HeatmapLabelOptions.

  • One Cartesian prop contract. Every Cartesian wrapper derives its props from CartesianChartProps, which now also carries the events bundle with the selection events. No wrapper gains or loses a prop.

Fixed — shared truth and touch (CF-1C1, 2026-10-09)#

  • A tap's echo is no hover. On every Cartesian chart the compatibility mouse events a browser fires after a tap no longer turn the tap's sticky tooltip into a hover that never ends: the card stays, then leaves on its touch timer, and a real mouse afterwards hovers as before. A tap with pinning pins and announces once.

  • A sliding finger names the mark under it. On the eleven standalone families a slide now carries the tooltip, attention and current item to the mark under the finger, as the pointer does; before, the card kept naming the mark the slide started on.

  • CSV and copy keep every column. Raw rows without declared columns (the categorical families, the heatmap) export every field any row carries, in first-seen order, instead of the first row's fields only. Explicit columns still win.

  • Missing is never blank or 0. A Cartesian data-table cell without a host value reads "(no value)", as the tooltip does. A treemap or sunburst node without a value reads "(no value)" in the tooltip and the announcement instead of 0, and a group is announced with the sum of the values below it, as the table reads it. A real 0 stays 0.

  • Repeated sibling ids. Treemap, sunburst and organisation-chart siblings that repeat an id stay two nodes (keyboard, attention, drill path) instead of one silently overwriting the other, and are reported once in development.

  • Category identity follows the selection key. A categorical legend keeps a number or a Date apart from a text that spells it (1 and "1") in any row order, and names a Date by its time. The legend entry id of a number or Date category changes form (it was its text); entry ids stay opaque, so store them as reported.

Added (CF-1C1, 2026-10-09)#

  • SingleTooltipData.valueMissing: the item carries no value; the default card writes "(no value)" and yValue is no reading.

Changed — BREAKING (bounded host composition, ADR-105, AF-B, 2026-10-09)#

  • Headless legend. useChartLegend() and the legendLink prop let a host render its own legend from the chart's resolved discrete legend (ChartLegendEntry: identity, target, label, semantic key as painted, visibility, available actions) and act through the chart's own legend route (activate, showAll), which owns the state, the guards and the announcement. The five standalone legend families now announce and report every legend change, isolate and Show all included.

  • Identity-based legend events. onLegendChange replaces onLegendToggle (no alias): one event per applied change from every route, naming the entry and target by identity, every hidden target and the input. ChartLegendItemRef is removed; a menu's legend target carries the ChartLegendEntry. The export legend types are ChartExportLegendItem and ChartExportLegendOptions.

  • Every export returns its file. The handle exports and the standalone functions resolve with ChartExportResult (format, filename, blob, downloadStarted, warnings); exportChartAsCsv is asynchronous. download: false produces the same file, captured once and at rest, without a download. onExportComplete(result) receives that object, also for menu exports. Failures stay ChartExportError; an inherit-mode PNG whose web font could not be embedded warns font-fallback.

  • A controlled change is a request. requested is always present on ChartSelectionChange and OrgCollapseChange. With selectedItems or collapsedIds the host prop is the truth: a request is computed from it, and drawn, listed and announced only once the host passes it back.

  • One input vocabulary. Selection, activation, org collapse, the legend and the context menu report ChartInteractionSource: 'pointer' (mouse, pen), 'touch' (finger) or 'keyboard'; a menu action reports the input that opened the menu (OrgCollapseChange no longer says 'menu').

  • Serialisable zoom easing. zoom.animation.easing is a ChartMotionEasing name (default 'calm', the same curve) instead of a function; zoom.animation.duration is bounded to 0 to 3,000 ms.

  • Per-format Export menu. contextMenu.features.export takes true, false or {png?, svg?, csv?, pdf?}; a format the chart cannot write is never offered and an empty submenu is not shown. The default is unchanged.

  • Public type cleanup. ChartAnnouncementCategory (without the internal 'immediate') and ChartTooltipContext name types that public signatures referenced; contextMenu carries no any, and the thirteen Cartesian wrappers type the menu element as CartesianPoint<TDatum>.

Changed — BREAKING (motion, ADR-104, 2026-10-08)#

  • animation is a profile and six classes. {enabled, profile, entrance, update, exit, tooltip, attention, replay}; the entrance fields (duration, easing, type, stagger, layers) moved under entrance. AnimationUpdateOptions and ExitPhaseOptions are removed; ChartMotionProfile, ChartMotionEasing, EntranceMotionOptions, UpdateMotionOptions, ExitMotionOptions, InteractionMotionOptions, ReplayMotionOptions and SeriesEntranceMotionOptions are new. A series owns only its entrance (with delay) and its exit.

  • Profiles calm (default) and responsive. Calm: entrance 400 ms (stagger 30 ≤ 200), update 300, exit 200, tooltip 130, attention 140 (release 250), replay 1,000; responsive is shorter in every class.

  • Data updates move by default. update: false switches it off.

  • Named curves only. easing is 'calm' | 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'; no free CSS curve, no callback. The default is the calm curve, also for the scalar count-up; the default gauge sweep no longer overshoots.

  • Standalone, scalar and graph families take profile, and where they have them tooltip, attention (standalone, sankey, org chart) and replay (the gauge). The sankey and the organisation chart now take animation.

  • animation: false also stops a discrete zoom animation and the grouped slot movement, as reduced motion already did.

Added (motion, ADR-104, 2026-10-08)#

  • A calmer interaction. The tooltip fades in and out on the tooltip duration and travels between the marks it describes (a change of side, a card following the pointer or a finger, and a new card are placed directly); a return while it fades reverses instead of blinking. The crosshair and the cues stay immediate and now leave together with the tooltip. The context steps back on the attention duration and returns more slowly when the pointer leaves. The heatmap's colour-key marker travels to a new value.

  • Grouped series reflow. When a grouped bar, range bar, box plot or dumbbell series is hidden or shown, the others move to their new slots on the update timing, after the exit if one is configured; the pointer and the labels follow the drawn glyph.

  • A calmer gauge replay on the replay timing: back for about a third, up for the rest, on an ease-in-out.

Fixed (motion, 2026-10-08)#

  • animation.update animates every data change. An isolated change snapped and only a second change inside the same window moved. Every change after the first rows now travels from what is drawn, and a change mid-update continues from there.

  • A horizontal bar moves its length in an update; a bar or histogram moves its whole rectangle; waterfall, candlestick and OHLC updates move, every part of a glyph together.

  • Line and area updates follow the configured curve (they used a fixed one).

  • A change of orientation is drawn at once, never as a morph between a vertical and a horizontal layout.

  • No neighbour flashes into its final slot when a grouped series is hidden, and range bar, box plot and dumbbell keep their slots through an exit, as grouped bars do.

  • The tooltip's first frame carries its content. Firefox could show one frame of a target without its card.

  • A pointer that moves within one item of a standalone family renders nothing; a travelling tooltip renders no family component either.

  • A plain <Chart> follows ChartDefaults.animation.

  • An export never captures a motion mid-way: every scripted motion (an update, a reflow, a count-up or replay, a discrete zoom) declares its end and the export waits for it, within the chart's motion budget plus one second (at least four).

  • Invalid durations fall back to the profile with a development diagnostic; tooltip and attention are held at 600 ms, geometry at 3,000 ms.

  • A two-finger gesture on <Chart> is no hover: a pinch or pan no longer drives the tooltip and the attention from its first finger.

  • A tap on a radial bar ring shows its card, and a sliding finger carries it, as on the other standalone families.

Fixed (keyboard menu target, grouped bar exit, hierarchy attention, 2026-10-07)#

  • Shift+F10 and the ContextMenu key open the menu on the keyboard's current item on PieChart (and the donut), FunnelChart, RadialBarChart, NightingaleChart, RadarChart, TreemapChart, SunburstChart, SankeyChart (node or link) and TimelineChart, with or without selection and activation props. Before, without those props, they opened the chart's menu. Enter, Space, selection and tab stops are unchanged; the radar opens on the same vertex for Shift+F10 as for Enter.

  • A grouped bar series that leaves through its exit animation keeps its column. Hiding one of several unstacked bar series no longer fades the leaving bars across the whole category band, and the remaining bars hold their columns until the exit ends, then take their final columns. Rest geometry and an instant hide are unchanged.

  • Treemap and sunburst address a tile or arc by its node identity, the same identity the keyboard, selection and the menu use. Two nodes with the same label are always two nodes; a value change, resize or reorder keeps the hovered or focused node current; drilling in or out clears the previous view's hover and keyboard highlight instead of marking another node.

Added (chart-family capabilities: histogram bins, funnel context, timeline time, bar label placements, sparkline domain, ADR-103, 2026-10-07)#

  • Histogram binWidth: every bin the given width in x units, on multiples of it; the last bin includes its upper bound. One bin rule: binCount, binWidth or bins (HistogramBinRuleOptions); an untyped call with several uses bins, then binWidth, then binCount, and says so in development. A width that is not a finite number above 0, or that would make more than 1,000 bins, falls back to the default count with a diagnostic.

  • Histogram areaProportional (count and sum, HistogramAmountOptions): unequal bins are drawn per the narrowest bin's width, so a bar's area is its amount; equal bins draw exactly as before. Labels, the tooltip value, the announcement and the table keep the amount; the tooltip adds a "Per {width}" row (host key charts.tooltip.binRate). Unequal bins without it are reported in development.

  • Funnel formatter context: dataLabels.formatter(value, context?) receives FunnelLabelFormatterContext (datum, seriesId, seriesName, the source index, shareOfFirst, shareOfPrevious over the visible stages). FunnelDataLabelOptions types the funnel's dataLabels.

  • Timeline timeAxis with TimelineEvent.time (TimelineTimeAxis, TimelineTime): events placed by machine time, equal times on one x, markers never moved by labels; keyboard, table and announcement in time order, equal times in the host's order; the CSV keeps the host's order and adds time. Ordinal placement stays the default.

  • Candlestick bodyStrokeWidth (default 1, 0 allowed) on CandlestickChart and the candlestick series; the colour stays up.stroke / down.stroke, the wick keeps wickStrokeWidth; forced colours keep a border on a borderless rising body.

  • Bar label positions 'inside-end' and 'inside-base' (BarLabelOptions, BarLabelPosition): inside the bar against its value end or its base, by sign and axis direction, a stack segment at its own ends. Bar only; the waterfall keeps 'inside' | 'outside'.

  • BarChartSeriesConfig.label: a series' own labels, replacing the chart's label for that series as a whole object.

  • Sparkline domain: an exact two-sided value domain for line, area and bar; equal values draw at equal heights across sparklines that share it; values beyond it are drawn to the edge and keep their number.

Changed (chart-family capabilities, ADR-103)#

  • A bar Sparkline reads from zero. Its automatic domain now always reaches zero (padded only away from it); before, all-positive bars stood on the bottom of a padded min–max domain, so their lengths misstated the values (T9-D9). Line and area sparklines are unchanged. With a domain, a zero outside it is cut at the edge and the domain is never widened.

  • HistogramSeriesOptions and TimelineChartProps are type aliases (they carry the new unions); an interface … extends of either becomes a type … &. Setting binCount and bins together is now a type error (bins already won at runtime).

Added (selection and activation on the standalone families, ADR-102, 2026-10-06)#

  • Selection and activation on nine standalone families: selection, selectedItems, defaultSelectedItems, onSelectionChange and onItemActivate (also in events) on PieChart, FunnelChart, RadialBarChart, NightingaleChart, RadarChart, HeatmapChart, TreemapChart, SunburstChart and SankeyChart, with the meaning they have on <Chart> (ADR-042): a click or tap selects, then activates; Space selects, Enter activates (Shift+Enter on a sankey, whose Enter still moves between nodes and links); controlled or uncontrolled; single or multiple; an item that is not drawn is inactive, not removed. Without the props nothing changes.

  • The items: a slice, stage, bar or sector (an item of the chart's one series, keyed by categoryKey or selection.itemKey); a radar vertex (series × variable, reached on the keyboard with PageUp / PageDown); a heatmap cell with a row; a treemap or sunburst leaf by its id (a parent keeps drilling); a sankey node by its id and a link by its id or its source and target.

  • LinkDatum.id, optional: a link's identity for selection, activation and attention. Parallel links without ids share one identity, select together and are reported once in development.

  • Types: ChartSeriesItemRef, ChartCellItemRef, ChartNodeItemRef, ChartLinkItemRef, ChartSeriesItem, ChartCellItem, ChartNodeItem, ChartLinkItem.

  • selectedItemCount in the menu context now counts the selected drawn items of the standalone families.

Changed (selection and activation, ADR-102)#

  • ChartItemRef and ChartItem are a union of four forms (an item of a series, a cell, a node, a link). The series form is unchanged; code that reads seriesId or key from a ChartItemRef it did not narrow now narrows ('seriesId' in ref, or kind). See the migration notes.

Fixed (selection and activation, ADR-102)#

  • A sankey link's attention was named by its nodes' labels: two nodes with one label shared the attention of their links. It now reads the link's id, else its source and target node ids (T8-X1).

  • CategoricalSlice.index on pie, funnel, radial bar and nightingale was the position among the drawn slices; it is now the row's index in data, as documented (T8-X5).

  • Docs: a selected item carries a caret, not an outline (T8-X2); ADR-042's export sentence is superseded (T8-X3).

Added (compound grouping and orientation, ADR-101, 2026-10-06)#

  • Several series on the four compound wrappers: series on RangeBarChart, RangeAreaChart, BoxPlotChart and DumbbellChart (RangeBarChartSeriesConfig, RangeAreaChartSeriesConfig, BoxPlotChartSeriesConfig, DumbbellChartSeriesConfig), each from the single-series keys or from series, never both (the type refuses the mix; a caller outside the type system is told once in development).

  • Automatic grouping: several visible range bars, boxes or dumbbells of one family, orientation and axis stand side by side in their category, in series order; a hidden one gives its slot back and the rest re-centre. No option. Families never group with each other. Range areas overlay in series order, each at its own opacity.

  • Horizontal box plot: horizontal on BoxPlotSeriesOptions and BoxPlotChart (no navigator or annotations when horizontal, as a horizontal range bar); the same five statistics, roles and words.

  • Several dumbbells: one legend entry per series with a paired key of its two end colours, on screen and in an export; every end is read with its series first ("Plan: 2024"). One dumbbell series is unchanged.

Fixed (compound grouping and orientation, ADR-101)#

  • A box on a reversed value axis ran from Q3 away from Q1; it now spans the two quartiles (T4-X1).

  • Two range bars or boxes in one category drew in one slot, the earlier hidden and not reachable by pointer (T4-X5).

  • Two dumbbell series were announced, tooltipped and tabled by their end names only (T4-X4).

  • Endpoint labels of range bars, range areas and dumbbells could overlap another series' labels; they now join the plot's one collision decision.

  • Docs: the reason given for "dumbbell single-series only" (T4-X2) and the box plot's widthRatio base (the band, not the step; T4-X3).

Added (scalar scales, targets and gauge replay, ADR-100, 2026-10-06)#

  • A tick scale on GaugeChart, LinearGauge and BulletChart: scale (ScalarScaleOption): true for ticks at round values for the scale's length, {ticks: [...]} for exactly these values. The ends are always labelled; a tick label gives way to an end or the target, its tick staying; labels use the readout's notation. Ticks are in no name, table or CSV.

  • One target on all three: target (ScalarTarget) is now on ScalarChartProps, so GaugeChart has one too (a mark across the band, above the needle or the bar), and takes {value, label} to name it ("Budget 1,500"). Beyond the scale its mark stops at the nearer end, every text keeps the true number.

  • GaugeChart indicator: 'needle' (default) or 'bar', a narrower arc inside the band from startAngle to the value; zones, target and readout stay.

  • GaugeChart replay (default off): a hidden click or tap at the pivot and, where the host's menu keeps it (contextMenu.features.replay), Replay animation re-run the entrance to the current value. Ignored while the gauge moves; nothing moves under reduced motion; no value, callback, name or export changes.

  • Forced colours: the scalars' value marks and targets in CanvasText, scale ticks in GrayText.

  • Host keys charts.contextMenu.replayAnimation, charts.contextMenu.replayReducedMotion, charts.contextMenu.replayAnimationOff, charts.scalar.namedTarget, charts.scalar.aboveScale, charts.scalar.belowScale, charts.accessibility.targetPlain, charts.accessibility.namedTarget, charts.accessibility.namedTargetPlain, charts.accessibility.scalarValueAbove, charts.accessibility.scalarValueBelow.

Changed (scalar scales and targets, ADR-100)#

  • target on LinearGaugeProps and BulletChartProps moved to ScalarChartProps and widened from number to ScalarTarget (number | {value, label?}). Code that passes a number is unchanged; code that reads the prop as a number needs a narrowing (MIGRATION-NOTES).

Fixed (scalar scales and targets, ADR-100)#

  • A target of 0 was missing from a scalar's accessible name (T7-X1).

  • A target that is not a finite number was drawn at NaN coordinates and stated as "NaN"; it is now neither drawn nor stated, and reported (T7-X2).

  • A value beyond the scale read "(100%)" or "(0%)" in the accessible name; it now reads "above the range" or "below the range" without a share (T7-X3).

  • "% of target" was stated for a negative target or a value of the other sign; it is stated only where it means something (T7-X4).

  • The documentation said a scalar has no context menu; it has one that exports (T7-X5).

Added (hierarchy and flow, ADR-099, 2026-10-06)#

  • One label policy on TreemapChart and SunburstChart: label (HierarchyLabelOptions): secondary ('share', 'value' in the chart's valueSemantics, 'none'; treemap default 'share', sunburst 'none') and levels (the levels that draw text, counted from the shown root). Visual only; every reader keeps every name and value.

  • Sankey: nodeAlignment ('justify' default, 'source', 'sink', 'center'), linkColor ('source' default, 'target', 'gradient', also in SVG, PNG and PDF) and nodeOrder ('auto' default, 'input').

  • Org chart: collapsible with collapsedIds (controlled), defaultCollapsedIds (uncontrolled) and onCollapsedChange (OrgCollapseChange); a count badge, Enter and Space, and the menu items Collapse, Expand and Expand all below; cardFields (OrgCardField, at most three meta fields per card, default the title).

  • Host keys charts.contextMenu.collapse, charts.contextMenu.expand, charts.contextMenu.expandAllBelow, charts.org.collapsedState, charts.org.expandedState, charts.org.collapsedView.

Fixed (hierarchy and flow, ADR-099)#

  • A treemap with groups started 23 px below the top of its plot: the shown root reserved a header band but draws no header (CH-34). Only a group that draws a header reserves one now; the tiles use the space.

  • A treemap with three or more levels reserved an empty header band on every nested group and drew no header there; every internal node now draws its header (N3). A nested group also had no drawn element behind its keyboard focus.

  • A drilled treemap or sunburst kept drawing the old tree after the host replaced it; the drill path is now held by node identity (N2).

  • The sankey keyboard grouped nodes by their distance from the sources instead of the column they are drawn in, and stepped them in input order; it now walks the drawn columns top to bottom (N1). The data table keeps the order of the links.

Added (quantitative scales, ADR-098, 2026-10-05)#

  • One quantitative colour scale QuantitativeColorScale (LinearColorScale with its own colors, StopsColorScale/ColorStop, ClassesColorScale/ColorClass) and ColorLegendOptions, shared by the heatmap, the treemap and the sunburst.

  • Colour by measure on TreemapChart and SunburstChart: colorMeasure (ColorMeasureOptions: label, scale, valueSemantics, legend) and the node field HierarchyNode.colorValue; a colour key with the measure's name; the measure in the tooltip, a table column, the announcement, the description and the CSV (colorValue).

  • Bubble size: sizeDomain and sizeName on scatter series and ScatterChart; the opt-in size key sizeLegend on ScatterChart and ChartOptions; a size column in the data table.

  • Heatmap axes: xAxis and yAxis (HeatmapAxisConfig: title, formatter, temporalSemantic, order, reversed, wrap, autoRotate).

  • Host keys charts.scale.noValue, charts.scale.measureValue, charts.scale.valueWithMeasure, charts.scale.size, charts.scale.sizeClampedBelow, charts.scale.sizeClampedAbove; tooltip fields colorMeasure, sizeName, sizeMissing.

Changed (quantitative scales, ADR-098) — BREAKING#

  • HeatmapColorScaleOptions, HeatmapLinearColorScaleOptions, HeatmapStopsColorScaleOptions, HeatmapClassesColorScaleOptions, HeatmapColorStop, HeatmapColorClass and GradientLegendOptions are replaced by the neutral names (no alias); HeatmapSeriesOptions is no longer exported.

  • HeatmapChart.colorRange is removed: the colours are the linear scale's colorScale.colors. HeatmapChart.gradientLegend is renamed colorLegend.

  • The heatmap's row gutter and column band are measured (no fixed 80 px): long labels wrap, turn or are cut with their full name, never overflow. With a name, the colour key carries it as a caption.

  • A bubble's size domain is the full data, not the zoomed, reduced or grouped window; a bubble without a finite size is drawn at its marker size (it was drawn at the middle radius).

Fixed (quantitative scales, ADR-098)#

  • A bubble kept changing its radius while the reader zoomed or panned.

  • A NaN or infinite size drew an invalid circle radius.

  • The bubble size never reached the data table.

  • Under forced colours the heatmap's gradient kept its authored hue while every cell collapsed to Canvas.

  • <Chart>'s screen-reader table re-rendered every row on every pointer move (30 moves over 10,000 points: 11.9 s → 0.47 s in the development bundle).

Added (waterfall subtotals, ADR-097, 2026-10-05)#

  • Floating subtotals on the waterfall: a row of kind 'subtotal' runs from the balance at the previous subtotal or total to the current balance and states the net movement since then; a 'total' stands on zero at any position. One role field, kindKey (WaterfallStepKind: 'change', 'subtotal', 'total'), on WaterfallChart and WaterfallSeriesOptions.

  • The label formatter context carries stepKind on a waterfall label.

  • The data table names each waterfall bar's kind in its own column (host key charts.accessibility.stepKind); CSV and copy-as-table carry the raw kindKey column.

  • Development diagnostics for an unknown kind, a summary row whose own value differs from the derived amount, and a non-finite change.

Changed (waterfall kinds, ADR-097) — BREAKING#

  • totalKey and totalIndices are removed (no alias); use kindKey.

  • SingleTooltipData.waterfall.isTotal is replaced by kind; its value and the tooltip's yValue are the amount the bar draws.

  • A change without a value draws no bar (it drew a zero-height bar).

Fixed (waterfall, ADR-097)#

  • The tooltip, the keyboard announcement and the table no longer disagree on a summary's word, and the word no longer depends on whether the row is the last of the zoom window.

  • The crosshair guide of a negative total stands at its balance, not on the zero edge, where its readout already was.

  • A NaN or infinite change no longer removes every later bar.

  • A summary row is a pointer and keyboard stop whatever its own value field holds (hit points stand at each bar's balance), and Copy value copies the drawn amount.

  • On a reversed value axis every waterfall bar is drawn at its values; it was shifted down by its own height.

  • Forced colours paint a subtotal GrayText, like a total.

Added (Cartesian overlays: error bars, soft bounds, zones, ADR-096, 2026-10-05)#

  • Error bars on line, scatter and bar (errorBars: {lowKey, highKey}, ErrorBarOptions; scatter also xLowKey/xHighKey, ScatterErrorBarOptions), on <Chart> and the LineChart, ScatterChart and BarChart series configs. Absolute bound fields; a stem and caps in neutral foreground ink; an end the domain or a log axis cannot show runs open to the plot edge. The bounds join the automatic domain and are read in every tooltip mode, the data table, the keyboard announcement, CSV and copy-as-table. Custom renderers receive errorBounds / xErrorBounds (TooltipErrorBounds).

  • Soft axis bounds (softMin/softMax on numeric axes): the automatic domain reaches at least to them, never narrowing the data.

  • Zones on line and area (zones: {axis?, ranges}, SeriesZones, ZoneRange, LineZoneRange, ZoneBoundary): the paint split by ranges of y or x where the drawn path crosses a boundary; an area's fill split by region; a named zone in tooltip (zone), table and announcement.

  • Host keys charts.errorBar.range, charts.errorBar.lowerOnly, charts.errorBar.upperOnly, charts.errorBar.xValue, charts.tooltip.zone.

  • One internal domain contribution model (DomainContribution).

Changed (Cartesian overlays, ADR-096)#

  • Breaking: the Bar's errorBars.stroke, errorBars.strokeWidth and errorBars.capLength are removed (no alias). See the migration notes.

  • A bar's error bar draws in neutral foreground ink instead of the series colour (its lower half was invisible inside its own bar, EB-3).

  • A series with error bars is never reduced and blocks time grouping (the bar's bounds were summed, EB-4).

  • The BarChart wrapper no longer injects a tooltip valueFormatter; the bounds come from the tooltip data on every route, and a host formatter writes all three numbers instead of silencing the range (EB-2).

Fixed (Cartesian overlays, ADR-096)#

  • CSV and copy-as-table carry the error-bound columns (EB-1).

  • One bound a log axis cannot show no longer erases the valid side (EB-5).

  • An outside bar label and a point label sit beyond their whisker (EB-6, confirmed in Firefox).

Fixed (radial sweep entrance in every engine, ADR-095 amendment, 2026-10-05)#

  • Fixed (sweep entrance, Firefox): the radial sweep of RadialBarChart, GaugeChart, NightingaleChart, SunburstChart and RadarChart turned about a point beside the pivot in Firefox (up to 95 px on a half dial; the rings and roses appeared cut, the dial skewed). The reveal is now an SVG mask in the swept group's own coordinates, about the pivot each family passes explicitly, and measures the same in Chromium and Firefox: on the pivot, from the resolved start, clockwise, the domain's share at every moment of the duration.

  • Changed (internal): the CSS conic-gradient mask, its registered properties --ac-sweep-angle/--ac-sweep-open and the inline --ac-sweep-from/--ac-sweep-share/--ac-sweep-duration/ --ac-sweep-easing are gone (undocumented, read by no consumer); the transient frame circle is replaced by a transient mask. When the reveal ends the mask and the group's mask reference are removed, so a chart at rest carries no animation helper; an export taken mid-sweep leaves both out. Reduced motion draws no mask, as before.

  • No public API changed. Resting geometry and every visual baseline are unchanged; the seven visual--donut-legend-* baselines updated in T2B (callout leaders on the drawn slice's middle) are reviewed and accepted.

Changed (one radial angle convention, ADR-095, 2026-10-05) — BREAKING#

One radial clock: degrees, clockwise from 12 o'clock, a domain as startAngle + endAngle. Default charts draw exactly as before. Migration: migration/MIGRATION-NOTES.md.

  • Breaking (RadialBarChart): startAngle counts from 12 o'clock (was 3 o'clock; default 0, was -90, same rings). maxAngle is removed; endAngle (default startAngle + 360) is where the tracks end. No alias.

  • Breaking (PieChart, NightingaleChart): padAngle is degrees (was radians); the defaults are exactly the former quantities.

  • Changed (GaugeChart): a reversed, empty or full-circle pair draws the default dial and reports a diagnostic (it drew a malformed or empty arc); a startAngle alone keeps the 270° dial.

  • Added: PieChart.startAngle and NightingaleChart.startAngle (default 0; whole chart).

  • Added (diagnostics): an invalid radial angle (not finite, a sweep of 0° or less, more than one turn) is reported once through the host sink (ADR-076); the family default is drawn, and no NaN reaches a path.

  • Fixed (sweep entrance): a partial domain is revealed over the whole duration (it finished at the domain's share of it) and the mask is centred on the pivot (it was centred on the swept group's box, up to 95 px off on a half dial). The Gauge now uses the shared radial entrance.

  • Fixed (Pie): one slice-angle model for the slices, sector labels, callouts, keyboard cue and menu point and the tooltip anchor. The callouts and anchors used a second walk that shared the pad differently from the drawn slices; their leaders now leave the drawn slice's middle exactly (they sat off it by up to half the pad times the slice count: 0.92° on a five-slice allocation with the default pad).

  • Removed (internal): the unexported core/shapes/pie.tsx (Pie), whose only reader now draws from the slice model.

Added (per-datum paint on more families, ADR-094, 2026-10-05)#

Additive public API (ADR-029). Without a resolver every chart draws what it drew.

  • Added (paint on six more families): the BC4 per-datum paint resolver (DatumPaintResolver, fill only) on the lollipop series (the head; the stick follows it unless stickStroke is set), the range-bar series (the interval; context value is the high end), the dumbbell series (each end separately), and on FunnelChart, RadialBarChart and NightingaleChart (the item is its legend category, so its key, the radial name-column key and the export legend show the paint; a funnel key shows the stage as drawn). Tooltip swatch, export, label contrast and forced colours follow, as on bar and pie.

  • Added (DatumPaintContext.role?: ChartValueRole): the part of a mark painted part by part; a dumbbell's resolver is asked once per end with role: 'low' or 'high' and that end's value. The connector keeps the series stroke.

  • Changed (funnel legend): a hidden stage's key keeps its own (painted or palette) colour instead of falling back to the palette.

Fixed (truth corrections, ADR-093 amendment, 2026-10-05)#

  • Fixed (CH-28): under forced colours a rising candle is hollow and a falling one filled, with solid wicks (the rules pointed at test ids that no longer existed), and range bars take the bar series' system colours, their legend keys too.

  • Fixed (CH-29): the crosshair's value readout on a stacked bar or area segment names the stack's sum through the segment, and on a waterfall step its running balance: what the axis reads where the guide stands. On a reversed value axis the tooltip and crosshair anchor stands at the bar's value end (it stood at its start).

  • Fixed (CH-32): sankey node and link DOM ids encode the host id (spaces, punctuation, any script) injectively, so every aria-activedescendant resolves; the host id itself is unchanged.

Fixed (truth and defect corrections, ADR-093, 2026-10-05)#

Corrections of existing promises; one additive field (Sparkline.accessibility). No new capability, no family feature.

  • Fixed (CH-7, zoom follows the scale): drag selection, wheel, pan, pinch and the navigator read pixels through the x scale: a reversed axis zooms the part under the pointer (it zoomed the mirrored part), a drag on an uneven continuous axis selects exactly the rows inside it, and the navigator follows reversed. An xy drag picks the band shown on a log or reversed y axis. Changed meaning (fix): the y fractions of onZoomChange and setZoom are positions on the plot from its bottom edge, read through each y scale; on a linear axis that is not reversed nothing changes, on a log or reversed axis they now name the band shown. The x fractions stay row fractions of the full data.

  • Fixed (CH-8): a pinned tooltip stays on the host row it was pinned on across zoom, pan, reduction, grouping and rerender (it showed the row at the same window position); it clears under the existing policy.

  • Fixed (CH-12): a lone explicit min at or above every value (or max at or below every value) no longer turns the axis around: it is ignored with a diagnostic and the axis spans the data. A non-finite min or max shows the unavailable state with a diagnostic (W12).

  • Fixed (radar): a missing or non-finite value is a gap (no vertex, edge or fill), never 0; the table, the announcement and the keyboard readout say "(no value)". Without min/max the scale follows the visible data (with zero, round ring steps) instead of a fixed 0 to 10 that clamped real values silently; beyond an explicit bound a value keeps its true number, with ≥/≤ on the scale and in the description (W-CF-3). New host keys charts.scale.axisClampedBelow and charts.scale.axisClampedAbove.

  • Fixed (CH-20): RadarChart no longer takes a fixed 30 px of height for a legend above or below; the legend adds its measured rows, as on the other categorical families.

  • Fixed (shared tooltip): a row without a value in <Chart>'s shared tooltip reads "(no value)" instead of a blank.

  • Fixed (CH-24): without stackGroup, a stack beside an unstacked bar takes its own column instead of filling the band under the bar (the column rule of ADR-091 now holds without a declared group).

  • Fixed (CH-25): a stack total or an area value that would print on a bar it does not belong to is hidden; its value stays in the tooltip, table and announcement.

  • Fixed (CH-26): under forced colours bars, histogram bins and waterfall steps take their system colours (the rules targeted rect; the marks are paths), and a bar's legend key takes its bar's colour.

  • Fixed (CH-27, Walt's decision): under forced colours a classed heatmap's legend swatches collapse with the cells, a missing cell keeps no edge, and the class stays a name in the legend, tooltip, table and announcement, with the current cell's class marked in Highlight.

  • Fixed (LAB-AT4): each crosshair guide passes through what its readout names: a horizontal series reads its value on x and its category on y (they were swapped); a compound glyph's value guide crosses the high end, the median or the close its readout names; a histogram bin reads as its interval.

  • Fixed (CH-10): a scalar's accessible name reads its range when min is not 0 ("0 in the range −50 to 50", new host key charts.accessibility.scalarValueInRange), never "0 of 50"; a missing pie, funnel, nightingale or radial-bar value reads "(no value)" in the announcement, value cell and share cell, never 0; two treemap, sunburst or org-chart siblings with one label are two reachable nodes with their own DOM id (the key is the host id or the sibling position, not the label); an empty or blank accessibility.label is no name on any family, <Chart> included, so no chart carries an empty name. Changed (DOM ids): hierarchy item ids now encode the node key (e.g. …-n0.n2), not the label.

  • Fixed (CH-11): treemap, sunburst and org-chart paths isolate each label, join with → (ltr) or ← (rtl) and open with a direction mark, in the tooltip and the table; the treemap breadcrumb separator points to the inline end under rtl (it was mirrored twice).

  • Added (Sparkline.accessibility): accessibility={{label}} (Pick<AccessibilityOptions, 'label'>) makes a sparkline one image with that name; without a name it is decorative and hidden from assistive technology (aria-hidden).

  • Proven (QR-2): OHLC, box plot, range area and range bar name and toggle their series in the legend, and the exported legend draws the screen's key and name (browser, Chromium and Firefox).

Added (per-datum paint and quantitative colour, ADR-092, 2026-10-05)#

Additive public API (ADR-029). Without the new forms and resolvers every chart draws what it drew.

  • Added (HeatmapColorScaleOptions forms): the type is now the union of HeatmapLinearColorScaleOptions (the former shape, unchanged), HeatmapStopsColorScaleOptions (type: 'stops', HeatmapColorStop[]: colours at values, the first and last stop are the ends) and HeatmapClassesColorScaleOptions (type: 'classes', HeatmapColorClass[]: named [from, to) ranges with a class legend; the class is named in the tooltip, the data table, the announcement and the description). A value in no class is drawn neutral and named; a missing value stays empty. Malformed stops and classes, overlaps, values in no class and an unused colorRange are reported.

  • Added (per-datum paint): paint on bar and scatter series (and BarChartSeriesConfig, ScatterChartSeriesConfig), marker.paint on line and area series, and paint on PieChart: a bounded resolver (DatumPaintResolver, DatumPaintContext, DatumFill, DatumPaint) that returns paint only. The mark, its inside-label contrast, its tooltip swatch and its export follow the result; forced colours override it; a series legend keeps the series colour (a pie key follows its slice). Other returned fields, a non-colour and a throwing resolver are reported and fall back to the series paint.

  • Added (tooltip): SingleTooltipData.colorClass, the class row of the default tooltip.

  • Translations: charts.scale.classes, classRange, classFrom, classBelow, outside, valueInClass and charts.tooltip.colorClass, in every available language.

Fixed (label collision per plot, ADR-091 amendment, 2026-10-05)#

  • Fixed (NEW-01): the value labels of several area series and the plot's stack totals take one collision decision: a label no longer prints over a label of another series or over a total. The totals come first, then the areas in series order; a value that gives way stays in the tooltip, the data table, the announcement and the CSV, and an export draws the same kept labels. No API change.

  • Docs: API.md § 3.1 no longer says area, range, dumbbell and lollipop wrappers take no label; the § 10.8 example compiles again.

Added and fixed (Cartesian labels and stacks, ADR-091, 2026-10-04)#

Additive public API (ADR-029). Without the new options every chart draws what it drew.

  • Fixed (CH-23): the export legend draws a line key with the series' dash pattern (explicit lineDash or the automatic second encoding), at the painted key's length; SVG, PNG and PDF. LegendExportItem.dash.

  • Added (stackGroup): on bar and area series (<Chart>; BarChartSeriesConfig, AreaChartSeriesConfig). Implies stacking. Bar groups stand side by side in the category, each with its own baseline, percentage base, tooltip total and stack total; area groups stack from their own baselines.

  • Added (stackTotal, StackTotalOptions, StackTotalFormatterContext): one total per stack and category on bar series (<Chart>; BarChart): the net total of the visible members, beyond the end its sign points to. None on a percentage stack. The data table gains a total column and the keyboard announcement names the total.

  • Added (label roles): label on area and lollipop series and wrappers (PointLabelOptions); label: EndpointLabelOptions (endpoints: 'both' | 'low' | 'high') on range bar, range area and dumbbell series and wrappers. LabelFormatterContext.role names the end. Off by default.

  • Added (wrap): AxisConfig.wrap and AxisOptions.label.wrap on a category axis: names wrap into at most that many lines before they rotate or are cut; the full name stays the <title>. Default 1. On a vertical category axis it caps the existing gutter wrap. TimeAxisConfig, NumberAxisConfig and the upright value axis config do not take it.

  • Changed (only with the new options): an automatic value domain keeps room for a stack total or an outside value label at the end it points to, so the label at the domain's extreme is drawn rather than hidden.

Added and changed (marker shapes and legend reading order, ADR-090, 2026-10-04)#

Additive public API (ADR-029) and three changed defaults that change visual baselines where they apply.

  • Added (MarkerShape): one bounded marker vocabulary, 'circle' | 'square' | 'diamond' | 'triangle' | 'triangle-down', exported from the root. MarkerOptions.shape on line and area markers (<Chart> and the wrappers); marker.shape on scatter and bubble points (ScatterChart series and <Chart> scatter series); markerShape on LollipopChart (chart and per series), DumbbellChart and their <Chart> series; marker.shape on RadarChart series. Every shape has the area of the circle of the same size. The legend key, the tooltip key, the emphasised node, the keyboard cue, every export and forced colours follow it.

  • Changed (TimelineChart.marker): takes the whole vocabulary instead of 'circle' | 'diamond'. An explicit 'diamond' is now the equal-area diamond of the vocabulary: about a quarter wider than the inscribed diamond it was, and the timeline layout reserves its reach.

  • Changed (default, automatic shapes): a point series whose colour does not keep it apart (from the seventh default palette slot, or where a host palette repeats; the trigger of the dash pattern, ADR-038) draws a marker shape. A chart of six or fewer distinctly coloured series is unchanged. An explicit shape always wins.

  • Added (legend.align): 'start' | 'center' | 'end' along the side, logical under rtl; the default is the old alignment (center above or below, start beside). The export legend follows.

  • Changed (default, stack reading): the legend and the shared tooltip of a stacked bar or area list the stack's members in the order the eye meets their segments (the top segment of a vertical stack first). No option. A chart without a stack keeps the series order. Legend entries now carry data-legend-series (the series they toggle) beside data-legend-item (their position).

  • Changed (default, narrow side legend): a legend at left or right stands below the chart where beside it the chart would keep less than 240 px, and returns when the room returns. The export carries it where it is drawn. A side column is as wide as its widest entry; its names no longer wrap (closes CH-16).

  • Fixed: an export handle kept from an earlier render exported the composition of that render; every handle now runs the latest export work, as the context menu already did.

Changed (default label text and pie tooltip rows follow value semantics, ADR-089, 2026-10-04)#

Two default texts now follow the value semantics. No new API.

  • Changed (data labels): a bar, waterfall, line, scatter or histogram label without formatter or format writes the series' value semantics (its own, else its y axis'), in the chart's presentation, as the axis, tooltip and table already did. Without a declaration it stays the plain number; a percentage stack keeps its share labels. Placement and collision are unchanged.

  • Changed (tooltip): the pie tooltip always states the value and the share on two rows; the share is a percentage (61.7%) with or without valueSemantics, never a bare number. tooltip.valueFormatter writes the value row of the pie and funnel tooltips and no longer reaches their share row. CSV and Copy value stay raw.

Added (value semantics and wrapper configuration, ADR-089, 2026-10-04)#

Value meaning on every numeric family and wrapper parity with <Chart>. Additive: no member changes meaning and no consumer migrates. See DECISIONS.md ADR-089 and API.md § 3.1, § 10.6.

  • Added (value semantics): valueSemantics (ChartValueSemantics, the Cartesian type) on PieChart, FunnelChart, NightingaleChart, RadialBarChart, RadarChart, HeatmapChart, TreemapChart, SunburstChart, SankeyChart and ScalarChartProps (GaugeChart, LinearGauge, BulletChart). Labels, scales, readouts, tooltips, data tables and announcements write a value in it; a role's own formatter or format and tooltip.valueFormatter still win; a share stays a percentage; CSV and Copy value stay raw. Without it every text is as before.

  • Added (wrappers): label (PointLabelOptions) on LineChart, ScatterChart and Histogram; visible and showInLegend on the series configs of LineChart, AreaChart, BarChart, ScatterChart and LollipopChart; annotations on every Cartesian wrapper, on BarChart, DumbbellChart, LollipopChart and RangeBarChart only while upright.

  • Changed (types, compatible): horizontal of BarChart and DumbbellChart moved into an orientation union, as on LollipopChart and RangeBarChart; every existing call, a computed boolean included, still compiles.

  • Changed (tooltip): with valueSemantics the pie tooltip states the value and the share on two rows; without it, unchanged.

Changed (unified context menu, ADR-087, ADR-088, 2026-10-04)#

One context menu across the Active product family (CM1–CM6). Breaking type changes, with a migration note in migration/MIGRATION-NOTES.md. See DECISIONS.md ADR-087.

  • Changed (renderer): every chart except the Sparkline renders its menu with the Base UI menu the Active Grid uses (@base-ui/react is a new dependency). The package's own popup, useStandaloneContextMenu, useContextMenuItems and useLongPress are gone.

  • Changed (types): ContextMenuItem and ContextMenuOptions are removed. New: ChartContextMenuOptions, ChartContextMenuItem (the Grid vocabulary: label, action, icon, variant, disabled, disabledReason, shortcut, keepOpen, visible, separatorBefore, children, 'separator'), ChartContextMenuBuilder, ChartContextMenuContext, ChartContextMenuTarget, ChartContextMenuFeatures, ChartLegendItemRef. items is a builder that receives one frozen context per opening.

  • Added: host items first; a legend entry is a target (Hide or Show, Show only this, Show all); Copy value(s) on an element and Copy as table on the chart; disabled items with a reason; features switches the chart's own groups off; trigger: 'button' adds a visible menu button; the scalars have an export-only menu; every family announces the opening (closes CH-5); items are at least 44 px high under a coarse pointer.

  • Removed: Zoom here and Pan here (Reset zoom stays); the translation keys charts.contextMenu.zoomHere and charts.contextMenu.panHere.

  • Added (translation keys): copyValues, hide, show, showOnly, showAll, label, labelFor, notZoomed, allShown, lastVisible, noValue under charts.contextMenu.

  • Added (token): the host role --radius, from which the menu derives its corner radius.

  • Fixed (PDF, ADR-088): a PDF export draws the whole chart: the export clone always carries a viewBox, so a wide chart is scaled into the page instead of clipped on the right.

Changed (handle contract, ADR-086, 2026-10-04)#

  • Changed (type, A4): setZoom exists only on ChartHandle, the handle of <Chart> and the Cartesian wrappers. Every other family (pie, heatmap, the categorical, hierarchical, graph and scalar families, timeline, sparkline) exposes the new base ChartExportHandle (exports and element access), and its runtime handle no longer carries a no-op setZoom. A ChartHandle ref still fits those charts. Supersedes ADR-085 point 4.

  • Docs: the audit report under reviews/ (deleted 2026-10-07) is Prettier-formatted (whitespace only).

Fixed and narrowed (labels, export routes and public options, ADR-085, 2026-10-03)#

The one correction tranche after the independent audit reviews/ACTIVE-CHARTS-COMPREHENSIVE-FINAL-AUDIT-2026-10-03.md (deleted 2026-10-07). A public option works on its family or leaves its type (W10, ADR-082); no new capability. Breaking type changes, each with a migration note in migration/MIGRATION-NOTES.md. See DECISIONS.md ADR-085.

  • Fixed (value truth, NEW-01, NEW-02): GaugeChart, LinearGauge and BulletChart rest on the semantic value in every animation mode. With count-up the readout ended on the value clamped to the scale (150 on 0–100 read 100) and rounded to two decimals (1.234 read 1.23); it now ends on value itself, as with animation: false, reduced motion and zero duration. The geometry still clamps to the scale; readout and accessible name agree.

  • Fixed (CH-18): LinearGauge and BulletChart count up from min on the package ease-out, as the gauge needle (ADR-084): no wait at the start on a scale that does not start at 0.

  • Fixed (export, LAB-AT12, LAB-AT18, CH-17): the context menu runs the handle's composed exports. A menu PNG, SVG or PDF carries the same legend on the same side as a handle export, on <Chart> and on every standalone family; the standalone menu writes its PNG at contextMenu.pngScale and offers CSV. A <Chart> side legend is exported beside the plot. The exported legend uses the screen's entries: a dumbbell's two endpoints, a tile for a shape series, the value key as the name where name is absent.

  • Fixed (legend, LAB-AT24): FunnelChart, RadarChart, RadialBarChart and NightingaleChart draw a left/right legend, isolate an entry on a double click and export their legend.

  • Fixed (AT8): a waterfall, candlestick or OHLC series has a neutral legend key instead of a palette colour that matched no mark.

  • Fixed (tooltip, LAB-AT6): split cards honour tooltip.style and tooltip.valueFormatter; a shared or split row is read at the addressed x (a scatter series without a point there is not listed); a chart of only ranges, box plots or bins shows its single card instead of an empty one.

  • Fixed (crosshair, LAB-AT4): the y readout uses the series value semantics, the notation of the tooltip, instead of two fixed decimals.

  • Fixed (histogram, LAB-AT11): a selected bin stays selected (the selection was pruned unless a row lay exactly on the bin's bound); an explicit yAxis.min/max is honoured and a negative sum or mean reaches below zero; a sum or mean without yKey draws counts and says so.

  • Fixed (motion, LAB-AT10): animation.update.easing reaches bar and histogram updates (the JavaScript transition ignored it); a range area enters with fade by default (its declared draw-x had no band renderer).

  • Fixed (time axis, LAB-AT14): a parentLevel row groups by the unit it names: one "2025" under a year of daily ticks, not one per month.

  • Fixed (marks, LAB-AT15): an inside label longer than its bar or waterfall step along the value direction moves outside (or hides) under fallback: 'auto'; a box plot outlier on the axis extreme is drawn whole.

  • Fixed (radial bar, LAB-AT25, CH-19): the sweep entrance starts where the rings start (it began a quarter turn early); a value past max fills its track and keeps its true number; the name, key and value columns stand from the first frame instead of being revealed by the sweep.

  • Fixed (navigation, LAB-AT20, LAB-AT22): the org chart's wheel zooms without also scrolling the page, and only while its zoom controls show; an image or PDF of a partial org view carries its note in the caption. The timeline pans only as far as its content, and keyboard focus brings its event into view.

  • Fixed (CH-10): closing a context menu returns focus to the chart; the scalars' descriptions no longer promise arrow-key navigation.

  • Fixed (LAB-AT19, LAB-AT23): a sankey node's value sizes the node (it was overwritten by the flow sum); Sparkline delivers a replaced onExportComplete, onExportError or onReady (its memo comparator kept the first); its bare-<svg> root and getContainerElement() returning null stay, now documented on the handle.

  • Changed (LAB-AT7): with several yAxisIds, the yAxis of LineChart, AreaChart and ScatterChart configures the first axis only; a further axis is a plain number axis on the opposite side (title, range and formatter were copied to every axis).

  • Changed (LAB-AT16): ChartHandle.setZoom on a chart without a data-range zoom reports the no-op through the chart diagnostics.

  • Removed from types (no reader): CrosshairOptions.snap; per-series description and tooltip; label.position on line, scatter and histogram series; tooltip.mode other than 'single' on range area, range bar, box plot and histogram; yAxis.autoRotate/parentLevel on an upright value axis; lollipop series[].yAxisId; colors on waterfall, candlestick and OHLC; navigator on dumbbell, waterfall and a horizontal lollipop or range bar; animation.update on dumbbell, lollipop, box plot, range bar and range area; draw-x for a range area; range-area dataGrouping.approximation; histogram selection.itemKey, a category or time x axis and a sum or mean without yKey; the standalone tooltip's mode, snapDistance, pin (and valueFormatter on org chart and timeline) and animation's stagger, update, layers, exit; accessibility.announcements on the standalone families; NodeDatum.group; showLastPoint on a bar sparkline.

  • Docs (LAB-AT13, LAB-AT21): contract text that contradicted the runtime (animation defaults 400 ms and 30 ms, waterfall corner radius, dumbbell and lollipop connector paint, rising candle close ≥ open, MACD without a histogram, radial startAngle, linear gauge sweep, scalar colors, standalone name, hierarchy meta).

Fixed (radial bar scale beside its columns, gauge needle motion, ADR-084, 2026-10-03)#

Two visual corrections found on the chart reference before the audit. No API change. See DECISIONS.md ADR-084 and QUALITY-BUDGETS.md § 7.

  • Fixed: RadialBarChart no longer drops the scale label that falls in its name or value column's band (the 30 at nine o'clock of the reference allocation, which left a bare tick beside the first key). A column starts past the scale label its rows would meet, plus 6 px; a side without a column keeps its labels inside the frame. The rings stay centred in the room that remains.

  • Fixed: GaugeChart's count-up starts at min, not at 0. On a scale that does not start at 0 the needle stood still for up to 40 % of the entrance while the readout counted through values off the scale, then swung late and fast. The needle and the readout move together from the first frame on the package's CSS ease-out (was a steeper cubic) within the unchanged 400 ms; an update moves on from the resting angle; reduced motion draws the final state at once.

  • Tests: e2e/reference-corrections.spec.ts (new, scenarios ref/); baselines vq5-rest-radial-* and fv0-capture-* updated for the radial bar only.

Changed and fixed (legend in a pie export, legend height by side, ADR-083, 2026-10-03)#

Closes ROADMAP LAB-AT3. See API.md § 1, § 3.1, § 5, § 10 and migration/MIGRATION-NOTES.md.

  • Fixed: a PieChart image export (PNG, SVG, PDF) carries its legend on the side it is drawn: the same entries and order, each slice's drawn colour, a hidden slice dimmed, every entry also when the painted legend is folded, no preview. includeLegend now applies to the pie.

  • Added: LegendExportOptions.position takes 'left' and 'right': one column beside the chart, mirrored under rtl. top and bottom are unchanged; <Chart> still exports its legend under the plot.

  • Changed: the default of LegendOptions.maxHeight depends on the side: 25 % of the chart height above or below the chart (unchanged), the chart height beside it (was 25 %). An explicit maxHeight wins on every side.

  • Fixed: the default applies on PieChart, FunnelChart, NightingaleChart, RadarChart and RadialBarChart, which had no default cap; an explicit maxHeight already worked there.

Changed and fixed (API truth on PieChart and HeatmapChart, ADR-082, 2026-10-03)#

Found by the chart reference in UI Lab (ADR-081, LAB2). See API.md § 3.1, § 3.2 and migration/MIGRATION-NOTES.md.

  • Narrowed (types only, no runtime change): the tooltip of PieChart and HeatmapChart no longer accepts mode, snapDistance or pin; their animation no longer accepts stagger, update, layers or exit, and the pie's no longer accepts type. Neither family ever read them.

  • Fixed: PieChart draws its legend on the left and right, in one row with the pie, which takes the width the legend leaves; it drew no legend at all there before. Top and bottom are unchanged.

  • Fixed: a double click on a pie legend entry isolates its slice, as LegendOptions.isolate promises; the pie did not pass it on before.

No public type changes. See API.md § 2 (Automatic value domains) and § 10.4.

  • Pie and donut callouts stay inside the chart. The centred pie now gives each side room for the wider callout column; a long name on one side (for example "Government bonds" beside "Equity" at 438 px) no longer runs past the chart edge. Charts with callouts of unequal width draw their pie a few pixels smaller at narrow widths.

  • A box plot's outliers are inside its value domain. outliersKey now spans the automatic domain, so an outlier beyond the whiskers is drawn instead of being clipped away while the tooltip names it.

Fixed (label contrast, radial rows and heatmap scale side, ADR-079, 2026-10-02)#

No public type changes. See API.md § 1 (Text on marks), § 10 and § 10.5.

  • Label contrast on real paint (N1). Inside labels and label-safe surfaces now measure the colour the browser paints in the chart's scope: tokens inside color-mix() or other functions, alias chains of any depth, and translucent paint composed over the chart's background. A colour the browser cannot resolve is unknown paint and no longer counts as readable: the label goes outside or is hidden. Affects bar, waterfall, pie and donut sector labels, funnel, treemap, sunburst and heatmap labels.

  • Label background (N2). A bar or waterfall label background is a surface only through its fill; a border-only background no longer lets an unreadable inside label through.

  • Heatmap scale at extreme magnitudes (N3). Positions and the repaired centre stay finite for every finite domain (-1e308 … 1e308).

  • Radial Bar name rows (N4, revised 2026-10-03). Name and value rows never overprint: closer than a text line they are thinned at one regular step, outermost first, as Highcharts and AG Charts thin a crowded category axis; the rings never change for it. The value column is left out before any ring is. Each drawn name carries a small key in its ring's colour.

  • Radial Bar pointer. Moving across the rings no longer returns the chart to rest in the gaps between them: each ring owns its band to the middle of the gaps (an unpainted hit band).

  • Tooltip beside grouped bars. The card is placed beside the category's group, so neighbouring bars keep one side; it changes side only where the group's side cannot hold it.

  • Radial Bar colour keys stand in one straight column between the names and the rings.

  • Radial Bar tooltip under the pointer stands beside the pointer, towards the centre first, never over the addressed ring, and keeps its side from ring to ring; it no longer jumps to the quadrant of each ring's arc.

  • Heatmap tooltip keeps its side from cell to cell while that side fits, instead of taking the first side that fits each cell. In a heatmap too short for the card to stay above or below its cells through a vertical move, the card stands beside the cell first (right, then left), so moving down a column no longer flips it across the pointer.

  • gradientLegend.position: 'right' (N5). Now honoured: a vertical scale beside the matrix, which gives up the width; exported in place. 'bottom' (default) unchanged.

  • Tests: the four W12 browser cases assert the host diagnostics sink (ADR-076) instead of console text.

Added (configuration: bar width, label fallback, pie roles, heatmap scale, ADR-078, 2026-10-02)#

Additive public options, decided by Walt (W-CF-1 … W-CF-3); no existing member changes meaning. See API.md § 10.2–10.5.

  • Bar sizing. widthRatio (share of the category step, 0.1 … 1) and maxWidth (CSS px; Infinity allowed) on BarSeriesOptions, WaterfallSeriesOptions, RangeBarSeriesOptions and the BarChart, WaterfallChart and RangeBarChart props (BarSizingOptions). Bars shrink, never overflow; the bars of one category are sized as one centred group; the first declaring bar series decides for the group, and a disagreement is reported once.

  • Label fallback. LabelOptions.fallback: 'auto' | 'hide' (default 'auto'): with position as the preferred place, 'auto' keeps the family's alternative and 'hide' keeps the preferred place only.

  • Formatter context. LabelOptions.formatter(value, context?) receives {datum, seriesId, seriesName} (LabelFormatterContext) on bar, waterfall, line and scatter labels. One-argument formatters are unchanged.

  • Pie and donut roles. dataLabels (the callout) takes name, value and minAngle beside percentage (PieCalloutLabelOptions); new sectorLabels puts one line inside each slice (PieSectorLabelOptions: percentage by default, value, name, formatter, format, minAngle). A sector label shows where it fits and reads, else it is hidden; with sector labels a slice is drawn on its label surface and the legend swatch follows (W-FV-3).

  • Heatmap colour scale. colorScale: {type: 'sequential' | 'diverging', min, max, mid} and a three-colour colorRange for a diverging scale (HeatmapColorScaleOptions). Values beyond a fixed end take its colour, keep their true value everywhere, and the scale reads ≤ / ≥; the chart's description says so (four charts.scale.* keys in all five languages). An invalid scale is reported once and drawn safely.

  • Heatmap recede. HeatmapChart highlight={{recede: true}} lets the other cells step back to Active's near context step while a cell is addressed (W-CF-2, an amendment of W-FV-4; default off, no strength, never under forced colours or in an export).

Changed (configuration, ADR-078, 2026-10-02)#

  • A waterfall label honours position: inside draws it on its step where it reads; the default stays above the step.

  • Stacked segment labels are inside. A stacked bar with labels and no position drew each label beyond its segment, on the next segment; it is now inside where it reads, and an explicit 'outside' is reported once.

  • A bar label inside an unreadable bar that would leave the plot outside is hidden instead of clipped; a range bar on an axis with under 4 px per category draws one pixel instead of nothing; a waterfall step is never narrower than one pixel.

Changed — BREAKING (public contract and missing data, ADR-077, 2026-10-02)#

Public types narrowed to what the charts draw (Walt, W-QC-1 … W-QC-3). No consumer in the repository used a removed member; see migration/MIGRATION-NOTES.md.

  • <Chart> series are Cartesian. SeriesOptions (the type of ChartOptions.series) is the thirteen series types <Chart> renders; PieSeriesOptions and HeatmapSeriesOptions are no longer members (they typechecked and then drew nothing). Use PieChart and HeatmapChart.

  • Markers. MarkerOptions.shape is removed (every marker was a circle). A scatter or bubble series' marker is Pick<MarkerOptions, 'size'> (enabled, visible, fill and stroke were ignored there; the point's paint is the series fill and stroke).

  • Scalar readout. dataLabels on GaugeChart, LinearGauge and BulletChart is Pick<DataLabelOptions, 'formatter' | 'format' | 'background'>: the readout is always drawn (FV4), so false, enabled, fontSize, color and percentage are gone. FunnelChart and RadialBarChart no longer accept dataLabels.fontSize or dataLabels.color, which they ignored.

Fixed (public contract and missing data, ADR-077, 2026-10-02)#

  • Heatmap: a missing value is not 0. A cell whose value is null, undefined, NaN or infinite is drawn empty (no colour, no label), shows no tooltip, reads "(no value)" to the keyboard and in the data table, and never enters the colour scale's extent (a NaN used to give every cell a NaN position). 0 is a value.

  • OrgChart: two people with the same name are two cards. Pointer, keyboard, tooltip anchor and reporting lines address a card by its id path; hovering the second of two equal names at one depth made both current and anchored the tooltip to the first.

  • Line and scatter labels honour label.format through the shared number resolver (.25 with '.0%' is 25%); measurement and drawing read the same text.

  • Horizontal bar outside labels sit beyond the value end: left of a negative bar, and on the correct side on a reversed value axis (also vertical); the edge check that moves a label inside reads that side.

  • Stale JSDoc and API prose: marker visible defaults to 'hover', highlightStyle.dimOpacity defaults to the far context step (0.64 light, 0.70 dark), the heatmap 'auto' label colour is one colour, a selected item carries a caret beside it (no outline).

Added (diagnostics family parity, ADR-076, 2026-10-02)#

  • Diagnostics go to a host sink. <ChartDefaults diagnostics> takes a ChartDiagnosticSink ({level: 'debug' | 'warn' | 'error', source, message, data?}, a caught error under data.error); nearest provider wins, a provider without one inherits. Without one, warn and error go to the console and debug is dropped. New public types ChartDiagnostic, ChartDiagnosticLevel, ChartDiagnosticSink; the same contract as active-grid's GridDefaults diagnostics.

Changed (diagnostics family parity, ADR-076, 2026-10-02)#

  • Development diagnostics are reported outside production only and once per ground; four producers that ran in production (data validation, format strings, axis formatter, unreadable ticks) no longer do, and none repeats on a re-render or recompute.

  • A contract problem the chart handles itself is warn, no longer console.error; only a failed context-menu export without onExportError is error ('Export failed', data: {error, context: format}), in every environment.

  • Messages name structure and never host data: invalid sankey, hierarchy and scalar input report positions, depths and the violated rule instead of node ids, labels and values; a repeated category reports the field and the rows; a series is named by its identity, not its display name. The [active-charts] prefix is replaced by the source ([ActiveCharts], [ActiveCharts.BarChart], …).

  • Empty data no longer produces a validation warning.

Changed (attention on Cartesian layout, intervals and financial charts, ADR-075, 2026-10-02)#

  • No public API change. Bars and their kin speak the calm attention grammar (class A, W-FV-2, ADR-075): bar, grouped, stacked, mixed-sign and horizontal bars, histogram, range bar, waterfall, box plot, candlestick, OHLC and the bars of a combination. The hovered or focused item holds its colour; the other series at its category (a grouped neighbour, the other segments of its stack) step back a little (light 0.82, dark 0.85); everything else steps back to one moderate tier (0.64 / 0.70) instead of the old 0.35 / 0.30. A shared tooltip holds its whole category. Value labels never change on hover.

  • One label colour. A value label asked inside a bar uses the chart's foreground and stays inside only where that reads (4.5:1); otherwise it stands outside the bar (a stack segment drops it). No bar picks black or white text of its own any more.

  • The tooltip sits beside the bar or its category, never over it, its value label or its category label.

  • Richer tooltips: a histogram bin as an interval with its boundary convention and its count, sum or mean; a range bar with its length; a waterfall step with starting balance, change and resulting balance, and a total named as a subtotal or the total; a box plot with its outliers; a stacked segment with its stack's total; a shared tooltip over two value axes grouped per axis. In a shared tooltip each row now shows its own series' key shape, and only the hovered series' row is emphasised.

  • Keyboard and pointer cues: a histogram bin is named by its interval on the axis; candles and OHLC bars get a time crosshair that leaves the glyph clear (quiet for the pointer, the focus colour for the keyboard), their rising and falling colours never change; a box plot's outliers each get a marker and a value tick under the keyboard.

  • Waterfall bridges are solid structure (1.5 px, the muted foreground), also into a subtotal or total, and never step back with a step.

  • Horizontal bars wrap long category names in a gutter of at most 40 % of the chart's width before cutting them.

  • Compact axis numbers are a trade, not a floor (N6): only the axes that need it switch, and a wider chart never draws a narrower plot.

  • A zero-length range bar is drawn as one pixel at its value.

Changed (attention on Cartesian marks and connections, ADR-074, 2026-10-01)#

  • No public API change. Lines, areas and points speak the calm attention grammar (W-FV-2, ADR-074). In single tooltip mode, and while a legend entry previews a series, the other series of a line, area, range area, scatter, bubble, dumbbell or lollipop chart step back to one moderate tier (light 0.64, dark 0.70) instead of nearly disappearing; a series' own points never step back; a shared or split tooltip dims nothing. Bar and other shape series keep their deeper step until FV7.

  • A point keeps its size. The hovered or focused scatter point, bubble, dumbbell end or lollipop head is drawn again on top at full strength and at exactly its own radius, with no halo; the bubble's tooltip sits beside the circle. A line's 1 px emphasis no longer animates.

  • One opacity per scatter point: a point is drawn at its fillOpacity once (it was that opacity twice, 0.49 for the default 0.7).

  • Stacked areas are flat bands by default (gradient defaults to false for a stacked area, at the gradient's strongest opacity, 0.4); an unstacked area keeps its gradient.

  • Dumbbell connectors read as data: the muted foreground at 0.8 (3.2:1 light, 5.2:1 dark), 2 px, unless the host sets connectorStroke.

  • Several lollipop series stand side by side within a category instead of on top of each other.

  • A range area is marked at both ends of its band under the pointer and the keyboard, never at its middle.

  • A bubble at the edge of its domain stays inside the chart: the layout keeps the largest marker's radius free on every side (the domain is unchanged).

Changed (attention and labels on trees, flows and timelines, ADR-073, 2026-10-01)#

  • No public API change. Trees, flows and events speak the same calm attention grammar and label language (W-FV-2, W-FV-3, ADR-073). Treemap and sunburst: every tile, arc and header label is the mode's foreground colour in light and in dark (no black-and-white patchwork per branch); tiles, arcs and header strips are drawn just far enough towards the background for it, one factor per branch colour, so the branch stays recognisable. A sibling of the addressed item steps back a little, an unrelated branch more (light 0.82 and 0.66, dark 0.85 and 0.70); the sankey keeps one-hop adjacency and steps the rest back to the far tier.

  • Names wrap before they are cut. Treemap tiles, sankey node labels and org cards take a name over at most two lines, breaking only where a line may break, and cut it once at the end of its last line; a sunburst label is a straight line measured to fit inside its arc (one or two lines), never by the arc's length. Every sankey node keeps a label, also a very small one; the labels of a column make room for each other.

  • Org chart. Cards rest on the card surface with a name, then the role; the reporting lines are visible in both modes and restored to full strength around the hovered or focused card, which takes a subtle tint measured so its texts keep 4.5:1. A tree that cannot fit at its readable scale starts on its root and says so ("Partly shown: drag or zoom to see the rest") in a strip under the plot, beside the zoom controls, and in its description. The tooltip names the person, the role and whom they report to, with no number.

  • Timeline. Labels and dates sit in collision-free lanes above or below their marker, the first and last moved inward so they never leave the frame; a crowded lane cuts a label to the room it gets. The current event no longer grows or turns bold: its connector is drawn solid. The keyboard is located by a mark beside the event's label. The tooltip shows the date, the event and the host's description, with no index number.

  • Two translation keys in every language: charts.org.partialView, charts.org.reportsTo.

Changed (heatmap and scalars at rest and under attention, ADR-072, 2026-10-01)#

  • No public API change. The heatmap is quantitative colour, so nothing in it changes under attention (W-FV-4, ADR-072): no cell brightens or recedes, the addressed one included; quiet marks in the row and column header gutters locate the pointer's cell, focus-colour marks the keyboard's, and the marker on the colour scale its value. The source that acted last owns the one painted cell (a key press after a hover now names the keyboard). The pointer tooltip sits above or below the cell, leaving its row and column headers visible.

  • The heatmap's numeric colour scale is shown by default (W-EC-3), inside the chart and its export, with its low end, middle and high end in the cells' notation; gradientLegend={false} hides it. Heatmap numbers use the shared chart notation (whole numbers without .00).

  • Gauge fits its swept arc (N2): radius and centre follow from the actual startAngle/endAngle sweep, the end labels and the readout; below a useful size it keeps the value and says it has no room.

  • Linear gauge and bullet name both domain ends and the target at rest (W-EC-5); the value has its own row and is never drawn where the maximum would be. Gauge, linear gauge and bullet draw every number in the value's notation (N11).

  • Scalar keyboard focus shows a tick at the indicator (bullet: the measure's end) and a mark beside the readout; never an outline.

  • Two translation keys in every language: charts.scalar.target, charts.scalar.insufficientSpace.

Changed (attention and labels on radial and categorical charts, ADR-071, 2026-10-01)#

  • No public API change. One calm attention grammar and one label language for the radial and categorical families (W-FV-2, W-FV-3, ADR-071). Pie, donut, nightingale, radial bar and funnel: the addressed part keeps its own colour, size and place (the pie and donut slice no longer moves out) and every other part steps back to one moderate tier (light 0.64, dark 0.70, derived so both modes step back equally far; dark steps back less than before). Radar: a vertex no longer grows; under the pointer the other series step back to that tier, under the keyboard nothing steps back and the tooltip lists every series at that dimension; the keyboard is located by a mark beside the dimension's label. No text changes colour, weight or size on hover or focus.

  • Nightingale, radial bar and funnel rest at full colour (they rested at 80–85 % opacity) and take their colours from the ten-colour palette (they repeated after five, so categories six and up change colour); colours follow each category, so hiding one recolours no other.

  • Radial bar draws every category or says it cannot. Rings are fitted to the radius and the number of categories, thinning from barThickness (now the preferred thickness) to a floor; where not every ring fits, the chart shows "Too small to show all N items" (translated) and states it in its description instead of leaving rings out. An angular scale is drawn at rest outside the rings, with its origin labelled.

  • Radar ring values are drawn at rest beside the first spoke, and the web is centred with slightly more room.

  • Funnel labels in one colour. Inside labels are the mode's foreground on a stage drawn towards the background just as far as needed for 4.5:1 (the legend swatch shows the stage as drawn); where a colour would need more than half, the label goes outside. Percentages are shares of the first stage, which a caption under the stages names, and the tooltip adds a row with that share.

  • Nightingale fits its frame. The rose is sized from its measured labels; on a narrow chart the labels are shortened, then hidden, before the rose shrinks below half the frame. Values use the shared notation in pie, nightingale, radial bar and funnel.

  • Tooltips sit beside the addressed slice, wedge, ring, vertex or stage, away from the centre, its label and a donut's centre metric, wherever a side fits.

  • New translation keys charts.insufficientSpace, charts.funnel.percentOf and charts.funnel.shareOf (every available language).

Changed (cues instead of rings, ADR-070, 2026-10-01)#

  • No public API change. Cues instead of rings (W-FV-1, ADR-070). In normal light and dark rendering no family draws a border, outline, ring, contour, halo or bracket around a data mark any more, for pointer hover, keyboard focus or persistent selection. Keyboard focus is located by a short mark in the focus colour (3:1 or more against the background) that points at the item or sits beside its identity: in <Chart> on the category axis (a stacked segment also at its level, an interval at both ends, on the value axis; a sample with a solid marker, a quiet guide and an axis cue, a scatter point on both axes); a radial or side tick just outside a slice, wedge, radial bar end, spoke, sunburst arc, funnel stage or timeline event; a mark beside a treemap tile's label or group header, under a sankey node's label (a flow: under both endpoint labels), before an org card's name. The heatmap draws nothing around the current cell and changes nothing else: only that cell turns a touch brighter in its own hue, the colour scale marks its value, and the keyboard adds a cue in each header gutter; no text changes. Values shown in the cells (opt-in) now take one label colour, the foreground, instead of black or white per cell, and the cell scale is drawn towards the background as far as that colour needs for 4.5:1 on every cell (the colour legend shows the same range). A selected <Chart> item carries a filled caret beside it instead of an outline or ring. The current org card is tinted on its surface instead of edged in its colour. Under forced colours every cue is Highlight and the current mark keeps a Highlight edge; the chart root keeps its control focus frame. The root attribute data-ac-locator is now data-ac-cue (package-private).

Changed (branch context in hierarchy and flow, ADR-069, 2026-10-01)#

  • No public API change. Treemap, sunburst and sankey: hierarchy and flow attention (ADR-069). Pointer hover draws no foreground edge on a tile, arc or node any more; the addressed item keeps full strength while its context follows the branch: a sibling steps back a little, an unrelated branch more, ancestors and descendants stay. The sunburst no longer dims by ring depth. A treemap leaf directly under the root draws no group background or header strip (the false band in a receded root leaf is gone). A keyboard-focused sankey flow keeps its colour and width and gets a link locator at both ends of its band; a thin sunburst arc gets a short radial locator. The tooltip of these three families sits beside the addressed tile, arc, node or flow when a side fits.

Changed (attention core, ADR-068, 2026-10-01)#

  • No public API change. Chart entry and datum focus are two stages: while the keyboard is on the chart itself, its 2 px focus ring is the indicator; once a datum carries the keyboard locator, the chart ring becomes one quiet 1 px line, so the datum locator is the single dominant ring (ADR-068). Pointer and keyboard are two sources of attention in every standalone family: a pointer that leaves the plot hands the attention back to the keyboard's item instead of clearing it.

  • Internal: a shared attention core, a closed vocabulary of relation roles, context policies and locator kinds, family sections in interaction.css without added specificity, new locator kinds in the transient layer and an anchor option for the tooltip positioner. New transient root and item attributes (data-ac-origin, data-ac-target-kind, data-ac-locator, data-ac-relation) never reach an export.

Added (family visual evidence, ADR-067, 2026-10-01)#

  • No runtime change and no public API change. The family visual target is now measured in the browser (e2e/visual-family-target.spec.ts, ADR-067): encoding, isolation, relation, focus, tooltip-anchor and context-at-rest probes over every family, with a ledger of known gaps owned by the phases FV1–FV8, and a change-detecting capture matrix. The closeout ledger is re-keyed to the same phases. Source comments that cited the deleted visual designs now cite ADRs, ARCHITECTURE and QUALITY-BUDGETS (comment-only).

Changed (tooltip containment and keyboard focus shape, ADR-066, 2026-10-01)#

  • A tooltip is as wide as its content needs, up to four fifths of the chart: names wrap only at that cap, and every value stays inside the card's padding. The cap now resolves against the chart (it resolved against the tooltip itself, so a value could stand outside its card). A split tooltip keeps its fixed column and wraps a long name in it instead of cutting it, inside the chart.

  • A tooltip that fits the chart stays inside it: when neither side of the pointer has room, it shifts inside the chart instead of leaving it.

  • One keyboard ring for every family, without an outline: the host's ring colour drawn a quarter towards the foreground, so the ring itself keeps 3:1 against the background in both modes. The Cartesian ring loses its foreground edge; the standalone families draw it on the focused shape (a sankey node's rect, an org-chart card, a timeline marker), never on a group, so a focused label is no longer drawn thick or blurred. No glow.

Fixed (tooltip containment and keyboard focus shape, ADR-066, 2026-10-01)#

  • A standalone chart's tooltip stayed hidden until the pointer moved again, and never showed for the first keyboard-focused item.

  • Under forced colours, a focused pie, funnel or nightingale item lost its ring to the system background; it now takes Highlight.

  • An export of a chart with keyboard focus carried the focused item (data-focused) and the root's aria-activedescendant; an export shows the chart at rest.

Changed (interaction states across every family, ADR-065, 2026-10-01)#

  • A narrow axis degrades in one order: a plain number axis (auto-formatted, no declared form or kind: 'number') first turns compact ("48 bn") when its labels would squeeze the plot below its minimum; only then does a title give way; a time axis whose scale knows no coarser set thins by stride rather than overlap its labels; and a category label is cut only while it stays recognisable (four graphemes, or its whole first word: "Jan …"), otherwise the axis shows fewer whole labels. An outer date label of a truncating axis takes its shorter form ("1 Jul") before it is cut.

  • A tooltip is at most four fifths of the chart's width; a long name wraps, a value never does.

  • Exports and prints show the chart at rest, selection included: selection marks are interaction state and no longer reach an SVG, PNG, PDF or print (W-VQ-6). The hover emphasis of an org-chart card or a timeline event comes from the stylesheet and is never exported.

  • Legend spacing in ems of the legend font (identical at the default size).

Changed (standalone families on the shared state model, ADR-065, 2026-10-01)#

  • Standalone families on the shared interaction state. Donut and pie (the current slice moves out, its siblings step back), funnel, nightingale and radial bar (current item full strength, siblings step back), radar (series emphasis, a growing vertex, the other series inactive), treemap (a foreground edge, the other leaves step back), sunburst (the arcs of the current ring step back) and sankey (a node keeps its links and their far ends, a link its two endpoints; the rest of the flow steps back). No standalone renderer paints a dim or an offset of its own.

  • Legend preview on the donut, pie, funnel, nightingale and radial bar (the item) and the radar (the series), by pointer and keyboard focus.

  • Heatmap hover and focus: an outline (a background halo and a foreground edge) over the current cell; no other cell changes colour. The keyboard ring sits on the drawn cell.

  • Radar keyboard ring on the data vertex (it sat on the tip of the axis); vertices are drawn above every polygon, so a vertex inside a later series' polygon can be hovered. The axis labels are measured before the radius is fixed.

  • Treemap and sunburst tints are opaque tints of the group colour, tuned per colour mode: a dark surface keeps their hue instead of turning muddy.

  • Sankey labels: sources outside on the left, sinks outside on the right, middle columns in the gap; the side columns are measured from the labels. labelPadding now defaults to the measured width (it was 90 px).

  • Timeline lane labels are measured and no longer cut at the chart edge.

  • Entrance animations release their final state (fill mode backwards), so state paint applies after the entrance.

Fixed (interaction-state coverage, ADR-065, 2026-10-01)#

  • Shared and split tooltips on bars: the hovered category now stands out in every series (its bars full strength, the other categories step back); before, a grouped, stacked or mixed-sign bar chart with a shared tooltip showed no hover at all. No series is dimmed.

  • Histogram: the keyboard reaches every bin, and the tooltip shows the bin's value (it showed the bin's start).

Changed (Cartesian geometry standards, 2026-09-30)#

  • Bars round their value end by default: 3 px (cornerRadius still sets a series' own, 0 rounds nothing), clamped to a quarter of the bar's width, on the value end only — the top of a positive column, the bottom of a negative one, the inline end of a horizontal bar. In a stack only the outermost segment of each sign rounds. Negative bars used to round their base.

  • Bars are at most 64 px wide. A chart with few categories draws deliberate columns instead of slabs; grouped bars narrow as a group and stay together and centred. The hit targets, focus ring and selection outline follow the drawn bar.

  • Crisp edges. Bar and waterfall edges sit on whole pixels (stack segments meet without a hairline); a candle's 1 px wick sits on a half pixel with an odd body centred on it, and candle and OHLC strokes render crisp.

  • The waterfall joins the bar family: the same value-end radius, width cap and crisp edges; its connectors run edge to edge.

  • Stroke hierarchy: a box plot's whiskers and caps are one step lighter than its box, the median stays the strongest; a lollipop stick is drawn one tier below its marker (stroke opacity 0.55 unless the series sets its own stick colour).

Changed (tooltip, typography and chrome, 2026-09-30)#

  • One surface family. The tooltip (every mode), the context menu and the crosshair value chips share the popover surface, a 1 px border and a per-mode elevation from the package stylesheet: a soft shadow in light, a deeper one in dark. The context menu lost its blur; the crosshair chips are no longer foreground-filled pills. A tooltip style.boxShadow still wins.

  • Tooltip hierarchy. Values are drawn one weight step above names (500 and 400), with tabular numerals at the inline end; in shared mode the row of the hovered series names it in the value weight. The key of a row has the shape of the legend key for its series type (a bar a rounded square, a line a stroke with its dash, a marker a circle); a candle or OHLC bar keys its up or down colour and a waterfall step its increase, decrease or total colour.

  • Value-end anchor. A bar, histogram bin, waterfall step, range bar, box, candle or OHLC bar opens its tooltip (and its crosshair) at its value end instead of its middle.

  • No blink across bar gaps. A pointer hover outlives a miss by 100 ms; the keyboard still ends its hover at once.

  • Typography tiers. Data labels are drawn in the value weight (500); the empty-state text is the metadata tier (11 px, muted). No size floor moves.

  • Grid. Grid lines run perpendicular to the value axis only: a category axis draws no grid by default, and a time axis only for a line, area or range-area series. The grid is a little stronger and tuned per mode, and a zero line one step stronger marks zero when the domain crosses it. gridLine.enabled still decides where it is set.

  • The legend and tooltip keys of range area (a stroke) and of OHLC, box plot, range bar and candlestick (a square) now follow their series family.

Changed (interaction state model, ADR-064, 2026-09-30)#

  • One interaction-state model for every Cartesian family (ADR-064). The chart computes the state once (rest, emphasis, recede, inactive, focus) and writes it as data-* attributes; the package stylesheet paints it. No series renderer applies a dim opacity of its own any more.

  • Bars and other shapes answer a hover with their own geometry. The hovered bar, histogram bin, waterfall step, range bar, box, candle or OHLC bar keeps full strength and its siblings recede; other series go inactive. The dot that used to sit in the middle of a bar is gone.

  • Markers get an emphasised node: the hovered scatter, bubble, dumbbell or lollipop mark is drawn again on top, slightly larger, with a background halo; a lone marker series recedes behind it.

  • A shared or split tooltip dims no series: every series the tooltip lists gets a node at the hovered row.

  • Keyboard focus has its own ring around the current bar body or node: the --ring colour between a background halo and a foreground edge (3:1 or more against the background), shown whether or not anything dims and with the tooltip off.

  • Legend preview: hovering or focusing a legend item of <Chart> emphasises its series and fades the others. It never toggles and never changes aria-pressed; click and double-click are unchanged.

  • The recede and inactive strengths are tuned per colour mode from contrast floors (2:1 and 1.4:1 for every categorical colour on background and card; QUALITY-BUDGETS § 12). highlightStyle.dimOpacity still sets a series' own inactive strength.

Fixed (interaction state model, ADR-064, 2026-09-30)#

  • Exports taken while hovering, focusing or previewing a legend item carried the transient state (dimmed series, crosshairs, the hover dot). An export now shows the chart at rest, byte for byte; print does the same.

  • The zoom animation of <Chart> ignored prefers-reduced-motion.

  • The tooltip fade ignored reduced motion wherever the tooltip was mounted outside a chart scope; the fade is now a stylesheet rule the reduced-motion rule always reaches.

  • A hover no longer re-resolves or re-measures the axis ticks (CH-13), and moving within one item re-renders nothing.

  • The forced-colours rule for the hover dot targeted a circle inside the dot and never applied; the node now takes the system highlight colour.

Changed (axis layout, ADR-063, 2026-09-30)#

  • Axes are laid out from measured extents (ADR-063). One internal function stacks tick, gap, measured labels, the parent row and — only when it is drawn — the title, and both the margin reservation and the axis placement read it. An axis title now keeps one gap (0.75 em of the tick font) outside its widest label: it moves in for short labels and out for wide currency or percentage labels, on left and right axes alike. Visible effect: margins and title positions shift, an unrotated x axis takes about 24 px instead of a fixed 40 px, and screenshots of Cartesian charts differ.

  • Rotated x labels reserve the row they draw. The resolved rotation feeds the bottom (or top) extent; a resolved 0° no longer reserves the old 88 px. A truncating axis caps a rotated row at 70 px and cuts longer labels there (full text in <title>); rotated labels are no longer cut to the tick spacing. An axis with label.truncate: false reserves its full label length.

  • A title that is not drawn reserves nothing, on every side (the vertical axes previously kept 16 px under hideAxisTitles). When the measured axes would squeeze the plot below 120 × 80 px, the titles of that dimension are left out before any label.

  • First and last glyphs on an ordinal-time axis (candlestick, OHLC, box) keep the same gap to the plot edge as two neighbours: the axis pads 0.7 step instead of 0.5 when it carries glyphs.

Fixed (axis layout, ADR-063, 2026-09-30)#

  • Axis titles overlapping or crowding tick labels (for example −12,5 %, € 1.234.567 or a dual NAV axis), including under dir="rtl".

  • Dumbbell and lollipop markers at the value extremes were cut by the plot clip; they are now drawn whole (the domain is unchanged).

  • The forced-colours and print stylesheets targeted grid-line test ids that no longer existed, so grid lines kept their screen colours in Windows High Contrast and in print.

Fixed (re-convergence, 2026-09-30)#

  • A time x axis honours min and max. The domain was built from the data alone; explicit bounds (epoch milliseconds) now set it, as on a number axis.

  • A chart without rows rejects exportAsCsv with EXPORT_NO_DATA. <Chart> with data={[]} and a standalone family with fixed columns (the timeline) wrote a header-only file and reported success.

  • onLegendToggle fires once per toggle. It ran inside a state updater, which React may run twice (always under StrictMode).

  • Sunburst ring-one labels are fitted in their painted weight (600). They were measured at the inherited weight and could overflow their arc.

  • Categorical item names use the formatted value (pie, funnel, nightingale, radial bar, radar), like the announcement already did.

  • A grouped bucket of ISO-string x values stays selected. The selection existence check read only Date and number x values, so an uncontrolled selection dropped the bucket at once.

  • Indicators treat NaN and Infinity as missing, like null; one non-finite source value no longer hides every later EMA and MACD value.

  • PeriodSelector windows clamp to the end of a shorter month: 31 March minus one month starts on 29 February, not 2 March.

Changed (packaged-artifact proof, ADR-061)#

  • date-fns and @date-fns/tz are declared dependencies. The inlined formatter closure imports them; an install outside the workspace reads only this manifest. No runtime change.

  • Documentation. README Quick Start passes a presentation and translator, the export paragraph describes ChartExportFonts instead of the removed base64 font, and API § 4 states that a host without jspdf/svg2pdf.js declares them external to its bundler.

Documentation (export guarantee tiers, ADR-060)#

  • Export guarantees. docs/EXPORT-GUARANTEES.md states the tiers L0 to L3b-print per format and script, the route to take, the error kinds, a host font example and the host L3b recipes. No runtime change.

Added (export font sets in SVG and PNG, ADR-059)#

  • Export fonts in SVG and PNG. The same ChartExportFonts as PDF, with the same precedence (the call's fonts, else ChartDefaults.exportFonts, else none; 'inherit' skips the defaults), on exportAsPng/exportAsSvg of the handle, on PngExportOptions/SvgExportOptions, and in the context menu's PNG and SVG export. With a set, every text names the host's families in order before its live font stack.

  • ChartExportFonts.embedInSvg (default false): the SVG also carries every face as an @font-face data URL. A PNG with a set always embeds every face, so the browser falls back per glyph across the host's families before any system font (CH-6, for the explicit set).

  • SVG and PNG embed TrueType, OpenType CFF, WOFF and WOFF2 as given; a font collection or unknown bytes reject with EXPORT_FONT_FORMAT_UNSUPPORTED. Font errors of an SVG or PNG export carry format: 'svg' or 'png'.

Without a set, SVG and PNG export exactly as before (the SVG is byte-identical to the earlier capture). PDF is unchanged.

Changed — BREAKING (export font model and PDF, ADR-058)#

  • One export-font model. ChartPdfFont and the PDF option font: {family, normal, bold?} (base64) are removed. PDF export takes fonts: ChartExportFonts: families in fallback order, each with faces whose source is an ArrayBuffer, a Uint8Array or a lazy host loader, plus weight and style. exportAsPdf also accepts fonts: 'inherit'. Migrate font: {family, normal: base64} to fonts: {families: [{family, faces: [{source: bytes}]}]}.

  • PDF refuses what it cannot draw. Without export fonts, text outside WinAnsi rejects with EXPORT_FONT_REQUIRED (it used to draw broken). With or without fonts, complex and right-to-left scripts, combining marks, ZWJ/ZWNJ and variation selectors, emoji and supplementary-plane characters reject with EXPORT_TEXT_UNSUPPORTED; text no family covers rejects with EXPORT_GLYPH_COVERAGE. No file is written.

  • The standard font is Helvetica. Without export fonts the PDF drew in Times since FAM-V2-T6 (svg2pdf.js did not find the family Helvetica); it now draws in Helvetica, as documented.

Added (export font model and PDF, ADR-058)#

  • ChartDefaults.exportFonts: the export fonts of every chart below, used by the handle's and the context menu's PDF export. An explicit fonts wins; 'inherit' skips the defaults for one call.

  • Per-element font choice in PDF. Each text element draws in the first family that covers all of it (coverage read with jsPDF's own parser); bold, italic and bold italic faces are optional and fall back deterministically, never synthesised. Faces load before the capture moment; a loader runs once per face object and a failed load is retried by the next export.

  • ChartExportError.code (ChartExportErrorCode) and its metadata (format, family, detected, codePoint, reason, script, normalizable, families); missingModule stays. Never the exported text.

  • Types ChartExportFonts, ChartExportFontFamily, ChartExportFontFace, ChartExportFontSource, ChartExportErrorOptions.

Added (text measurement and RTL chrome, ADR-057)#

  • RTL chrome. Under dir="rtl" the legend flows from the right and its arrow keys follow the reading direction; the tooltip prefers the inline-end side of the pointer; the context menu opens toward the inline end, its submenu opens left with ArrowLeft and closes with ArrowRight; exported legends and report text follow the same direction. Axes, data order, the navigator and the plot arrow keys are unchanged.

  • Direction-aware SVG anchors. Geometric labels (ticks, annotations, slice, bar, funnel and sankey labels) keep their physical side in rtl; tile, header, breadcrumb and gauge titles start at the inline start. A reference-line label without a position sits at the inline end.

  • Measured, grapheme-safe text. Mounted charts measure labels in their painted font (weight, style, direction, letter spacing, kerning) and again after a font load; truncation never splits a grapheme cluster; export report text wraps CJK and never breaks at NBSP or NNBSP.

Changed (text measurement and RTL chrome, ADR-057)#

  • A treemap tile shows its share whole or not at all (no truncated number).

Added (direction and bidi, ADR-056)#

  • dir prop on every chart. One UI direction: the explicit dir, else the page's direction (the nearest dir attribute, followed when it changes), else ltr; never the locale. The context menu under <body>, the tooltip and the screen-reader table carry it. It never reverses an axis or reorders data.

  • Isolated text. Series names in the tooltip and legend and labels in the screen-reader table are dir="auto" and isolated; formatted values are one isolated run; SVG text is unicode-bidi: isolate (new partial styles/direction.css). A right-to-left series name in the legend's isolation announcement is isolated with FSI/PDI.

  • Export keeps direction semantics. direction and unicode-bidi are materialised into SVG, PNG and PDF exports; an ltr chart with Latin text exports byte-identically. CSV carries no bidi controls.

  • Formatted values read in their locale's direction (P6 refinement): tooltip and screen-reader table values carry their presentation locale's script direction where it differs from the chart's.

Changed (portable runtime, ADR-054)#

  • jspdf and svg2pdf.js are optionalDependencies: rendering never loads them, and an installation without them fails exportChartAsPdf (and the menu's PDF export) with a ChartExportError whose missingModule names the module, instead of the bundler's resolution error.

  • ChartExportError is public (root export), so a host can recognise a failed export in onExportError.

  • The CSV serializers come from @workspace/active-utils/serialization, and IsoDateString from @workspace/active-types/primitives/iso-date, instead of the server-bearing /helpers barrel and the active-types root.

Fixed (reporting and export, ADR-053)#

Each defect below was found by the T6 browser evidence and has a witness that failed on the old code.

  • An export across a data change shows one moment. The legend of an image export was built when the export was called and the drawing was cloned after the chart had settled; a host that changed its series in between got the new lines under the old legend. The handle now reads the legend at the capture moment, from the latest commit (<Chart> and the standalone families alike).

  • Legend and report text are visible in a PDF under oklch() tokens. The legend swatches and the report title, subtitle and caption kept the host token's oklch() value, which svg2pdf cannot read: the swatches were missing and the text was drawn invisibly. Their colours are now written as rgb()/rgba() like the rest of the chart.

  • A PDF draws only in the font it embeds. Text of weight 600 (the report title) fell back to Times, unembedded and Latin-1 only, also with a host font: svg2pdf asked for a style named after the weight. Numeric weights now become bold or normal, and without font.bold bold text uses the regular embedded file.

Added (reporting and export, ADR-053)#

  • legendItems on exportChartAsPng, exportChartAsSvg and exportChartAsPdf accepts a function, called at the capture moment. An array is taken as given. Additive.

Fixed (accessibility and touch, ADR-052)#

No public API changed. Each defect below was found by the T5 browser evidence and has a witness that failed on the old code.

  • A reference-line label is no longer hidden by the marks (PH6-01). The lines and bands of crossLines stay between the grid and the series; their labels are now drawn above the series with a halo in the chart background (Canvas under forced colours). At 390 and 320 px the "105" of the dual-axis chart was covered by the last bar.

  • Every way of opening the context menu is announced. Shift+F10, the context-menu key and a long press now announce "Context menu opened", as a right click already did.

  • The keyboard reader hears menu, zoom, legend and pin announcements. The chart's live region showed the keyboard cursor text before any other announcement, and that text stays set while a reader navigates, so a keyboard zoom was never heard. The most recent announcement now speaks, and a cleared one no longer brings the older cursor text back.

  • A host menu item gets the item the menu was opened on. Opened from the keyboard, the element is the focused item; from a right click or a long press, the item under the pointer; on empty plot area, null as before. Opening moves focus into the menu, which cleared the hover the element was read from, so a keyboard-opened host item received null and the built-in "Copy value" was disabled.

  • The keyboard focus follows onto a submenu trigger ("Export ▸"), as on a plain item.

  • A long-press menu stays open when the finger lifts. A browser whose own long press is longer than the chart's (500 ms) read the lift as a tap and closed the menu with the click that followed; the press now cancels its own click.

  • No console error on every touch move over a Cartesian chart. The hit layer called preventDefault() in a passive listener, which the browser ignored with an intervention error; the page scrolled regardless, and still does.

  • The navigator brush follows a finger. The navigator strip declares touch-action: pan-y, so a sideways drag moves the brush instead of being taken for a page pan; a vertical swipe still scrolls the page.

Added (host-integration contract, ADR-051)#

  • englishChartTranslator: the package's English translator, complete by type, for standalone hosts, fixtures and examples that pass it on purpose. Nothing installs it implicitly; a chart without a translator still throws.

  • ChartPresentation is an alias of the active-i18n ValuePresentation (active-i18n D23); the type is unchanged. No breaking change.

Fixed (stylesheet activation and scoped text measurement, ADR-050)#

  • A stylesheet that stops or starts applying refreshes the chart's decisions. A change of media, disabled, rel, type, href or title on a <style> or stylesheet <link> in <head>, and a stylesheet <link> that fails to load, now move the style generation: on-fill label colours follow, and an export taken during such a change starts again instead of writing the previous view. What still needs invalidateChartStyles() is listed in API.md § 13 (CSSOM edits including sheet.disabled and <style>.disabled, adopted sheets, stylesheets outside <head>, media conditions other than colour scheme and forced colours).

  • Labels are measured in the font they are painted in. Tick labels, data-label collision and export report text resolve var(--font-sans), an inherited family and the weight in the chart's own element instead of the page body. Two sibling charts under different font variables now lay out the same labels differently and correctly (rotation, truncation, thinning, edge fit); a change of --font-sans to a font that is already loaded measures again. Before mount a chart estimates its labels and reads no style, so server markup and hydration agree. A bold report title wraps where its bold width says.

  • No public API changed.

Changed — BREAKING (collision-safe series identity, ADR-049)#

  • A series that declares an id is named id:<id> in selection. The seriesId of ChartItemRef and ChartItem (in selectedItems, defaultSelectedItems, onSelectionChange and onItemActivate) is now the series identity itself: id:<id> for a declared id, else <type>:<key> (unchanged, e.g. line:price), else position:<index>. The namespaces are disjoint, so no two series of a chart share a seriesId. The id: prefix used to be stripped, so an id spelled like another series' identity (id: 'line:a' next to a line over a, id: 'position:1', id: 'line:a#2') produced one seriesId for two series: a click on one reported the other, and a stored selection marked both or neither.

  • Migration. Where a host writes a seriesId for a series with an id, prefix it: {seriesId: 'revenue', …} becomes {seriesId: 'id:revenue', …}. Selections stored from onSelectionChange must be discarded or migrated the same way. Derived identities are unchanged. No alias is provided.

  • Repeats are defined, not only reported. A repeated identity keeps its order-based #n suffix and is documented as such; the development report now names that outcome.

Fixed (series-scoped display truth, ADR-048)#

  • A percentage stack no longer changes what other series show. A percentage-stack member used to write its share into the displayed row shared by every series, keyed by field. A raw series over the same field then showed the share, a second percentage stack over that field overwrote the first stack's shares, and a hidden member emptied the field for every series. Each member now displays its share from rows of its own; geometry, domain, stack baselines, hit geometry, tooltips, the accessible table and announcements read those, and every other series keeps its own value. CSV, copy-as-table and the navigator stay raw, as before.

  • A zoomed waterfall keeps its running total beside a percentage stack. The waterfall used to carry the running total of the steps before a zoom window only while the displayed rows were the zoomed array itself, so any percentage stack in the chart restarted it at zero. The positions are now placed on the displayed rows by the zoom window and the origin of each prepared row.

  • No public API changed.

Changed (theming: the package owns only --ac-*, ADR-047)#

  • The internal animation custom properties moved out of the host's namespace. The fourteen properties the charts write inline for their entrance and update animations are now --ac-fade-*, --ac-morph-*, --ac-segment-*, --ac-sweep-* and --ac-wipe-* (they were --chart-fade-duration, --chart-sweep-angle and their siblings). The public --chart-* names belong to the host's token contract. They were never documented and no workspace consumer read them; a host stylesheet that did must use the new names. Timing and behaviour are unchanged.

  • Documentation. The theming design, the architecture and competitive review and the two older library comparisons are non-canonical research under docs/research/ (deleted 2026-10-07); the obsolete stacked-area analysis is removed. Canonical claims that no longer matched the source are corrected (ChartHandle exports return promises, onExportError, no ChartOptions.background, no d3-interpolate, no visual fields on <ChartDefaults>, the fixed sqrt x-axis domain, the PDF font).

Added (theming: a host palette is the host's, ADR-046)#

  • Guards for the package's brand-neutral identity. The unit suite pins the chart values of styles/defaults.css in both modes and the thresholds the browser suite asserts. The platform host now sets its own chart palette (active-ui ADR-030) and charts pick it up through the token contract; the package defaults, the public API and the rendering are unchanged, so a page without host tokens shows the same palette as before.

Removed (theming: no visual theme API, ADR-045) — breaking#

  • theme and ChartThemeOverride. Charts and ChartOptions take no theme prop and the type is no longer exported. A per-chart or per-region change of a token is plain CSS on the chart's className or an ancestor; charts follow it without a remount (ADR-044).

  • The visual fields of <ChartDefaults>. <ChartDefaults> no longer takes theme or colors, and a chart no longer inherits a palette from it. Set colors on the chart that needs its own categorical palette; leave it out to use the host palette.

  • --bullet-bar. The token, its package default and its role are gone. The bullet's value bar is drawn in the on-fill token that contrasts most with the bands and track beneath it: dark over pastel bands, as before; light over a dark track, where it used to be dark on dark.

Changed (theming, ADR-045)#

  • The chart wrapper is data-chart-scope (was data-chart-theme-scope) and carries no inline style. A scoped host override set on it or on an ancestor reaches the chart.

  • colors is documented as what it is: the categorical palette of one chart, replacing the host palette in the colour assignment and nothing else.

Fixed (theming, ADR-045)#

  • The pie and donut legend colours every key like its slice. Without colors, the legend used a five-slot palette, so the sixth slice onwards showed a legend colour that differed from its slice; it now follows the same ten slots (or colors) as the slices.

Added (theming: live-element resolution, invalidation and export consistency, ADR-044)#

  • invalidateChartStyles(). A value-free notification for a style change no observer can see (a rule inserted through the CSSOM, an adopted stylesheet): every chart re-reads its tokens in its own scope. It sets no theme and carries no value. A class, an inline custom property or a stylesheet element needs no call.

Changed (theming, ADR-044)#

  • Colour decisions resolve in the chart itself. The on-fill colour of a label (bar, heatmap, treemap, sunburst, funnel) is decided on the tokens of the live chart element, so a scoped host override and the per-chart theme (removed in T3) count; it no longer reads the page body. The decision is taken after mount, before the first paint; server render and hydration read no computed style for a colour.

  • Runtime switching without a remount. Charts re-decide on a colour-mode, accent, token, scoped-override or stylesheet change: a class, data-theme or custom-property change on any ancestor of the chart, a stylesheet added, removed, rewritten or loaded, and prefers-color-scheme/forced-colors. One shared observation serves every chart and is released with the last one; a colour change recomputes no geometry.

  • Heatmap cells and gradient legend are one scale. A cell paints its colour as a CSS mix of the two ends of the scale, the same references the legend uses, so both follow a token change by themselves. Behaviour: a cell's fill and point.color in its hover event are now a CSS expression (color-mix(in srgb, …) or a token reference) instead of an rgb() string.

  • Exports are one moment of the chart. SVG, PNG and PDF run one capture transaction: they wait for a still chart and loaded fonts, clone and read every colour and the background in one step, and start again (at most three times, then ChartExportError) when the page styles changed meanwhile. Every colour an element declares is resolved in that element's own scope and written as rgb()/rgba(), which a PDF can read. Behaviour: SVG files carry rgb() instead of the host's notation (oklch()).

  • Behaviour — the PDF page takes the chart's background. exportChartAsPdf and exportAsPdf default to the chart's own background, as PNG already did, so a dark view exports as a dark page. Pass backgroundColor for another page colour; a light report from a dark view is an export in the light colour mode.

Removed (theming, ADR-044)#

  • The unused dependency d3-interpolate (the heatmap no longer interpolates in JavaScript).

Changed (theming: CSS token contract and package defaults, ADR-043)#

  • Charts read the public tokens directly. Categorical and data roles come from --chart-1 … --chart-10, --chart-positive/-negative/-caution, --chart-sequential-*, --chart-diverging-*, --chart-on-fill-* (and the transitional --bullet-bar); surfaces, text and controls from the host's --background, --foreground, --card, --muted, --muted-foreground, --border, --ring, --popover, --accent, --primary (and their -foreground pairs). No chart reads a Tailwind --color-* alias any more, and the scattered literal fallbacks are gone.

  • Complete package defaults. Every token has a package default for light and for dark (.dark on the document root or an ancestor), so a chart renders fully without any host stylesheet. A token the host sets always wins, whatever the stylesheet order: the package never declares a public token.

  • One package stylesheet, loaded by the entry. Token defaults, focus, motion, forced colours and print are one stylesheet that the package entry imports once as a static CSS side effect; components no longer import stylesheets one by one. A host needs no utility-class scan of the package source and no import.

  • theme (ChartThemeOverride) writes the public tokens it overrides (--chart-N, --chart-positive, --foreground, --card, …) on the chart wrapper, so it keeps working against the new reads. theme, colors and the <ChartDefaults> visual fields are otherwise unchanged.

  • Structural colours follow the host on the element. The focus ring, for example, now reads --ring where a scoped theme sets it, instead of an alias resolved on the document root.

Fixed (theming, consumer-visible, ADR-043)#

  • The heatmap's cells follow the sequential colour role. The cells used a fixed light-to-dark ramp while the gradient legend used --chart-sequential-min/-max; in dark mode the two disagreed, and a host value never reached the cells. The cells now use the same resolved range as the legend.

Added (selection, linked views and navigator, ADR-041, ADR-042)#

See ADR-041 and ADR-042 and the migration notes.

  • Item selection and activation. <Chart> and the thirteen Cartesian wrappers take selection (mode: 'single' | 'multiple', itemKey), selectedItems (controlled) or defaultSelectedItems (uncontrolled), onSelectionChange(selection, change) and onItemActivate(item, source), also inside events; ChartOptions.selection. An item is ChartItemRef {seriesId, key, interval?}: the series identity and the value of the item key, never an array index; a grouped bucket carries its interval. New public types ChartItem, ChartItemKey, ChartItemRef, ChartSelectionChange, ChartSelectionEvents, ChartSelectionOptions, ChartSelectionProps, ChartSelectionState.

  • Selection for every reader. A click selects and activates; Space selects and Enter activates the focused item; Shift+F10 and the ContextMenu key open the context menu. A selected item is outlined (in Highlight under forced colours), read "…, selected" in the screen-reader table and in the focus announcement, and each change is announced. A hidden, zoomed-out or regrouped item stays selected but inactive; an uncontrolled selection drops removed rows with source: 'update'.

  • Host words. Keys charts.accessibility.selectedState, itemSelected, itemDeselected, selectionHint, activationHint in the key contract and in every platform language.

Changed (selection, linked views and navigator, ADR-042)#

  • Behaviour — linked views link by value. A <SyncGroup> shares the x value under the pointer or keyboard focus and the zoomed x range as values, and each member shows them over its own rows: two charts with other rows, sizes or series now show the same dates. Members that read x differently (another axis kind or temporal semantic) no longer follow each other, the y range is no longer linked, a member that unmounts withdraws what it shared, and ranges that arrive faster than a chart renders no longer echo between two charts.

  • Behaviour — the navigator draws compound series. Candlestick and OHLC as a low–high band with the close, range area and range bar as a band, a box plot as a min–max band with the median; hidden series are left out; gaps stay gaps (a missing value was drawn as 0); long series keep every spike (extrema per pixel column). Date rows on an ordinal-time axis are placed by date (they were all drawn at the left edge).

Changed (Level 1 budgets and evidence, ADR-039, ADR-040)#

See ADR-039 and ADR-040 and the migration notes.

  • Breaking — percentage stacks take raw values (W9). stackedPercentage (BarChart, and new on AreaChart and AreaSeriesOptions) draws each value as its share of the visible members of its stack; hiding a series restacks to 100 %, and a hidden member has no share. Mixed signs keep their direction (ADR-020). Axis, tooltip, table, announcement and segment labels read percentage points; CSV stays raw. stackTotal and stackTotalTolerance are removed.

  • Breaking — honest options (W10). ChartOptions.background is removed. Scalar, categorical, hierarchical, graph, heatmap and timeline props no longer accept the props they cannot act on (colors, tooltip, contextMenu, onZoomChange, onLegendToggle, animation, per family; also inside events). An area's fillOpacity now acts in gradient mode too (the top of the gradient; default 0.4).

  • Breaking — exports return promises (W11). exportAsPng, exportAsSvg, exportAsCsv and exportAsPdf resolve when the file is handed over and reject with an error when it could not be produced; exportChartAsSvg is asynchronous. New onExportError event; context-menu export failures are reported instead of swallowed. Image exports wait until the chart is still. New report: {title, subtitle, caption} export option (ChartExportReport) and PDF font option (ChartPdfFont); PNG embeds the host's same-origin web font. Standalone families export their own rows as CSV instead of doing nothing.

  • Breaking — invalid input shows the unavailable state (W12). A sankey cycle or unknown node, an empty or inverted scalar range, a negative or cyclic hierarchy and an inverted explicit axis domain render the translated charts.unavailable state with a development diagnostic instead of throwing (sankey) or drawing a degenerate chart.

  • Two y axes. One binding (axis.id ?? axis.position) for every stage; an unknown yAxisId falls back to the default axis everywhere, with a diagnostic. CrossLineOptions.yAxisId binds a reference line to an axis. Fixed: the right y axis stood on the left edge of the plot, its labels inside the plot.

  • Fixed: a bottom or top axis took the unit of vertical series (an age axis under a percentage area read "30%").

  • Fixed (PH5-01): the outer labels of a truncating category or ordinal-time axis stay inside the chart.

  • Accent themes (W18): the active-ui accent themes no longer replace the measured categorical palette.

  • Evidence: performance budgets for the accepted workloads (benchmark:browser fails on a breach), detached-DOM and heap measurement in the cleanup suite, and type-checked documentation examples.

Changed (readable presentation, ADR-037, ADR-038)#

Charts stay readable and distinguishable as width shrinks, series multiply and translations lengthen (W7, W8, ADR-037, ADR-038). No prop changed; the defaults did, and every dashboard looks different.

  • Palette (W7). The default categorical palette is hue-distinct: blue, orange, teal, pink, violet, green, sky, ochre, crimson, slate, with its own lighter values in dark mode. Every colour reaches 3:1 against page and card; the first six stay distinct under simulated protanopia, deuteranopia and tritanopia. colors and per-series colours still win.

  • Colour roles. Tokens at the owner (@workspace/active-ui themes.css): --chart-sequential-min/-max replaces --chart-heatmap-min/-max (the heatmap is its one reader; a host that set the old names sets the new ones), plus the new --chart-diverging-* and --chart-on-fill-dark/-light.

  • Text on marks (QR-4). Inside bar labels, funnel segments, sunburst arcs, treemap tiles and headers and heatmap cells take the on-fill colour that reaches 4.5:1 against what they sit on; a label that no on-fill colour makes readable moves outside a free-standing bar or is left out. The funnel and heatmap luminance heuristics are gone; the funnel's value line is no longer drawn at 80 % opacity.

  • Second encoding. A line series from the seventh palette slot on, or whose colour repeats past the palette, draws a dash pattern that follows its identity, in the plot and in the legend key. lineDash wins.

  • Text floors (W8). The layout scale runs from 1.0 at 600 px to 0.9 at 320 px (it was 0.75 up to 500 px), and responsive text never goes below 10 px (ticks, labels) or 12 px (legend). Fixed 9 px texts (treemap values, sunburst outer arcs, timeline dates, small heatmap cells, pie and nightingale values) are 10 px. A text that does not fit is left out, not shrunk.

  • Tick measurement. The canvas measured tick labels in its default 10px sans-serif, because it ignores a font stack with var(); it now measures the host font, and measures again once fonts have loaded. Axes show fewer ticks where labels used to touch, and vertical axes are a few pixels wider.

  • Dense categories (PH3-01). A category axis with less than 4 px per category no longer rounds or pads its bands, and a bar is at least 1 px wide: a 1,000-category chart on a 1,120 px plot draws every bar instead of none.

  • Plot edges (PH3-03, PH4-01). A candlestick, OHLC, box plot, bar, range bar or waterfall on a continuous x axis without an explicit domain gets half a glyph of room at both ends; the first and last glyph are whole and hoverable in every engine.

  • Legend (AC-B1). A legend that does not fit its height folds behind a "Show N more" / "Show fewer" button (aria-expanded) instead of scrolling without a cue. New host keys charts.legend.showMore and charts.legend.showFewer, in every language of @workspace/locales.

  • Organisation chart. The initial fit never shrinks node text below 10 px; a tree that does not fit starts on its root, and keyboard focus pans to the node it reaches.

  • Motion (W8). Default entrance 400 ms (was 600, standalone families up to 800), stagger 30 ms per series (was 80) capped at 200 ms; funnel segments and hierarchy levels use the same pace. Explicit animation settings still win; reduced motion still turns motion off.

  • Screen-reader table (PH1-03). The Cartesian data table sits in a clipping container and no longer widens a narrow page.

  • Forced colours. The first six line series and their legend keys get system colours with distinct patterns.

Changed (complete information paths, ADR-035, ADR-036)#

What a chart draws can be read without sight or pointer (W5, W6, ADR-035, ADR-036).

  • Compound glyphs are one item (W5). Candlestick, OHLC, box plot, range bar and range area now have hover targets and keyboard stops: one per glyph, covering its whole value extent. Range bar and box plot had none by an earlier decision, which W5 reverses. The default tooltip lists every value role with its host word (charts.role.*) instead of the letters O H L C, the keyboard announcement states every role, and the data table has one column per role instead of an empty cell.

  • SingleTooltipData.roles (new public type ChartValueRole) carries the roles to a custom tooltip renderer.

  • Standalone families have a data table (W6). Pie, donut, funnel, radar, nightingale, radial bar, heatmap, treemap, sunburst, sankey, org chart, timeline, gauge, linear gauge and bullet render a screen-reader table with their own structure (category and share, matrix, path, source and target, parent, track, value with range, target and zones). It follows accessibility.dataTable and cannot widen a narrow page. Gauge, linear gauge and bullet now render a fragment (the SVG plus the table).

  • Waterfall. The table lists each step and its running total, and the announcement states both. A zoomed waterfall keeps the running total of the steps before the window; before, it restarted at zero (PH3-02).

  • Instants keep their time in the tooltip, crosshair, table and announcement (datetime preset); before, rows of one day read the same (PH1-02). A calendar date stays a day.

  • Standalone announcements (pie, donut, funnel, nightingale, radial bar, radar, heatmap, treemap, sunburst, sankey) read the value in the chart's notation (12,400), as the table does, instead of the raw number (12400).

  • Legend announcements use the name the legend item shows; an unnamed compound series was announced with an empty name.

  • New host keys (every language, ADR-028): charts.role.open, high, low, close, min, q1, median, q3, max; charts.accessibility.seriesRole, roleValue; charts.dataTable.category, value, share, series, name, parent, path, source, target, date, track, measure, zone, range, detail.

  • Accessibility statement. docs/accessibility/ACCESSIBILITY.md is an evidence-scoped, AA-oriented support statement without a certification claim; the supported AT pairs are NVDA with Chrome or Edge and VoiceOver with Safari. No human AT run is recorded yet.

Changed (truth-preserving data preparation, ADR-033, ADR-034)#

Reduction and grouping may make a chart faster; they no longer change what the data means (ADR-033, ADR-034).

  • Hidden series leave the automatic domain (W17). Hiding a line or a non-stacked bar now rescales the axis to the series still drawn, as hiding a stacked series already did. An explicit min or max is unchanged.

  • Reduction replaces LTTB. Above maxPoints rows (default 500, 0 turns it off), lines and areas keep, per pixel column, every series' first, last, lowest and highest row, every stacked-area top edge and both sides of every gap, bucketed on the actual x value (number, time, log, sqrt, or the rank on an ordinal-time axis). Every displayed row is a source row. Before, LTTB sampled by row position on the first series only and could drop a null gap, a secondary series' spike, categories, scatter points, waterfall steps and the rows of a compound series next to a line.

  • Families that are never reduced (W3). Bars (category or time x), scatter, histogram, waterfall, candlestick, OHLC, ranges, box plots, lollipops and dumbbells keep every row; next to a reduced line, every row they draw is kept. A chart with a waterfall is not reduced at all, so its running totals are computed on every step. A category x axis is never reduced. Charts below the density of their plot (at most four rows per series per pixel column) draw every row, as before; the long price series shape at desktop width now lists all its days in the data table instead of 500.

  • maxPoints on LineChart and AreaChart. The threshold is reachable from the two wrappers whose families reduce.

  • Grouping by role. Candlestick and OHLC group as first open, highest high, lowest low and last close; range-area and range-bar as lowest low and highest high; a bar's error bounds and a scatter's size follow the value's method. Every field is written under the series' own key (the 'ohlc' and 'range' methods no longer write hard-coded open/high/low/close fields or a [low, high] tuple). On a single-valued series, 'ohlc' and 'range' fall back to the type's default with a development diagnostic.

  • One grouping policy per chart. A chart groups every series or none: a series with dataGrouping: false (or {enabled: false}), or a waterfall, histogram, box plot, lollipop, dumbbell or horizontal series, keeps the whole chart ungrouped, with a development diagnostic. Before, an opt-out on a later series was ignored and that series was averaged.

  • GroupMeta.count is now the number of source rows in the bucket's interval (it counted the first series' values); GroupMeta.raw describes the first series with a value in the bucket.

  • Raw and displayed data (W4). The screen-reader table and keyboard announcements describe the displayed rows: a reduced table is described as showing N of M rows, a grouped table reads each x cell as its interval and adds a column with the number of source rows, and a grouped point's announcement names both. The CSV export (handle and context menu) writes the chart's full raw data; copy-as-table now writes the raw rows of the visible range instead of the displayed ones.

  • New host keys (every language, ADR-028): charts.accessibility.reducedData, groupedData, interval, sourceCount and groupedPoint.

Changed (resolved series model, ADR-031, ADR-032)#

Every stage of a Cartesian chart now reads one description of each series (ADR-031, ADR-032). Visible corrections a consumer notices without changing any code:

  • Series identity (W1). A series is identified by its id, or else by its type and primary value key. A series the host adds is drawn; a user's legend hide stays with its series when the host adds, removes or reorders series; a series the host removes and adds back is visible again; colours and exit animations follow the series, not its position. Two series with the same identity are reported in development. visible: false now means "starts hidden" consistently: the series is not drawn, stacked, hit or counted until the user shows it in the legend (before, it was drawn but not interactive). Categorical legends (pie, funnel, nightingale, radial bar, radar) follow the category (or series) the same way.

  • Hidden stacked series (W2). A hidden series no longer lifts the stack: the visible series restack onto zero and the automatic domain follows the visible stack.

  • Bars start at zero (W16). The automatic value domain of bar, histogram, lollipop and waterfall marks includes zero (not on a log axis); an explicit min or max still wins.

  • Stacked domains. A line on the same axis as a stack contributes its own extent instead of being added to the stack sum; bar and area stacks are separate stacks in the domain, as they already were in the geometry; horizontal stacked bars get a stacked x domain.

  • Orientation and scale. A horizontal range bar and points on a sqrt x axis get a numeric domain and are drawn inside the plot.

  • Category position. Points, lines, areas, candlestick and OHLC glyphs, hit points, annotations, vertical reference lines and the synchronised crosshair sit at the centre of their category, where the tick label is; bars are centred in their column (the gap between grouped bars is split over both sides).

  • Axis binding. A series bound to a second y axis (yAxisId) is drawn on that axis's scale; line, area, scatter, candlestick, OHLC, box-plot, histogram, waterfall and range renderers projected on the default axis before.

  • Hit geometry. After a legend toggle, grouped-bar hover targets are the drawn bars; a hidden series has no hover target.

Removed (breaking)#

  • The root entry publishes the chart contract only (ADR-029). Removed from @workspace/active-charts: the renderer building blocks (SvgRenderer, the *Series components), axis and grid-line components, overlays (Annotations, ChartContextMenu, CrossLines, Crosshairs, ChartNavigator, BrushOverlay, ZoomSelectionOverlay, SyncCrosshair, GradientLegend, AccessibleDataTable, VoronoiOverlay, ChartTooltipContent, TooltipFadeWrapper, ChartLegend), ResponsiveContainer, all internal hooks (useLayout, usePreparedData, useScales, useVoronoi, useChartTooltip, useLegend, useChartDefaults, useChartHandle, usePeriodSelector, the touch, zoom, animation and keyboard hooks, useSyncState, useSyncPublish), scale, layout and data helpers (createChartScale, computeLayout, lttb, validateChartData, applyMissingDataStrategy with its MissingDataStrategy type (no chart option used it), the grouping internals, computeHistogramBins, computeWaterfallPositions, …), the CSV building helpers (buildCsvString, formatCsvValue, deriveColumnsFromOptions), appendLegendToSvg, resolveCssVariables, ACTIVE_CHARTS_THEME, resolveColor, normalizeDataLabelOption, the animation, touch and tooltip constants, and the utilities getSeriesColor, resolveCollisions, estimateLabelRect, mean, median, localPoint. No consumer used them. Migration: none for the documented API; a host that composed these parts uses <Chart options> instead.

  • Formatter re-exports removed. formatNumber, formatCurrency, formatDate, formatDateTime, formatFractionAsPercent, formatPercentagePoints and their option types come from their owner, @workspace/active-i18n/formatters.

  • GeoChartProps removed. It had no implementation; a geographic chart will bring its own props type.

Added#

  • A browser benchmark (pnpm --filter @workspace/active-charts benchmark:browser, no retries) with the Phase 3 baseline in docs/PERFORMANCE.md.

  • id on every series type (candlestick, OHLC, box plot, waterfall, range area, range bar, dumbbell, lollipop) and on the wrapper series configurations (LineChart, AreaChart, BarChart, LollipopChart, ScatterChart, RadarChart). Optional; the derived identity applies without it.

  • Root exports for types a consumer needs to name: AxisFormatter, AxisTickValue, CategoryAxisTickValue, NumberAxisTickValue, TimeAxisTickValue, CategoryAxisConfig, NumberAxisConfig, TimeAxisConfig, AccessibilityOptions and its three option types, KeyboardAnnouncementMode, AnimationType, ExitAnimationType, ExitPhaseOptions, ChoreographyLayer, Period.

Fixed (consumer-visible)#

  • CSV and clipboard export are safe for spreadsheets (ADR-030). Text fields, and the header row, go through the delimited-text boundary of @workspace/active-utils: always quoted, a carriage return or tab no longer breaks a row, and text a spreadsheet would execute as a formula (=…, @…, -1+1) is neutralised. Numbers, booleans, instants and declared calendar days stay unquoted machine literals. "Copy as table" uses tab-separated quoting instead of the CSV rules. Derived CSV columns now include the price keys of candlestick/OHLC, the low/high keys of range series and the box-plot keys (these exports contained only the x column).

  • Data grouping keeps every series: a grouped multi-series chart aggregated only the first y key, so the other series rendered empty.

  • Histogram bins: rows without a finite x value are skipped instead of counted as 0, explicit bins ranges are honoured as bin boundaries, and the default bin name formats its bounds in the chart's presentation instead of raw numbers.

  • One Infinity value no longer empties a numeric axis; non-finite values are left out of the domain.

  • Technical indicators: two indicators of the same kind and period on different source keys no longer overwrite each other (the key gains the source key when it differs from the default); a period that is not an integer ≥ 1 yields null instead of NaN.

  • minPixelInterval is a hard floor: when it exceeds targetPixelInterval, ticks now stay at least minPixelInterval apart instead of the floor being lowered to the target.

  • Timers of the tooltip auto-dismiss, the legend click debounce and the long-press no longer fire callbacks after unmount.

  • onAnimationComplete({type: 'update'}) fires after a data-update animation; it never fired, and the update state never ended.

  • GaugeChart, LinearGauge and BulletChart with min === max render an empty range instead of NaN geometry.

  • SankeyChart with only zero-valued links renders like an empty chart instead of NaN paths; SankeyChart and OrgChart fire onAnimationComplete({type: 'load'}) once on mount.

  • HeatmapChart context-menu cell lookup no longer confuses cells whose x or y value contains |.

  • A cancelled touch no longer opens the context menu through the long-press.

  • A legend at the bottom, left or right now reports a double-click isolate through onLegendToggle and announces toggles and isolates to screen readers, as a top legend already did.

Changed (breaking)#

  • A chart no longer speaks English and no longer reads the translator sideways from active-i18n (ADR-028, 2026-09-18). The translator comes from the t prop or from <ChartDefaults t>, and without either every public entry point throws a configuration error: every wrapper, <Chart>, PeriodSelector, AccessibleDataTable, useKeyboardNavigation and the default fallback of ChartErrorBoundary. All hardcoded texts (about seventy, most of them screen-reader text and keyboard announcements, plus two Dutch tooltip labels) go through the ChartTranslationKey key contract.

  • Removed: useChartTranslations and fallbackTranslate. New exported types: ChartTranslationKey, ChartTranslationVariables. ChartTranslateFn now takes a contract key; any host translator with a string key remains assignable.

  • generateAriaLabel(options, t) and applyIndicators(data, indicators, t, sourceKey?) take the translator as an argument.

  • A combination chart's accessible name uses full type names (bar chart/line chart instead of bar/line chart).

  • A time axis declares temporalSemantic: 'calendarDate' | 'instant', and without that declaration the chart fails (ADR-027, 2026-09-17). Required in the type on every 'time' and 'ordinal-time' axis, including the default x-axis of LineChart, AreaChart, RangeAreaChart, CandlestickChart and OhlcChart (where xAxis is now required), and at runtime via <Chart>, including the implicit ordinal-time axis of a <Chart> without axes. The string shorthands xAxis="ordinal-time" and xAxis="time" are gone.

  • useChartAxisFormatters().date(semantic, preset?) takes the semantic as its first argument.

  • TimelineChart: TimelineEvent.date is display text and is shown verbatim in the tooltip, the announcement and the aria-label, as on the marker label; useTimelineKbd no longer has a locale option.

Changed#

  • A chart's presentation comes from its locale prop or from <ChartDefaults locale>, and otherwise the chart fails visibly (ADR-026, 2026-09-16). The 'en' fallback of <Chart>, of the chart-local context and of useChartAxisFormatters/useKeyboardNavigation has been removed; without an owner the entry point throws a configuration error. A nested <ChartDefaults> now refines the enclosing provider instead of replacing it. ChartLocaleOverride has been removed from the public API (zero consumers).

  • The accessibility documentation no longer claims WCAG 2.1 AAA or a verified screen-reader matrix (2026-09-25, Enterprise Parity Programme Phase 1). Neither was backed by a recorded run, and known gaps contradict them: compound series show empty data-table values, the standalone families have no data table, and OHLC, range and box-plot series have no keyboard stepping. accessibility/ACCESSIBILITY.md now states what each family provides and at which evidence level; the replacement claim is an open decision (W6). No runtime behaviour changed.

  • Documentation now states the known limits of automatic reduction and data grouping (ACHARTS-01, ACHARTS-06): default LTTB above 500 rows can drop a null gap, another series' extreme, categories, scatter points, waterfall steps and compound rows; compound series are not grouped. Stale README, API, architecture and recipe examples were repaired to the current contract (temporal x-axis declaration, height, tooltip renderer parameters, formatter presets). No runtime behaviour changed.

Fixed#

  • A calendar date no longer shows the previous day on any surface (ADR-027). Under a zone west of UTC, 2026-01-01 rendered as 31 Dec 2025 on the axis, and the tooltip, crosshair, parent row, screen-reader table and keyboard announcement each showed their own reading (a raw ISO string in the table, a parsed string in the tooltip). All surfaces now read the value through the declared semantic; the CSV export writes a calendarDate column as YYYY-MM-DD.

  • The parent row groups ticks on the day its label shows, no longer on the runtime's local fields (ADR-027).

  • The internal elements of the standalone wrappers follow the app's presentation (ADR-026). Tooltip and series labels of, among others, Heatmap, Pie and Funnel read a chart-local context that was never set and showed English notation, even under <ChartDefaults locale>.

  • The chart context-menu labels use the infinitive and sentence case (context-menu standardisation, 2026-08-31). In Dutch, Kopieer waarde became Waarde kopiëren, Kopieer als tabel became Als tabel kopiëren, Zoom naar hier and Pan naar hier became Hierheen zoomen and Hierheen pannen, Reset zoom became Zoom resetten, Export became Exporteren, and Exporteer als PNG/SVG/CSV/PDF became Exporteren als …. In English the UI Title Case disappeared (Pin to Top -> Pin to top). Only the TEXT changed: the same keys, the same actions, the same interaction. The norm lives in docs/standards/PAGE_STANDARDS.md section "A context-menu item names the action" ("Een contextmenu-item noemt de handeling"); lint-context-menu-label.mjs guards its mechanically checkable half.

[5.0.0] - 2026-08-22#

Axis ticks are now resolved once, by one engine, and every consumer of a tick reads that result: the axis labels, the grid lines, the axis margin and the export. The immediate defect was a value axis over [0, 22] rendering twenty-three crammed labels in 130 pixels; the cause was that four layers each computed their own answer, and that integerTicks discarded the density budget the renderer had already calculated. See ADR-024 and ADR-025, and migration/MIGRATION-NOTES.md for the upgrade.

Removed#

  • AxisConfig.integerTicks / AxisOptions.integerTicks. It generated twenty-plus ticks and filtered the non-integers out afterwards, which cannot control density by construction, and it silently overrode a caller's tickCount. Replaced by deriving the smallest meaningful step from valueSemantics — the declaration the library already has for what a value means (WS-13 / PP-045). {kind: 'count'} means whole numbers because that is what a count is; no second flag says it again.

  • AxisConfig.gridLine.numTicks / AxisOptions.gridLine.numTicks. Grid lines had their own density, which is how twelve grid lines ended up under twenty-three labels. Grid lines now always follow the resolved ticks of their axis. A deliberate minor/major grid contract does not exist today; when it is wanted it will be designed as one.

  • Axis props numTicks, tickValues, tickFormat, integerTicks, rotation, autoRotate; XAxis prop width; XAxis/YAxis prop numTicks; XGridLines/YGridLines prop numTicks. These components no longer choose anything; they receive a resolved ticks object and draw it.

Added#

  • core/ticks/ — the tick engine. resolveTicks is the single authority. Interval-first: pick a nice step from {1, 2, 2.5, 5} × 10ⁿ, then generate, then measure, then coarsen if the labels do not fit. Public helpers: chooseNiceStep, enumerateNiceSteps, generateTicksForStep, niceDomainForStep, resolveTickBudget, semanticTickQuantum, hasDuplicateAdjacentLabels, getTickPixelPosition.

  • AxisConfig.tickInterval. A fixed step in domain units. Stronger than tickCount; an unreadable choice warns in development but is still honoured, because an explicit statement is not silently discarded.

  • AxisConfig.targetPixelInterval (default 72 vertical / 100 horizontal) and AxisConfig.minPixelInterval (default 32 / 40). The target drives how open the axis looks; the floor is a readability guarantee that no automatic resolution and no tickCount may cross. The old single minTickSpacing = 24 played both roles and was a reasonable floor but a hopeless target.

  • Calendar-aware ticks for ordinal-time axes. Ticks land on the first observation of each week, month, quarter or year rather than on every n-th data point, so a 30- or 90-day window reads as a calendar.

Changed#

  • tickCount now works. It is a target: the pixel floor and the semantic quantum remain constraints, but nothing overrides it behind the caller's back. The precedence tickValues > tickInterval > tickCount > automatic is documented and covered by tests.

  • Axis margins are measured, not estimated. The left/right reservation now comes from measureTextWidth over the actual resolved labels. The previous 8 chars × 7px + 16 estimate reserved 82px for labels needing 30 — over ten percent of the plot width on every count chart in the repository.

  • Category axes keep their labels longer. A category label is an identity, so rotation and truncation now come before thinning. Time-series axes are unchanged in that respect: there a label is a position, and thinning costs nothing.

  • Category thinning uses a uniform stride anchored on the LAST value, with strides forming a doubling chain so a coarser step is always a strict subset of a finer one. Labels therefore disappear on resize without the remaining ones shifting. The previous sampleEvenly produced irregular gaps (indices 0, 2, 3, 5, 6 for seven days over five labels).

  • The legend no longer takes plot space (ADR-025). It is rendered as HTML outside the ResponsiveContainer and is now the sole owner of its own height. Every chart with a legend gains ~30px of plot area, and two same-height charts side by side align again when only one has a legend.

Fixed#

  • A value axis can no longer render two adjacent ticks with the same label. hasDuplicateAdjacentLabels inspects the real formatter output, so it also covers compact notation, consumer closures and every locale.

  • Grid lines coincide with their labels. They previously used scale.ticks(undefined) (d3's default of ten) while the axis used its own count.

  • Axis no longer re-formats labels the resolver already produced. A second formatting pass could diverge, because the boundary-aware date formatters read the tick index and the full tick list.

Also released here#

This release also cuts the work that had been accumulating under [Unreleased] since 4.1.0 — the WS-02 formatter re-exports, the WS-13 ChartValueSemantics declaration and the theme-token rename. Two of those carried their own ### Breaking notes and were waiting for a major to land in; their entries follow below, unchanged.

Why major#

Public props were removed (integerTicks, gridLine.numTicks) and the internal axis components changed shape. Independently of that, ADR-025 changes the rendered height of the plot area for every chart carrying a legend, which is a visible change on 72 call-sites. Both are contract changes, not fix size.

Added#

  • ChartValueSemantics: a series declares what its values mean, once (WS-13, PP-045, 2026-08-17). Until now only the axis was served: the app passed a closure, and a closure is behaviour you can call but cannot derive anything from. The tooltip and the screen-reader table fell back to one generic shape heuristic (Number.isInteger(v) ? 'integer' : 'decimal-2'), so a chart with a currency axis showed € 1.235.000 to a sighted reader and said 1.234.567,89 to a screen-reader user. That is not a matter of taste; it is a materially different quantity.

    The declaration is generic and owner/domain-blind: number, count, money (with an explicit currency), price (currency plus a declared price precision) and percent (with its stored unit explicit). Unit and precision are two independent properties — money EUR + 4 decimals, fraction-as-percent + 3 decimals and price in EUR + its own precision must all remain expressible, so a chart never derives that currency means two decimals.

    It lives on AxisOptions/AxisConfig (the usual place — one axis normally carries one unit) and on SeriesOptionsBase (the exception, for a chart with two units on one axis); the series wins. Axis, tooltip, split tooltip, screen-reader table and keyboard announcement all read the same declaration. Without a declaration nothing changes — the pre-WS-13 fallback stands, so the repair reaches exactly as far as the declaration does. CSV/data export is untouched by construction: it never took a presentation input (PP-013).

Changed#

  • Screen-reader output of custom accessible columns now follows the resolved presentation (WS-11 / CR-A11, 2026-08-16). SeriesColumn.formatter received (value, row) and therefore knew the value but not its notation. Two bar modes wrote past the shared formatters as a result: stacked percentages emitted `${v}%` and error bars concatenated raw numbers. A reader under a comma-decimal presentation heard 12.5% while the visible axis showed 12,5%.

    The signature is widened additively to (value, row, context), where context: AccessibleFormatterContext carries the chart's own presentation input — nothing else. Single- and two-argument formatters remain valid, because a function with fewer parameters is assignable in TypeScript.

    Consequences for the bar plug-in, both with the source unit stated explicitly rather than inferred from magnitude:

    • stacked percentages go through formatPercentagePoints(v, 'decimal-1', …) — percentage points by definition (the stack sums to stackTotal), never a fraction. The preset mirrors the tooltip rather than the axis: a table cell shows one value, and integer would round 12.5 to 13 in the only output a non-sighted reader has;

    • error-bar value, low and high all go through formatNumber(v, integer|decimal-2, …) — the same rule the default cell and the visible tooltip already used.

    Visible output moved with it: the default error-bar tooltip formatter of BarChart formatted its value but concatenated raw bounds. It now uses the same rule, so visible and assistive output cannot diverge.

    Nothing here changes the package boundary. The context carries a generic presentation and no administration, tenant or profile concept; machine CSV is untouched and still takes no presentation at all.

  • Machine CSV no longer infers date-only semantics from a clock value (CORR-09, 2026-08-15). formatCsvValue treated any UTC-midnight Date or ISO string as a calendar day and truncated it, so a genuine instant of 2026-01-19T00:00:00.000Z left the export as 2026-01-19 — time and type gone, and unrecoverable. Midnight is a clock reading, not a meaning.

    Semantics are now declared or lossless: the new CsvValueSemantic ('date' | 'dateTime') can be supplied per column key through CsvExportOptions.columnSemantics on buildCsvString/exportChartAsCsv; without a declaration the full value is kept. No locale, no presentation context — the machine boundary stays presentation-immune (PP-013).

    Migration: a column that really is a calendar day must now say so. An undeclared Date exports as a full ISO instant, which re-reads as the same value; the previous output re-read as a different type.

Added#

  • locale accepts a presentation-context object in addition to a language code. The new public type ChartPresentation (string | (DatePresentationContext & NumberPresentationContext)) applies to the locale prop of every chart, ChartDefaults.locale and all internal pass-through points; the value goes unchanged to the central active-i18n formatters. A bare language code remains fully valid ("notation follows the language") — existing consumers do not change. The package stays tenant-blind: no knowledge of administrations, profiles or settings; the consumer composes the context in the app layer.

  • useChartAxisFormatters(presentation?) / createChartAxisFormatters: shared axis-formatter factories (number, currency, fractionAsPercent, percentagePoints, date) that resolve their presentation from ChartDefaults — the replacement for the hand-written axis closures in the apps.

Changed#

  • Screen-reader text follows the chart's presentation (screen-reader text is presentation). The sr-only data table no longer formats numbers with the browser locale and no longer formats dates as ISO strings, but uses the central formatters with the chart presentation; the keyboard announcement of Cartesian charts formats the y-value (previously the raw JS number); the scalar aria-labels of gauge, linear-gauge and bullet format their numbers and percentages centrally. The English prose words in those labels ("of", "zone", "target") are deliberately left untouched — prose translation is a separate i18n follow-up.

Breaking#

  • The formatter re-exports follow the preset API of active-i18n. formatPercent no longer exists; the barrel and utils/format.ts re-export formatFractionAsPercent and formatPercentagePoints (source unit in the name) plus the types NumberFormat/NumberPreset/ PercentPreset. formatNumber takes a closed preset instead of a free Intl.NumberFormatOptions bag.

Changed#

  • Numeric type: 'number' axes without their own formatter or format string get a locale-aware default formatter. Until this change such an axis fell back to d3's tickFormat() with US punctuation, so an nl reader saw 1,234.5 on the axis next to a tooltip showing 1.234,5. Log axes deliberately keep the d3 behaviour (label suppression of non-significant ticks).

  • The internal share, legend, ring and zoom labels of pie, nightingale, radial-bar, treemap, funnel, radar, heatmap, org-chart and the stacked-percentage defaults of bar format through the central presentation layer with the chart locale, instead of toFixed/${v}% with a fixed dot notation. The tooltip, crosshair and split-tooltip defaults have been moved to the same presets.

Breaking (theme)#

  • ChartThemeOverride.success / .warning / .destructive are renamed positive / negative / caution, and they now write --color-chart-positive / -negative / -caution instead of --color-success / --color-warning / --color-destructive.

    Zero consumers in the monorepo: all twenty theme={{…}} call sites (nineteen in tests, one in an admin demo) set only foreground, card or mutedForeground. No compat shim, in line with CLAUDE.md Critical Rule 12.

Changed#

  • This package no longer reads semantic UI colours. --color-success, --color-warning, --color-info and --color-destructive belong to @workspace/active-ui. There they carry a text contrast contract (4.5:1) that does not apply to a chart mark; as long as both roles shared one value, an accessibility retuning on the UI side silently shifted candlestick, OHLC, waterfall and sparkline — and vice versa. No type binds a colour string to a role, so this passed typecheck, lint and the full test suite green.

    Migrated: candlestick-series, ohlc-series, waterfall-series, sparkline, cross-lines, the theme object and two JSDoc locations. The values of the new tokens equal the old ones, so no chart changed colour. See the addendum to ADR-007 and active-ui ADR-013.

  • core/overlays/cross-lines.tsx reads its defaults from ACTIVE_CHARTS_THEME.crossLine instead of from its own literals. Found along the way: the two sources had diverged — the theme object said band.fill: var(--color-muted) while var(--color-warning) was rendered, and the field nobody read was the one the documentation described. The theme object now carries the values that were actually shown (4,4 dashes, opacity 0.1), so there is no visual change.

  • ACTIVE_CHARTS_THEME.colors.success/warning/destructive are renamed positive/negative/caution. These fields had zero readers inside the package, as did the whole crossLine sub-object until the change above.

Fixed#

  • Documentation drift in ARCHITECTURE.md §5.1. The code sample of ACTIVE_CHARTS_THEME diverged from the source in four places: the bare var(--chart-N) form instead of var(--color-chart-N), five series colours instead of ten, a status key that does not exist, and axis fields (tickStroke, labelColor, titleColor, gridStroke) the theme object never had. Replaced by an excerpt from the source.

[4.1.0] - 2026-06-01#

Stacked area charts now stack at the geometry level. Previously <AreaChart stacked> only scaled the y-axis to the cumulative total while every area was still drawn from the plot floor (overlapping). Each stacked area now sits on a per-point baseline — positive segments stack upward from zero, negative segments downward — consistent with stacked-bar mixed-sign semantics (ADR-020). Stroke, markers, path-morph animation and the tooltip hit-test all follow the stacked coordinate. See ADR-023.

Added#

  • Stacked-area geometry (incl. mixed-sign). <AreaChart stacked> (and the low-level area series with stacked: true) renders true stacked bands with a per-point y0 baseline. Mixed-sign data stacks positives up / negatives down from the zero-line. Non-stacked area is unchanged (still anchored at the plot floor). The stacked-baseline source is shared with the renderer and the hit-test, so the tooltip lands on the stacked band rather than the raw value.

Why minor (not major)#

No public API or prop shape changed and all non-stacked area rendering plus all bar/percentage stacking is byte-identical (regression anchors: 2150 tests green, unchanged assertions). The bump above patch (4.1.0, minor) reflects newly-defined, consumer-visible behaviour for stacked area (geometry + the docs claim that area "stacks" is now true — O-2 closed). Additive, not breaking.

[4.0.0] - 2026-05-31#

PNG/SVG/PDF export now reproduces the chart's live colours and fonts: a per-chart theme override and var(--x, fallback) syntax (in colour and font-family) are resolved correctly instead of being lost (AC-A4). See ADR-022.

BREAKING CHANGES#

  • resolveCssVariables(svgClone) → resolveCssVariables(svgClone, scopeElement). The exported helper now requires a second argument: the live, in-DOM element to resolve CSS variables against (the chart's <svg>, which sits inside its theme scope). This is the only correct source — the detached clone cannot resolve its own scoped variables. All in-tree callers (the PNG/SVG/PDF export routes) pass the original svgElement. The major bump is on contract grounds: a public export's signature changed. Consistent with the v3.0.0 precedent (removed public createDefinedAccessor), the major is justified by the public-API change itself — not by the size of the change or the (zero) number of external callers. See migration note below.

Fixed#

  • Export honours the per-chart theme override. CSS variables are now resolved against the chart's own scope element (getComputedStyle(svgElement)) instead of document.documentElement. Because custom properties inherit, a scoped --color-* (set by theme={...}) wins, so the export shows the same colours as the live chart. The PNG background colour is resolved the same way.

  • var(--name, fallback) is honoured. The previous regex mis-parsed the name when a fallback was present (var(--color-foreground, #1f2937) captured color-foreground, #1f2937 as the name and left the reference literal). A balanced-paren parser now splits name/fallback correctly and resolves the fallback recursively (var(--a, var(--b, #fff))).

  • font-family var() is resolved too. Axis labels use font-family: var(--font-sans, …); an unresolved var() there is invalid in a standalone SVG and made text fall back to the default font. font-family is now part of the resolved attributes, so the export is fully var()-free.

Migration#

  • If you call resolveCssVariables directly, pass the chart's live <svg> element as the second argument: resolveCssVariables(clone, svgElement). There is no compatibility default — the scope element is required (an optional default-to-document would silently reproduce the bug). See migration/MIGRATION-NOTES.md.

  • The high-level export API (ChartHandle.exportAsPng/exportAsSvg/exportAsPdf, <Chart>'s export) is unchanged — no consumer action needed there.

Notes#

  • All other behaviour is unchanged. The live-chart rendering and the server-side pdf-worker route (which screenshots the live DOM and never used this resolver) were already correct.

[3.2.0] - 2026-05-31#

Axis tick labels and cartesian data labels now use accurate canvas-backed text measurement: long custom-formatted axis labels no longer overlap, the first/last tick label no longer clips at the plot edge (AC-V5), and data-label collision hiding is based on real label widths instead of a 0.6×fontSize estimate (AC-B3). See ADR-021.

Fixed#

  • Axis tick-label overlap (AC-V5a). On number/log/time x-axes with autoRotate off, the tick count came from a width/70 density heuristic that ignored label width, so long custom-formatted labels overlapped. Tick labels are now thinned by measured width: overlapping labels are dropped, the two domain endpoints are always kept, and the thinning is anchor-aware (a start/end-anchored edge label reserves its true horizontal extent). Short, well-spaced labels and truncating axes (category/ordinal-time) are unchanged.

  • First/last tick-label edge-clip (AC-V5d). The outermost tick labels were middle-anchored and overflowed the plot edge by half their width. The leftmost tick is now start-anchored and the rightmost end-anchored (by pixel position, so reversed axes work). Gated to non-truncating axes, so category/ordinal-time labels keep their middle anchor.

  • Data-label collision accuracy (AC-B3). estimateLabelRect (bar, line, scatter, histogram, waterfall data labels) now measures label width with the shared canvas measurer instead of 0.6×fontSize, so labels are hidden/shown based on their real rendered width.

Changed#

  • New internal utils/measure-text.ts — one canvas-backed measureTextWidth (with a character-count SSR fallback) now serves both the axis label layout and the data-label collision. axis.tsx's former private measurer was promoted into it; no public API changed.

Notes#

  • Minor (not major): no public API/prop shape changed, and the axis/label behaviour only changes where labels actually overlapped or clipped before. Positive-path layouts (short labels, truncating axes, pie/gauge) are unchanged. The bump above patch reflects the consumer-visible layout improvement.

  • Deliberately narrow: the 0.58-ratio utils/text-measurement.ts (pie/gauge/funnel/nightingale/radial-bar/bullet/linear-gauge) and the legend-export.ts canvas measurer were left untouched. Broad measurement consolidation is tracked separately.

[3.1.0] - 2026-05-31#

Mixed-sign / negative values in stacked bar charts now render correctly: positive segments stack up from zero, negative segments stack down from zero (AC-A1). Adds an absolute-sum percentage-normalisation (P1) so stackedPercentage works for mixed-sign data. See ADR-020.

Added#

  • Mixed-sign stackedPercentage (P1 / absolute-sum). When a stackedPercentage stack contains a negative value, each segment renders at |value| / Σ|value| × stackTotal (keeping its sign for direction) and the value-axis is pinned to a symmetric [-stackTotal, stackTotal]. All-positive stackedPercentage data is unchanged — it still uses the consumer-pre-normalised contract (the row-sum gap stays by-design). General auto-normalisation of all-positive raw values remains future work (AC-B2), which will reuse this helper rather than add a second implementation.

Fixed#

  • Stacked bar domain is mixed-sign correct. computeNumericDomain's stacked branch previously pinned min to 0 and used per-row total sums, so negative segments fell outside the domain. It now derives max from the per-row positive-sum and min from the per-row negative-sum (zero-line always in-domain). An all-negative stack now clamps max to 0 and renders downward instead of blank/clipped.

  • Stacked bar baseline accumulates by sign. computeStackBaselineMap now keeps independent positive (up) and negative (down) running totals, so a negative segment stacks below zero on top of preceding negatives instead of being painted above the zero-line. Positive-only stacks are byte-identical to before.

  • Negative-bar data-labels. An outside data-label on a negative bar now hangs below the bar's value-end (with bottom-edge auto-flip to inside) instead of floating above the zero-line detached from the bar. Positive bars are unchanged.

Notes#

  • This is a minor (not major) release: no public API/prop shape changed and all positive-only stacked rendering is pixel-identical. The bump above patch reflects newly-defined, consumer-visible behaviour for mixed-sign stacks.

  • Stacked area is out of scope here and still renders overlapping from the plot floor (the domain fix corrects its axis extent only). Real area-stacking is tracked as a new feature in ROADMAP.md §4.

[3.0.0] - 2026-05-30#

Adds a non-finite-projection guard so a value ≤ 0 on a log axis renders as a gap instead of a broken NaN path. Closes the shared root-cause behind AC-V1 (log-axis NaN coordinates) and AC-V3b (NaN path-tweens). See ADR-019.

BREAKING CHANGES#

  • createDefinedAccessor removed from the public export surface. Its sole role — the defined accessor for gap rendering — is superseded by the projection-aware isDefined predicate returned from createSeriesAccessors ({xAccessor, yAccessor, isDefined}). This is a breaking change to the public contract (hence the major bump on the v1.0.0 stability promise), even though no consumer imported it.

  • Internal: prepareMissingData's second parameter is now the isDefined predicate ((d) => boolean) instead of yKey: string. Not part of the public export surface.

Fixed#

  • Non-finite projections no longer leak NaN/±Infinity coordinates. callScale now returns undefined for a non-finite scale result (e.g. a value ≤ 0 on a scaleLog), honouring the already-documented "undefined-on-rejection" contract of the projection helpers. Previously the NaN passed the typeof === 'number' check and the ?? 0 fallback did not catch it, producing a broken line/area path and silent NaN path-tweens.

  • Every renderer now treats an unprojectable point as a gap/skip, not as pixel 0. Path series (line, area, range-area) break the path; point/rect/ segment series (scatter, box-plot, bar error-bars, range-bar, dumbbell, lollipop, histogram, waterfall) and the overlays (annotations, cross-lines) omit the element. The hit-test (Voronoi) already skipped unprojectable points via its == null guard and is fixed by the central guard alone.

Added#

  • Dev-only diagnostic. A deduped console.warn (per axis-type + value) fires in development when a finite numeric value projects to a non-finite result (e.g. ≤ 0 on a log axis), making the otherwise-silent gap diagnosable. Production rendering is unchanged.

Migration#

  • If you imported createDefinedAccessor, switch to the isDefined predicate from createSeriesAccessors(xKey, yKey, projectX, projectY). (No datyx or geneax consumer imported it; this is contract-hygiene, not a code migration.)

  • No runtime config change is required. Charts on a log axis that previously rendered a broken path through a ≤ 0 value now show a clean gap there.

[2.0.0] - 2026-05-29#

First major since the 2026-05-18 baseline reset. Makes the x-axis tick formatter value type honest per wrapper, so the date-on-category trap is a compile-time error instead of a silent runtime invalid-date label.

BREAKING CHANGES#

  • Axis-type-honest formatter typing. Each Cartesian wrapper now narrows the tick formatter value to its default scale: string for category-default wrappers (BarChart, WaterfallChart, BoxPlotChart, RangeBarChart, DumbbellChart, LollipopChart), Date | string for time-default wrappers (LineChart, AreaChart, RangeAreaChart, CandlestickChart, OhlcChart), and number for number-default wrappers (ScatterChart, Histogram). Consequence: formatDate(v as Date, ...) on a (default) bar chart no longer compiles, because v is string. Fix by pre-formatting a label and using the category axis, or by passing an explicit time type. See MIGRATION-NOTES.md.

  • Dumbbell/Lollipop xAxis is now optional. Both default to category at the type level, resolving the prior "explicit xAxis required" runtime dissonance.

  • The internal CartesianXKeyDiscriminator / CartesianTimeLikeAxis / CartesianCategoryAxis helpers are replaced by CartesianXKeyCategoryDefault / CartesianXKeyTimeDefault / CartesianXKeyNumberDefault (and a narrowed CartesianXKeyTimeOnly). These were not part of the public export surface.

Added#

  • Dev-only runtime guard: a single console.error per dataset when a Date value reaches a category (scaleBand) x-axis, catching type bypasses (as / any / untyped data). Production rendering unchanged.

  • New per-axis-type config types CategoryAxisConfig, TimeAxisConfig, NumberAxisConfig.

Notes#

  • The narrowing is uniform per wrapper, not per branch: opting into a non-default scale via an explicit type does not re-narrow the formatter value. This is a deliberate trade-off to keep (v) => ... inferable without an explicit type (per-branch narrowing makes TypeScript fall back to implicit any). Functionally safe: formatDate accepts both strings and dates. See DECISIONS.md.

[1.8.1] — 2026-05-29#

Documentation#

  • QR-4 — date-on-category-axis guidance. Documents the silent invalid-date trap (a Date xKey on a category/scaleBand axis is stringified to a non-ISO string, so a formatDate(v as Date, …) formatter renders common.placeholder.invalidDate as the literal tick label). No source/type change — the runtime behaviour is unchanged; this release makes the behaviour discoverable.

    • API.md §9.1 — new Default x-axis per chart table (bar-class charts default to category), a formatter value runtime type per axis type table, and a warning block with the two correct patterns.

    • New recipe page recipes/DATE-AXES.md — monthly bars, daily lines, range-comparison snippets.

    • DECISIONS.md — QR-4 (e) recorded as a deliberate non-implementation: a per-axis-type strict formatter-narrowing on AxisConfig would be a breaking change to the public x-axis discriminator (type-level time-like default vs. runtime category default on bar-class wrappers). Continues the v1.8.0 decision to keep the wide AxisTickValue default.

[1.8.0] — 2026-05-26#

Added#

  • Typed axis-formatter signature (QR-4). AxisConfig.formatter and AxisOptions.label.formatter now accept (value: AxisTickValue, ...) => string instead of (value: unknown, ...) => string. AxisTickValue is the union Date | string | number — an honest representation of what the runtime can produce across all scale types. Newly exported:

    • AxisTickValue — base union of all possible tick-value runtime types.

    • TimeAxisTickValue (Date | string) — for 'time' / 'ordinal-time'.

    • NumberAxisTickValue (number) — for 'number' / 'log' / 'sqrt'.

    • CategoryAxisTickValue (string) — for 'category'.

    • AxisFormatter<V extends AxisTickValue = AxisTickValue> — generic formatter-signature alias.

    Consumers can opt into stricter typing by using AxisFormatter<TimeAxisTickValue> etc. when declaring their formatter. The default (without a type argument) stays permissive for all existing call sites.

    Background: during the datyx-v3 FormSection migration two consumer-side bugs surfaced (Number(v) on an ISO string from ordinal-time, (v as Date).getUTCMonth() on a string). The old unknown signature gave no hint about what consumers could expect at runtime. Full rationale in DONE.md §2026-05-26.

Notes#

  • Non-breaking for TypeScript consumers: unknown is contravariant-assignable to AxisTickValue. Concrete narrowing casts such as v as Date keep working (Date ∈ union).

  • Internal axis-rendering call sites (x-axis.tsx, y-axis.tsx) were adjusted with explicit as AxisTickValue casts. The runtime flow is identical — the cast tells TypeScript that the d3-axis callback payload fits within the typed signature.

[1.7.5] — 2026-05-22#

Performance#

  • TR-9 — ChartGeometryContext referential stability. Before this change, <Chart>'s useMemo around buildChartGeometry produced a new ChartGeometry instance on every render whenever one of its deps (options, xScale, yScales, layout) was referentially unstable — which, under real-world consumer-side data instability, cascades through useScales. Consequence: all useChartGeometry() consumers re-rendered, and React.memo on the 13 series renderers + <SvgRenderer> was effectively inactive. Now the geometry bundle is reused through a structural-equality wrapper as soon as its contents are identical to the previous instance — the provider value stays reference-equal, memoized consumers do not re-render. A stability test (S1: unstable upstream + identical contents) proves N → 1 renders per unrelated parent toggle.

Added (internal)#

  • useStableChartGeometry in src/hooks/use-chart-geometry/use-chart-geometry.tsx. A wrapper around buildChartGeometry that compares the geometry bundle field by field with the previous instance and reuses the previous instance when the contents are equal. <Chart> consumes it instead of a direct useMemo(buildChartGeometry, [...]). buildChartGeometry stays pure — the stabilisation lives in the wrapper layer. Not exported from src/index.ts (ADR-009 narrow discipline respected).

  • Type-aware scale-contents compare. isScaleContentEqual compares scale.domain() + scale.range() element-wise. A naive Object.is(a.scale, b.scale) compare would fail under upstream instability because useScales produces new d3-scale instances on every data-ref change. Element comparison via isScaleElementEqual handles the scaleTime edge case (Date instances compared via getTime()); linear/log/sqrt/band/point/ordinal work with Object.is on number/string elements.

  • Safety rail — STABILISE_MAX_AXES = 8. For pathologically large axis sets (>8 y-axes) the compare-cost-versus-re-render-cost trade-off no longer holds; the wrapper then falls back to a plain rebuild without compare. No current chart type reaches this threshold; the rail is a safety valve for future callers.

Changed (internal)#

  • <Chart> uses useStableChartGeometry instead of an inline useMemo(buildChartGeometry, [options, xScale, yScales, layout]). One-line replacement plus import update.

  • Stability test (src/hooks/use-chart-geometry/use-chart-geometry-stability.test.tsx) — 3 new tests that verify the invariant:

    • S1 — unrelated parent re-render with identical-content inputs → 1 render in total (no re-renders through a memoized consumer).

    • S3 (regression guard) — an actual data change → the consumer does re-render (correct behaviour, no stale-data regression).

    • S1 variant — with a memoized upstream the fix also stays stable (no regression versus the previous useMemo chain).

  • ADR-009 supplemented (not superseded) with the new invariant: "provider-value contents-equal ⇒ instance-equal" — see DECISIONS.md § ADR-009.

Notes — what surfaced during execution#

  • Analysis gap: the TR-9 analysis document (removed after completion) §3.1 noted "scale → compare via Object.is, stable as long as useScales is stable upstream". During execution it turned out that S1 (consumer-side data instability) cascades precisely through useScales into scale-ref instability — that was the source of the S1-red-after-fix finding. Walt's stop-moment discipline (report, do not force) led to option (i) — extending isAxisContextEqual with a scale-contents compare — without scope creep beyond TR-9.

  • The stability test also uncovered a test-design bug: ConsumerSpy was initially not wrapped in React.memo, so the measuring instrument itself re-rendered on parent renders regardless of context. With memo around ConsumerSpy the test replicates the production pattern exactly (all 13 production series renderers are memo'd) and measures what we want to measure.

  • Compare cost in practice: for typical charts (1-2 y-axes) ~15-25 primitive Object.is ops + 4-8 element compares per scale. Negligible compared with the re-render work it saves.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors

  • pnpm --filter @workspace/active-charts test — 2109/2109 green (was 2106; +3 stability tests)

  • pnpm --filter @workspace/active-charts lint — 18 warnings (same baseline; motivated eslint-disable react-hooks/refs around previousRef.current access inside useMemo per the React docs)

  • pnpm typecheck (monorepo) — 38/38 tasks green

  • grep "✅" docs/ROADMAP.md — 0 hits

This fully closes Phase 3 (Performance) of the Traycer v2 follow-up. ROADMAP §1 (Traycer v2 follow-up) is empty after this release.

[1.7.4] — 2026-05-22#

Performance#

  • TR-8 — computeWaterfallPositions out of the tooltip hot path. Previously getTooltipExtras called computeWaterfallPositions(allData, ...) on every hover event on a Waterfall chart; that function recomputes the cumulative-position array in O(n) over all steps. With fast mouse movement over large waterfalls (>100 steps) this recomputed on every mousemove frame — wasted work, because the positions are identical between hover events. Now the positions are pre-computed once per chart render in <Chart> via a useMemo over [options.series, preparedData]; getTooltipExtras does an O(1) array lookup. Pure performance refactor — tooltip content pixel-identical.

Changed (internal)#

  • getTooltipExtras signature: the 3rd parameter changes from allData?: readonly ChartDatum[] to waterfallPositions?: readonly WaterfallPosition[]. The caller passes pre-computed positions for the active series instead of raw data. Per §7.14 no dual API is kept (no fallback compute path).

  • useChartTooltip exports buildWaterfallPositionsBySeries + the WaterfallPositionsBySeries type. <Chart> consumes the helper in a useMemo; the result is a Map<seriesIndex, WaterfallPosition[]> passed as the 6th optional argument to handleShowTooltip and as the 5th optional argument to handlePinTooltip. buildTooltipData does the per-series lookup via point.seriesIndex.

  • Scope boundary held. Only the tooltip call site in series-tooltip-extras.ts moves; the other two production callers of computeWaterfallPositions (hooks/use-scales/use-scales.ts for domain computation, core/series-plugins/waterfall-plugin.tsx for hit-testing) are untouched. Both run per render, not per mousemove — no hot-path problem. The parked "computeWaterfallPositions 3-call-site duplication" observation in DECISIONS.md has been updated: no longer "3 call sites with a tooltip hot-path concern", but "2 call sites, both per render, no performance bottleneck".

Test suite#

  • 2 waterfall tests in series-tooltip-extras.test.ts migrated to the new signature — the caller now builds the pre-computed computeWaterfallPositions(...) array and passes it to getTooltipExtras. Assertions identical in substance (same value/cumulative/isTotal expectations). Test name "allData-context" → "pre-computed positions" for readability.

  • 2106/2106 green on identical assertions; no test added or removed.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors

  • pnpm --filter @workspace/active-charts test — 2106/2106 green

  • pnpm --filter @workspace/active-charts lint — 18 warnings (same baseline)

  • pnpm typecheck (monorepo) — 38/38 tasks green — no external consumer affected

  • grep "✅" docs/ROADMAP.md — 0 hits

Phase 3 of the Traycer v2 follow-up is now partly closed (TR-8 done; TR-9 ChartGeometryContext Map reference-equality remains open — requires an analysis phase).

[1.7.3] — 2026-05-22#

Fixed (consumer-visible behavior)#

  • The tooltip stays inside the chart card and follows the cursor — three coupled fixes. v1.7.1's cursor-follow contract (ADR-016) let the tooltip visually exceed the chart area in order to stay close to the cursor; the viewport clamp caught the viewport edge. Visual verification revealed three problems that had to be addressed together:

    • Bug A — Cartesian margin-offset blind spot in the projection. useTooltipPosition computed projVpX = containerRect.left + clampedX + xOff for the viewport clamp. For Cartesian charts (chart.tsx via TooltipPositioner), + marginLeft is added to the rendered position after the hook. The projection missed that translation → the viewport clamp underestimated the actual tooltip position by marginLeft pixels → tooltip overflow on cards at the viewport edge. Present in limited form before v1.7.1 (the same blind spot in the parentElement version), masked by the bounds flip; fully visible after v1.7.1.

    • Bug B — no flip at the card boundary. v1.7.1's "always follow the cursor, the viewport is the only boundary" rule lets the tooltip exceed the chart card. Walt's requirement: the tooltip must NEVER fall outside the chart card. v1.7.3 introduces the card-boundary flip: when the tooltip would exceed the container bounds on its current side AND the other side does fit, it flips to the other side of the cursor (stays with the cursor + inside the card). Conceptually a different mechanism from the bounds flip removed in ADR-016: that one triggered at the plot boundary (smaller than the card) and pulled the tooltip away even where there was room; this one triggers only at the card boundary and flips to follow the cursor on the other side, not away from it.

    • Bug C — first-hover measurement on an empty div. The <TooltipFadeWrapper> between the TooltipPositioner div and the content delays children mounting by one render (FadeWrapper's mounted state starts false and flips via useEffect). The hook's first getBoundingClientRect therefore read 0×0; setMeasured(true) was called anyway → the tooltip got visibility:visible at the default offset before the card/viewport clamp could fire. Subsequent re-measures (on mousemove) did read the real size and corrected it — hence "first hover wrong, subsequent correct". v1.7.3 skips zero-rect measurements and uses a ResizeObserver on the tooltip div to re-measure when the size transitions from 0 to real.

  • Priority rule for a tight card: when the tooltip fits inside the card on neither side (tooltip larger than the card), cursor-follow wins over card bounds — the tooltip stays with the cursor and may stick out slightly beyond the card. The viewport boundary is the absolute hard limit that is never exceeded. Three levels: cursor → card (may be given up) → viewport (never given up).

See ADR-017 for the three coupled decisions and the cross-link to ADR-016 (why the plot-boundary flip had to go then, and why the card-boundary flip is added now — a different mechanism, a different trigger, preserving cursor-follow where possible).

Changed (internal)#

  • useTooltipPosition options: new originX?: number + originY?: number fields (default 0). Cartesian's TooltipPositioner (in chart.tsx) passes originX: marginLeft, originY: marginTop so that the hook's card and viewport projection accounts for the post-hook coordinate-system translation. 11 standalone call sites unchanged (default 0 suffices).

  • Test suite — 2 v1.7.1 tests removed, not reshaped (§7.14 applied consistently: tests that assert removed functionality are deleted, not bent):

    • "does not flip when the cursor is near the right edge of bounds" — asserted v1.7.1's "always follow the cursor up to the viewport boundary", replaced by v1.7.3's card-boundary flip.

    • "does not flip when the cursor is near the bottom edge of bounds" — same reason, Y-axis variant.

  • 6 new tests added: card flip on X, card flip on Y, cursor wins on a tight card, originX/Y in the projection (Cartesian scenario), ResizeObserver re-measure (Bug C reproduction), plus the viewport hard-limit test kept with an adjusted rationale. ResizeObserver is mocked in tests (jsdom does not have it).

[1.7.2] — 2026-05-22#

Changed (internal)#

  • TR-2 — useAnimationCompleteTimer consumed by GaugeChart + TimelineChart. Both charts held the same inline useEffect + setTimeout block that v1.6.2 already extracted into useAnimationCompleteTimer for PieChart + HeatmapChart. Drift-fix per Traycer v2 §11 R-2: the shared hook is now consumed by 4 standalone wrappers. Behaviour identical (same setTimeout duration, same cleanup, same callback-invocation under !enabled).

  • TR-4 — normalizeParentLevelOption(_axisType) parameter removed. The unused _axisType: AxisType | undefined parameter on normalizeParentLevelOption was retained per a JSDoc comment "for API stability and future axis-specific defaults" — but no concrete axis-specific default was planned. Removed per Traycer v2 §11 R-4. Three call-sites (x-axis.tsx, compute-layout.ts ×2, use-auto-axis-formatters.ts) adapted in the same change; AxisType import dropped from utils/normalize-options.ts (no longer referenced after the parameter removal).

  • TR-5 — internal-only symbols dropped from public barrel. Removed from src/index.ts and src/types/index.ts: ANIMATION_TYPE_WHITELIST, EXIT_ANIMATION_TYPES, resolveAnimationTypeForSeries, callScale, packBarPositions, unpackBarPositions. All six are still available as direct imports from their source files (types/animation-types.ts, types/scales.ts, hooks/use-data-transition/use-data-transition.ts) for in-package consumers; only the barrel surface is reduced. Per Traycer v2 §11 R-6: these are internal implementation details (per-series animation constraints, D3-scale polymorphism escape-hatch, animation-transition packing) that external consumers should not depend on. Two in-package callsites (svg-renderer.tsx, series-accessors.test.ts) migrated to direct imports.

Removed#

  • TR-1 — src/hooks/use-dark-mode/ dead folder deleted. The stub file (export {}; + a comment saying "No consumers any more. This file + the use-dark-mode/ folder can go") had been left behind since v1.6.2's useDarkMode removal. No consumers; physically removed per Traycer v2 §11 R-1.

Documentation#

  • TR-3 — README de-Geneax-genericised. "Enterprise chart library for the Geneax platform." → "Enterprise chart library."; the package-structure section's themes/ # Geneax chart theme → themes/ # ACTIVE_CHARTS_THEME (post-MP-9 rename, drift). Per Traycer v2 §11 R-3.

  • TR-6 — core/series-plugins/ added to README package-structure overview. The T2+T9 plug-in system was a major architectural feature (13 Cartesian plug-ins + _shared helpers + registry) but absent from the README's structure table since its introduction. Per Traycer v2 §11 R-7.

  • TR-7 verified, no action. Traycer v2 §11 R-7 flagged colors-prop JSDoc-parity on Scalar charts — re-verification showed LinearGauge (lines 50–55) and BulletChart (lines 54–57) already carry the equivalent JSDoc note that GaugeChart (lines 50–55) carries. Audit drift; no change needed.

Notes#

This release closes Phase 2 (Discipline-Slip Detection) of the post-maturity roadmap from docs/traycer/traycer-v2-output.md (removed after completion). All six hygiene items + the audit-drift verification were bundled in one session per the original effort estimate (~1.5 hours). Test-suite 2103/2103 green on identical assertions, no test adapted. Lint baseline unchanged (18 warnings, well within the 100-cap). Monorepo-wide typecheck 38/38 tasks green — no consumer in apps/ reaches into the removed barrel symbols. Phase 3 performance items (TR-8 memoize computeWaterfallPositions in tooltip path, TR-9 ChartGeometryContext Map reference-equality) remain open on the ROADMAP.

[1.7.1] — 2026-05-22#

Fixed (consumer-visible behavior)#

  • The tooltip now consistently follows the cursor on all 24 chart types. Before v1.7.1 (ADR-014 default #2: "edge-clamp left/top: always on") the tooltip flipped to the other side of the cursor as soon as its projected position would fall outside the chart bounds. On edge-to-edge chart types (treemap, funnel, sunburst, nightingale, radial-bar, sankey, heatmap), where the cursor is regularly close to the bounds, that constantly broke the cursor-follow agreement — treemap drill-down was visually always broken because of it, funnel intermittently. On Cartesian types (bar/line/area/scatter/etc.) the same happened only rarely because the axis margins keep the cursor away from the bounds edge: the same rule violation, just triggered less often. The bounds flip + bounds edge clamp has been removed; the tooltip now follows one rule — follow the cursor, the viewport clamp keeps it on screen. See ADR-016.

  • The viewport clamp now uses the correct positioning ancestor as its anchor. Before v1.7.1 the QR-7 viewport clamp read el.parentElement.getBoundingClientRect() — which resolved to the <TooltipFadeWrapper> div (a 0×0 box that sits below the chart SVG in document flow), not to the actual positioned ancestor. The resulting viewport projection was offset by roughly the chart height from the actual tooltip position; viewport overflow was wrongly detected or missed. Masked as long as the bounds flip often kept the tooltip inside the plot area; it would have become visible after removing that flip. The hook now takes a required containerRef in its options object — the caller explicitly passes the positioning ancestor the viewport clamp computes against. All 12 call sites migrated in the same session (one API, no fallback path — §7.14). See ADR-016.

Changed (internal)#

  • useTooltipPosition signature: the options parameter is now required, with containerRef: RefObject<HTMLElement | null> as a required field. Before v1.7.1 the whole options object was optional; the anchorClamp flag stays optional within the object. Not publicly exported (internal to @workspace/active-charts), so no breaking change for consumers — only package-internal call sites affected.

  • Test suite — 3 flip-asserting tests removed, not reshaped. They tested functionality that deliberately disappears (per §7.14: do not "rescue" a test by asserting something else when the underlying behaviour has changed). Replaced by three new viewport-clamp tests that verify the cursor-follow agreement + the correct containerRef anchor. Removed tests, for traceability:

    • "flips to the left when first hover is near the right edge" (was use-tooltip-position.test.tsx:116)

    • "flips up when first hover is near the bottom edge" (was use-tooltip-position.test.tsx:164)

    • "clamps to left edge when mouse is already past the left boundary" (was use-tooltip-position.test.tsx:188)

[1.7.0] — 2026-05-22#

Changed (consumer-visible behavior)#

  • QR-5 (revised) — Path-C rect-bounded hit-test for Histogram + Waterfall. Before v1.7.0 both types fell back to a voronoi-only hit-test (Path B), which is less precise for rect-shaped elements — a hit now registers anywhere within the whole bin rect (Histogram) or step rect (Waterfall) instead of only near the centre. Path B remains additive as a fallback where the rect does not hit (consistent with the bar plugin since T2+T9-v2).

Added (internal)#

  • computeHistogramRects + computeWaterfallRects in src/core/series-plugins/_shared.ts. Shared rect-geometry helpers for Histogram + Waterfall — one source of rect bounds, shared by the renderer and the Path-C hit-test (the same philosophy as T1-v2's computeBarGeometry for bar; no drift possible).

    • computeHistogramRects: input {bins, binValues, valueAxisDomain, projectX, projectY} → BarRectPosition[]. Math identical to the pre-v1.7.0 inline computation in HistogramSeries: barX = min(x0, x1) + barWidth = abs(x1 - x0), barY = min(valuePos, baselinePixel) + barHeight = abs(baselinePixel - valuePos) via resolveBaseline.

    • computeWaterfallRects: input {data, positions, xKey, bandwidth, projectX, projectY} → BarRectPosition[]. Math identical to the pre-v1.7.0 inline computation in WaterfallSeries: pyTop = projectY(max(y0,y1)) + barHeight = max(abs(pyBottom - pyTop), 1) (minimum 1px for zero-value steps).

Changed (internal)#

  • HistogramSeries uses computeHistogramRects instead of an inline computation. The renderer + the histogram plugin's Path C now share one source of bin-rect bounds.

  • WaterfallSeries uses computeWaterfallRects instead of an inline computation. The renderer + the waterfall plugin's Path C now share one source of step-rect bounds. (pyTop/pyBottom for connector-Y and label-Y are still computed inline from pos.y0/y1 — those derived positions are not part of the rect bound itself.)

  • histogram-plugin.tsx + waterfall-plugin.tsx now emit BarRect[] alongside FlatPoint[]. Path C is additive — the Path B fallback is unchanged.

Scope revision (vs the original QR-5)#

The roadmap text before the step-0 inventory suggested "QR-5 rollout to 4 types: RangeBar/Histogram/Waterfall/BoxPlot — mechanical rollout of a proven pattern". The step-0 inventory refuted that assumption:

TypePre-v1.7.0 statusQR-5 outcome
HistogramPath B via emitPadBFlatPointsPath C added via shared helper
WaterfallPath B via emitPadBFlatPointsPath C added via shared helper
RangeBarNo hit-test (deliberate Walt decision T2+T9-v2 §4.2.1: "Gantt-like use does not need flatpoint emission")Deliberately kept excluded — parked as a loose observation
BoxPlotNo hit-test (deliberately no yKey shape; "no yKey; uses min/q1/median/q3/max instead")Deliberately kept excluded — parked as a loose observation

Adding Path C to RangeBar/BoxPlot would reopen deliberate earlier exclusions, not tidy things up for external use. Just as the OHLC hit-test asymmetry is parked, RangeBar + BoxPlot deliberately stay without a hit-test. An external consumer does not run into a loose end here — those types work as designed.

computeWaterfallPositions call duplication — an honest note#

The observation parked before v1.7.0 (from T3 phase 2): "three call sites each compute the same running totals". The step-0 analysis showed that this is in fact three callers of one shared function (the function itself has long lived in types/waterfall-series-options.ts), not three duplicate implementations. A real "3 → 1" consolidation would mean extending the geometry context with pre-computed positions per series (data + cache in buildChartGeometry). That is a substantial geometry-shape change outside the QR-5 scope.

What v1.7.0 did do:

  • Path C in the waterfall plugin uses the same computeWaterfallPositions — no new copy. Effectively: 4 callers of 1 function (was 3); no deterioration and no new duplication.

  • computeWaterfallRects accepts pre-computed positions as input, so the helper itself does not call computeWaterfallPositions — the caller decides whether/how to memoize.

The observation stands: a future session can extend the geometry context with pre-computed positions if the performance concern ever becomes demonstrable. Today computeWaterfallPositions is linear and not expensive enough to justify the scope extension.

Drift test pixel-identical#

Both renderer migrations are pixel-identical for existing behaviour. 2102/2102 tests green on identical assertions, no test adjusted. The deliberate behaviour change is the added Path-C hit-test on Histogram + Waterfall (more precise rect hit detection).

Stop moments — outcome#

  • Stop moment 1 (rect-geometry helper does not deliver renderer bounds): not triggered. computeHistogramRects output identical to the pre-v1.7.0 HistogramSeries inline computation; computeWaterfallRects likewise.

  • Stop moment 2 (computeWaterfallPositions call sites diverge after "consolidation"): not triggered in the adjusted sense (no consolidation to 1 caller; see the honest note above).

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2102/2102 green on identical assertions.

  • pnpm --filter @workspace/active-charts lint — 18 warnings (same baseline; temporarily 19 after the plotHeight prop became unnecessary through the helper extraction, back to 18 after a destructuring cleanup).

  • pnpm turbo run typecheck (monorepo) — 28/28 tasks green.

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files (3 changed + 0 new)#

Changed:

  • package.json — 1.6.3 → 1.7.0

  • src/core/series-plugins/_shared.ts — computeHistogramRects + computeWaterfallRects + HistogramBinLike + WaterfallPositionLike added

  • src/core/renderer/series/histogram-series.tsx — inline computation replaced by computeHistogramRects; unused plotHeight prop removed from destructuring

  • src/core/renderer/series/waterfall-series.tsx — inline computation replaced by computeWaterfallRects

  • src/core/series-plugins/histogram-plugin.tsx — Path-C emit added

  • src/core/series-plugins/waterfall-plugin.tsx — Path-C emit added

Bump choice#

Minor (1.6.3 → 1.7.0) — a consumer-noticeable behaviour improvement (more precise hit detection on rect-shaped Histogram/Waterfall).

Remaining open topics after v1.7.0#

  • QR-2 — Parity-test category rollout (1 session, requires scope clarification)

  • QR-3 — Round-4 live smoke check (2-3 sessions)

  • Section 3 (MP-*) — only relevant for external npm publication

  • TEST-1 — UPO chart snapshot regression (1 session, not urgent)

  • Section 5/6 — Future chart types + features (growth)

New + existing parked observations (no open trigger):

  • RangeBar hit-test absence (deliberate choice T2+T9-v2 §4.2.1) — newly parked

  • BoxPlot hit-test absence (deliberate choice, no yKey) — newly parked

  • computeWaterfallPositions call duplication — stays parked; real consolidation requires a geometry-shape extension

  • OHLC hit-test asymmetry — parked, unchanged

  • C1 (AxisContext extension) — parked, unchanged

  • useVoronoi → useHitTest rename — parked, unchanged

  • BarRect axis-id fields — parked, unchanged

[1.6.3] — 2026-05-22#

Added (internal)#

  • QR-10 remainder — <SrOnlyAnnouncement> in src/core/accessibility/sr-only-announcement.tsx. A shared component for the keyboard-nav screen-reader live region. Before v1.6.3 this was copy-paste in 11 standalone wrappers (a 22-line block per wrapper, ~242 lines in total). After v1.6.3: one component, a 4-line call site per wrapper. API:

    TSX
    <SrOnlyAnnouncement testId="pie-kbd-announcement">  {kbd.announcement}</SrOnlyAnnouncement>

    API shape choice: testId prop + children. All 11 call sites had exactly identical structure (role="status" + aria-live="polite" + aria-atomic="true" + an identical sr-only clip style); the only differences were the data-testid value and the content expression. Children cleanly cover the dynamic content; testId covers the unique per-chart name. Hardcoded a11y attributes + clip style in the component (stop-moment-3 discipline: no priority/assertive props until a consumer needs them).

Changed (internal)#

  • 11 standalone wrappers migrated: HeatmapChart, PieChart, SankeyChart, SunburstChart, TreemapChart, TimelineChart, RadialBarChart, NightingaleChart, RadarChart, FunnelChart, OrgChart. Per wrapper:

    • 22-line inline <div> block replaced by a 4-line <SrOnlyAnnouncement> call

    • Import of SrOnlyAnnouncement added next to the existing core/accessibility/ imports

    Pixel-identical: same a11y attributes (role="status", aria-live="polite", aria-atomic="true"), same sr-only clip style, same data-testid values, same content. 2102/2102 tests green, no test adjusted.

Code size#

LocationPre-v1.6.3Post-v1.6.3
11 wrappers × inline 22-line block~242 lines copy-paste11 wrappers × 4-line call (~44 lines)
sr-only-announcement.tsx (new)—~55 lines (incl. JSDoc)

Net duplication reduction: ~242 lines of copy-paste → 55 lines of shared component + 44 lines of call sites = ~99 lines. Saving ~143 lines, plus a single source of truth for the a11y pattern.

Stop moment — outcome#

Stop moment (call sites split between API shapes, or the extraction changes sr-only output): not triggered. All 11 call sites were fully identical in structure and content pattern; testId prop + children covered them 1-to-1 without any forcing.

QR-10 fully closed#

QR-10 had three sub-triggers:

  1. useAnimationCompleteTimer ✓ (v1.6.2)

  2. useDarkMode — reconsidered + removed (v1.6.2); the inline isDark fallback was cleaned up as well

  3. <SrOnlyAnnouncement> ✓ (v1.6.3, this entry)

No open QR-10 sub-triggers remain.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2102/2102 green on identical assertions, no test adjusted.

  • pnpm --filter @workspace/active-charts lint — 18 warnings (same baseline).

  • pnpm turbo run typecheck (monorepo) — 28/28 tasks green.

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files (12 changed + 1 new)#

New:

  • src/core/accessibility/sr-only-announcement.tsx

Changed:

  • package.json — 1.6.2 → 1.6.3

  • 11 standalone wrappers: heatmap-chart, pie-chart, sankey-chart, sunburst-chart, treemap-chart, timeline-chart, radial-bar-chart, nightingale-chart, radar-chart, funnel-chart, org-chart (each: import added + 22-line block replaced by a 4-line call)

Remaining open topics#

  • QR-2 — Parity-test category rollout (1 session, requires scope clarification)

  • QR-3 — Round-4 live smoke check (2-3 sessions, browser verification)

  • QR-5 — Path-C rect hit-test rollout to RangeBar/Histogram/Waterfall/BoxPlot (1-2 sessions)

  • Section 3 (MP-*) — only relevant for external npm publication

  • TEST-1 — UPO chart snapshot regression (1 session, not urgent)

  • Section 5/6 — Future chart types + features (growth on request)

Plus parked observations (C1, OHLC hit-test asymmetry, useVoronoi → useHitTest rename, computeWaterfallPositions 3-call-site duplication, BarRect axis-id fields).

[1.6.2] — 2026-05-22#

Added (internal)#

  • QR-10 — useAnimationCompleteTimer in src/hooks/use-animation-complete-timer/use-animation-complete-timer.ts. A shared hook that absorbs the onAnimationComplete?.({ type: "load" }) pattern: either fire immediately (animation disabled) or after a setTimeout matched to the keyframe duration, with cleanup. Before v1.6.2 this lived as an identical useEffect block in HeatmapChart and PieChart. The hook respects the pre-v1.6.2 stable-callback convention (onComplete not in the deps — the caller passes a stable reference).

Removed (internal)#

  • Inline isDark detection + useDarkMode fallback in HeatmapChart. Before v1.6.2 HeatmapChart had its own useState + useEffect with MutationObserver + matchMedia to detect dark mode, plus a hardcoded _DARK/_LIGHT colour pair as a fallback when var(--color-chart-heatmap-min/max) did not resolve.

    Reconsidered during the v1.6.2 session: HeatmapChart was the only consumer of that detection, and the fallback branch was never reached within Geneax (Tailwind v4 + shadcn tokens always resolve the variables). The inline detection existed strictly for standalone consumers without Geneax CSS — a scenario that MP-8 (default-colors.css in ROADMAP §3) is supposed to solve at package level.

    Removed:

    • isDark state + useEffect with MutationObserver/matchMedia in HeatmapChart

    • isDark prop on HeatmapChartInnerProps and HeatmapSeriesProps

    • Two isDark={isDark} pass-through locations

    • DEFAULT_COLOR_RANGE_DARK constant in heatmap-series.tsx (_LIGHT remains as the only sensible default without a dark/light condition)

    • The isDark ? [...] : [...] fallback branch in resolvedColorRange

    HeatmapChart now relies entirely on CSS variables, consistent with all 23 other chart types.

  • useDarkMode hook stub (src/hooks/use-dark-mode/use-dark-mode.ts) — introduced earlier in this session as a duplication-reduction extract, but the reconsideration showed that HeatmapChart was the only consumer. The file has been reduced to a stub (export {}) with a deprecation comment; the folder + file may be physically removed by Walt.

Changed (internal)#

  • HeatmapChart + PieChart consume useAnimationCompleteTimer instead of inline useEffect blocks. Identical behaviour (drift test pixel-identical: 2102/2102 tests green, no test adjusted). Two dead useEffect imports cleaned up.

Documentation — ROADMAP §2 table drift fix#

  • QR-7 struck through in the §2 table — handled in v1.5.0 (T4), but the table still said "open". Status drift repaired.

  • QR-5 note extended: the status after T2+T9-v2 made explicit. The 4 non-bar rect types (RangeBar, Histogram, Waterfall, BoxPlot) do go through the capability pattern but emit no barRects — Path C stays bar-only. The actual rollout fell outside the T2+T9-v2 scope; QR-5 remains an open mini-workstream (no autonomous scope).

Code size#

LocationPre-v1.6.2Post-v1.6.2
HeatmapChart useEffect block for onAnimationComplete~10 lines6 lines (useAnimationCompleteTimer call)
PieChart likewise~10 lines6 lines
HeatmapChart inline dark-mode detection + fallback~30 lines (useState+useEffect+fallback branch)gone
HeatmapSeries dark fallback (DEFAULT_COLOR_RANGE_DARK + isDark choice)~6 linesgone
use-animation-complete-timer.ts (new)—~35 lines

Net duplication reduction:

  • onAnimationComplete: 2 inline implementations → 1 shared hook

  • isDark: 30+6 lines of detection+fallback gone, no replacement — the standalone concern moves to MP-8 (default-colors.css), where it belongs. HeatmapChart's handling of theme awareness is now consistent with the other 23 chart types.

Pixel-identical#

Both extractions are purely mechanical refactors without behaviour change. 2102/2102 tests green on identical assertions, no test adjusted. Lint back to 18 warnings (same baseline as before T6.2).

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2102/2102 green on identical assertions.

  • pnpm --filter @workspace/active-charts lint — 18 warnings (same baseline).

  • pnpm turbo run typecheck (monorepo) — 28/28 tasks green.

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files (6 changed + 1 new + 1 stub)#

New:

  • src/hooks/use-animation-complete-timer/use-animation-complete-timer.ts — shared hook

Stub (to be removed):

  • src/hooks/use-dark-mode/use-dark-mode.ts — deprecation stub (export {}); the folder + file can go

Changed:

  • package.json — 1.6.1 → 1.6.2

  • src/components/heatmap-chart/heatmap-chart.tsx — useAnimationCompleteTimer consumed, dead useEffect import cleaned up, isDark state + useDarkMode import + fallback branch removed, isDark prop pass-through to inner + series removed

  • src/components/pie-chart/pie-chart.tsx — useAnimationCompleteTimer consumed, dead useEffect import cleaned up

  • src/core/renderer/series/heatmap-series.tsx — isDark prop removed from interface + destructuring; DEFAULT_COLOR_RANGE_DARK removed; _LIGHT renamed to DEFAULT_COLOR_RANGE (single pair)

  • docs/ROADMAP.md — QR-7 struck through, QR-5 note extended

QR-10 — two items completed + one reconsidered + one skipped#

QR-10 contained three items in the roadmap. Outcome of this session:

  • useAnimationCompleteTimer — completed (shared hook, two consumers)

  • useDarkMode — reconsidered during the session: the extraction was not a real duplication reduction (one consumer, one forced fallback route). The inline detection + fallback have been removed entirely; HeatmapChart now relies on CSS variables like the other 23 charts. Standalone concern moved to MP-8.

  • <SrOnlyAnnouncement> — skipped (autonomous-session discipline; the component API choice requires Walt's input). Remains open in §2.

Remaining open topics (after this session)#

  • QR-2 — Parity-test category rollout (1 session, requires scope clarification)

  • QR-3 — Round-4 live smoke check (2-3 sessions, browser verification)

  • QR-5 — Path-C rect hit-test rollout to 4 types (1-2 sessions, stays open)

  • QR-10 (remainder) — <SrOnlyAnnouncement> component (0.5 session, API choice)

  • Section 3 (MP-*) — only relevant for external npm publication

  • TEST-1 — UPO chart snapshot regression (1 session, not urgent)

  • Section 5/6 — Future chart types + features (growth on request)

No architecture debt remains open. All remaining work is opportunistic, conditional, or growth.

[1.6.1] — 2026-05-22#

Changed (internal)#

  • T7-v3-mini — FlatPoint.xAxisId/yAxisId populated in all producer paths. Since T7-v1 these fields have been optional on the FlatPoint type, but no producer populated them. T7-v3-mini fills them with geometry.axes.x.id (xAxisId) and the resolved yAxisId (already available locally in each plug-in).

  • The step-0 report corrected the roadmap claim:

    • 5 FlatPoint emit sites, not "10+":

      • _shared.ts emitPadBFlatPoints — 1 shared emit, covering 6 series types (line, area, scatter, histogram, waterfall, candlestick)

      • bar-plugin.tsx — 1 Path-B emit

      • lollipop-plugin.tsx — 1 emit

      • dumbbell-plugin.tsx — 2 emits (low + high)

    • No "voronoi fall-through" — T2+T9-v2 (v1.2.0) cleaned that up; useVoronoi has since been a thin dispatch shell that emits only via plug-ins. The roadmap claim "10+ plug-in emit paths + voronoi fall-through" reflected the state before T2+T9-v2.

Pixel-identical#

FlatPoint.xAxisId/yAxisId were read by no consumer at all — this is purely additive data. 2102/2102 tests green on identical assertions, no test changed.

Observation outside the T7-v3-mini scope#

BarRect (Path C, bar plugin) has no xAxisId/yAxisId fields in its type definition — only FlatPoint has them. T7-v3-mini only touches FlatPoint. If future consumers (a multi-axis feature) want to read BarRect's axis id, a type extension is needed. Not critical; parked observation.

Stop moment — outcome#

Stop moment (axis id not cleanly available, or populating it changes behaviour): not triggered. All 5 emit sites already had the yAxisId resolved locally; xAxisId is available everywhere via geometry.axes.x.id; no test broke.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2102/2102 green on identical assertions, no test adjusted.

  • pnpm --filter @workspace/active-charts lint — 18 warnings (same baseline).

  • pnpm turbo run typecheck (monorepo) — 28/28 tasks green.

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files (5 changed)#

Changed:

  • package.json — 1.6.0 → 1.6.1

  • src/core/series-plugins/_shared.ts — emitPadBFlatPoints adds xAxisId/yAxisId (covers 6 series types)

  • src/core/series-plugins/bar-plugin.tsx — Path-B flatpoint

  • src/core/series-plugins/lollipop-plugin.tsx

  • src/core/series-plugins/dumbbell-plugin.tsx — low + high (2 emits)

T7 architecturally fully closed#

With T7-v3-mini, T7 is fully complete (all four sub-triggers: v1, v2, v2.1, v3). And with it, section 1 of the roadmap — architecture debt — is complete as a whole:

  • T7 (chart-context shape): v1.0.4 + v1.1.0 + v1.1.1 + v1.4.0 (Finding A via T3) + v1.6.1 (FlatPoint population)

  • T2+T9 (SeriesTypePlugin): v1.1.2 + v1.2.0

  • T1 (baseline + bar geometry): v1.3.0 + v1.3.1

  • T5 (axis config + use-scales): v1.3.2 + v1.3.3

  • T3 (tooltip context flow): v1.4.0 + v1.4.1

  • T4 (tooltip positioning): v1.5.0

  • T6 (defaults policy): v1.6.0

What remains is not debt#

  • Loose observations (no open trigger): C1 (AxisContext extension), OHLC hit-test asymmetry, useVoronoi → useHitTest rename, computeWaterfallPositions 3-call-site duplication, BarRect axis-id fields

  • QR items: QR-2 (interactivity discoverability), QR-3 (zoom controls), QR-10 (... ) — not debt, opportunistic maintenance

  • Conditional branches: MP-* topics (npm-publishing prerequisites — only relevant for external release), Future chart types, Future features, TEST-1

  • Per-package test roadmap: active maintenance, not debt

[1.6.0] — 2026-05-22#

Changed (consumer-visible behavior)#

  • T6 — tooltip on by default + active point coupled to tooltip-enabled. Two coupled changes that make the hover experience coherent:

    1. normalizeTooltipOption on by default: undefined → {enabled: true}. Before T6 it was undefined → undefined (off). Charts without an explicit tooltip prop now show a tooltip on hover. Consumers who do not want a tooltip use tooltip={false}.

    2. Active point coupled to isTooltipEnabled: the active-point circle now renders only when the tooltip is on. Before T6 the circle rendered unconditionally on every voronoi hit; the "floating circle without a tooltip" surprise is resolved. tooltip={false} turns both off; the default config shows both.

  • Not touched: the other nine normalize*Option helpers. KeyboardNavigation deliberately stays on by default (accessibility priority); DataLabel stays per chart type (the chart wrapper decides); the seven other opt-in features (Legend, Crosshair, Zoom, Navigator, Sync, ContextMenu, ParentLevel) stay opt-in.

Step-0 report — roadmap correction#

The pre-T6 ROADMAP text claimed "five normalize*Option helpers, uniformly off by default". The step-0 inventory showed:

  • Ten helpers, not five

  • Three groups (eight truly opt-in, KeyboardNavigation deliberately on by default, DataLabel per chart type)

  • The "active point without tooltip" surprise was not in a uniform policy but in a specific coupling mismatch in handleHover: setActivePoint(point) was called unconditionally, tooltip.handleShowTooltip conditionally

T6 thereby resolves the actual mismatch (the coupling) and delivers the coherent default (tooltip on by default) as one cohesive fix. Walt's Option 1 over Option B (flip only) or Option C (coupling only): the flip and the coupling together give every chart without configuration the complete, expected hover experience.

Migration set#

CategoryCountEffectMigration needed?
Production dashboard (no tooltip prop)2 (session-stats-chart.tsx, login-stats-chart.tsx in admin)Default-on tooltip on hoverNo — desired behaviour
Production explicit (analytics + heatmap)4No changen/a
Test-chart demos~92 chart usages across 3 view filesMost already had an explicit tooltip; charts without one get default-on in a demo contextNo — exactly the demo's goal
11 standalone wrappers (internal)n/aFlip automatically via normalizeTooltipOptionNo — automatic
Active-charts unit tests2102 testsNo test asserted normalizeTooltipOption(undefined) === undefinedNo — no test adjusted

No tooltip={false} migrations needed in production code or test charts. The affected set was large (in files) but fully unambiguous (all affected charts want default-on).

Stop moments — outcome#

  • Stop moment 1 (migration set large or ambiguous): not triggered. The set is large but not ambiguous — all affected categories want default-on.

  • Stop moment 2 (default-on tooltip renders meaninglessly): not triggered. All affected charts (2 dashboard charts + test-chart demos) render meaningful default tooltips.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2102/2102 green on identical assertions. No test adjusted for the flip.

  • pnpm --filter @workspace/active-charts lint — 18 warnings (same baseline).

  • pnpm turbo run typecheck (monorepo) — 28/28 tasks green.

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files (2 changed)#

Changed:

  • package.json — 1.5.0 → 1.6.0

  • src/utils/normalize-options.ts — normalizeTooltipOption: undefined → {enabled: true} (was undefined)

  • src/components/chart/chart.tsx — handleHover: setActivePoint(point) placed under the isTooltipEnabled condition; the active-point circle's render condition extended with isTooltipEnabled &&

T6 architecturally closed#

No open T6 sub-triggers remain. Roadmap text corrected to the actual state (ten helpers, three groups, the coupling finding).

Remaining open T topics#

  • T7-v3-mini — populate FlatPoint.xAxisId/yAxisId (~1 session, mechanical, not critical)

Loose observations: OHLC hit-test asymmetry, useVoronoi → useHitTest rename, computeWaterfallPositions 3-call-site duplication. C1 parked.

All significant T topics completed (T7-v1+v2+v2.1 + T2+T9 + T1 + T5 + T3 + T4 + T6). Only the mechanical mini-trigger T7-v3 remains.

[1.5.0] — 2026-05-22#

Changed (consumer-visible behavior)#

  • T4 — tooltip positioning unified into one hook (useTooltipPosition in src/hooks/use-tooltip-position/). Before T4 tooltip positioning lived in two parallel implementations: the TooltipPositioner component in chart.tsx (~86 lines, for 13 Cartesian types via <Chart>) and the standalone useTooltipPosition hook (67 lines, for 11 non-Cartesian types). After T4: one hook with an anchorClamp?: boolean option absorbs the deliberate differences; TooltipPositioner stays in chart.tsx as an internal thin wrapper (~25 lines) that uses the hook internally for rendering properties (z-index, pointer-events, position:absolute). Not exported — an implementation detail of <Chart> (U1 shape — see docs/DECISIONS.md § ADR-014).

    Three consumer-visible behaviour changes (Walt's UX choices, §8):

    1. Default Y offset unified to +TOOLTIP_GAP (tooltip below the mouse) for all 24 chart types. Before T4 the 11 standalones had -TOOLTIP_GAP (tooltip above the mouse); they now move to the Cartesian default. The 13 Cartesian charts are unchanged.

    2. Left/top edge clamp on by default for all 24 chart types. Before T4 only the standalone hook had this clamp; the 13 Cartesian charts via <Chart> could let tooltips stick out of their plot bounds. Latent bug fixed — no functional loss, only preventing off-bounds rendering.

    3. QR-7 — viewport edge clamp on by default for all 24 chart types. When hovering close to the viewport edge, the tooltip is systematically pushed back into [0, window.innerWidth/Height]. Before T4 this clamp was missing in both implementations; off-screen tooltips with narrow viewports or charts close to the viewport boundary. QR-7 handled.

Preserved (deliberate differences — not unified away)#

  • Finding B anchor clamp (Cartesian only) — Path-C off-plot anchor protection from v9.0.0. Preserved via the anchorClamp: true option that <Chart>'s wrapper passes to the hook. Standalones leave the option off by default (no Path-C equivalent).

  • Coordinate system — Cartesian computes in plot coordinates (after the margin offset in the wrapper), standalones in container coordinates. Preserved — no unification.

  • Bounding area — plotWidth/Height for Cartesian via the wrapper, containerWidth/Height for standalones. Preserved.

  • Rendering properties (z-index, pointer-events, absolute positioning) — live in the TooltipPositioner wrapper; standalones render themselves with direct control. Preserved.

Drift test pixel-identical for the deliberately preserved differences#

Stop moment 1 (Finding B does not absorb cleanly): not triggered. The anchorClamp: true option absorbs the Path-C protection cleanly without shape strain; the hook is unaware of the concept and applies the clamp only when explicitly asked.

Stop moment 2 (behaviour change outside the regression checklist): not triggered. All behaviour changes fell within the §9 checklist (default Y-offset shift for standalones; edge clamp added for Cartesian; viewport clamp added for all 24).

Existing tests stayed green on identical assertions — 2102/2102. The 5 existing useTooltipPosition tests test flip-left, edge clamp, and flip-up; all four keep working under the new default Y offset because the flip condition (anchor + GAP + size > bounds) is symmetric for a +GAP/-GAP default — the flip outcome (tooltip on the other side of the mouse) is the same.

Code size#

LocationPre-T4Post-T4
chart.tsx TooltipPositioner~86 lines (own flip logic, anchor clamp, useLayoutEffect, useState)~25 lines (thin wrapper, hook call + ref merge + style)
use-tooltip-position.ts67 lines (4-arg signature)~115 lines (5-arg with options + Finding B + QR-7 viewport clamp)
Total positioning layer~153 lines in two implementations~140 lines in one implementation + one thin wrapper

Plus 11 standalone wrappers: no call-site changes — the 4-arg API is backwards-compatible (no options = anchorClamp: false default).

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2102/2102 green on identical assertions; no test adjusted.

  • pnpm --filter @workspace/active-charts lint — 18 warnings (same baseline).

  • pnpm turbo run typecheck (monorepo) — 28/28 tasks green.

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files (1 central change + 1 consumer change)#

Changed:

  • package.json — 1.4.1 → 1.5.0

  • src/hooks/use-tooltip-position/use-tooltip-position.ts — extended with the anchorClamp option, default Y offset unified to +TOOLTIP_GAP, QR-7 viewport clamp added via parentElement.getBoundingClientRect()

  • src/components/chart/chart.tsx — TooltipPositioner rewritten as a thin wrapper around useTooltipPosition; old useLayoutEffect/useState/TOOLTIP_GAP imports cleaned up

Not changed (backwards-compatible 4-arg API):

  • 11 standalone wrappers (treemap-chart.tsx, sunburst-chart.tsx, sankey-chart.tsx, radial-bar-chart.tsx, radar-chart.tsx, pie-chart.tsx, org-chart.tsx, nightingale-chart.tsx, heatmap-chart.tsx, funnel-chart.tsx, timeline-chart.tsx)

T4 architecturally closed#

One positioning implementation for all 24 chart types. QR-7 handled (⊆ T4). No open T4 sub-triggers remain.

Remaining open T topics#

  • T6 — Defaults-policy review (~1 session, standalone)

  • T7-v3-mini — populate FlatPoint.xAxisId/yAxisId (~1 session, mechanical, not critical)

Loose observations: OHLC hit-test asymmetry, useVoronoi → useHitTest rename, computeWaterfallPositions 3-call-site duplication. C1 parked.

[1.4.1] — 2026-05-22#

Added (internal)#

  • T3 phase 2 — getTooltipExtras in src/utils/series-tooltip-extras.ts. A record/utility mapping that supplies the tooltip extras per series type (type-specific fields on top of the standard yKey emit). Outside the T2+T9 contract — Option B (see docs/DECISIONS.md § ADR-013). Fits the record boundary that T2+T9-v1 drew and T5-v2 reconfirmed (next to getSeriesYKeys, defaultApproximationForType, animation-types).

Changed (internal)#

  • Two inline branches in useChartTooltip → one record call. Before T3-v2 useChartTooltip had an if (seriesOpt?.type === "candlestick") branch (lines 214-233) + an if (seriesOpt?.type === "waterfall") branch (lines 238-254). After T3-v2: one getTooltipExtras(seriesOpt, point, allData) call (~3 lines), the result spreads into the ohlc?/waterfall? fields on SingleTooltipData. Shotgun-surgery pattern removed from the tooltip layer.

    Pixel-identical for waterfall + candlestick: 2102/2102 tests green on identical assertions, no test adjusted.

  • OHLC brought along — asymmetry resolved (consumer-visible). Before T3-v2 seriesOpt?.type === "candlestick" explicitly excluded ohlc; ohlc tooltips showed only the yKey emit even though the type has the same 4-key shape (openKey/highKey/lowKey/closeKey) as candlestick. After T3-v2 ohlc shares the candlestick branch in getTooltipExtras — the same extraction logic, no separate ohlc branch (that would introduce new duplication).

    Broader context: this is the same category of inconsistency as the OHLC hit-test observation from T2+T9-v2 (ohlc systematically treated less richly than candlestick because of a historical parity lag). T3 phase 2 resolves the tooltip asymmetry; the hit-test asymmetry stays a loose observation — a different layer, not touched here.

Drift test pixel-identical#

Waterfall and candlestick produce identical tooltip output before and after the refactor. Drift test confirmed via the existing test suite (2094 pre-T3-v2 tests green after the change; 8 new tests for getTooltipExtras → 2102 in total). The only deliberate behaviour change is bringing OHLC along (ohlc now gets extras), with its own test in series-tooltip-extras.test.ts.

Stop moments — outcome#

  • Stop moment 1 (the record-mapper shape strains on waterfall's allData context): not triggered. The optional allData?: readonly ChartDatum[] parameter carries the dataset context cleanly; candlestick + ohlc ignore it; waterfall consumes it.

  • Stop moment 2 (ohlc cannot share candlestick's branch): not triggered. The shared case "candlestick": case "ohlc": works symmetrically — identical key shape, identical extraction logic.

Known, unresolved broader concern#

  • computeWaterfallPositions duplication — three production call sites (useChartTooltip via getTooltipExtras, useScales, waterfall-series.tsx). T3 phase 2 touches one (the tooltip call site moves to the utility), but the duplication remains. A broader clean-up candidate for a later mini-trigger (comparable to the stackBaselineMap/computeBaselineByRow that T1-v2 cleaned up, but now across three call sites instead of two). Not T3 scope.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2102/2102 green (2094 pre-T3-v2 + 8 new getTooltipExtras tests). No existing assertion adjusted.

  • pnpm --filter @workspace/active-charts lint — 18 warnings (same baseline).

  • pnpm turbo run typecheck (monorepo) — 28/28 tasks green.

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files (2 new + 2 changed)#

New:

  • src/utils/series-tooltip-extras.ts — getTooltipExtras utility (~110 lines)

  • src/utils/series-tooltip-extras.test.ts — 8 unit tests (candlestick + ohlc brought along + 4-key validation + waterfall with allData + waterfall without allData + line/bar/dumbbell fallbacks)

Changed:

  • package.json — 1.4.0 → 1.4.1

  • src/hooks/use-chart-tooltip/use-chart-tooltip.ts — two inline branches (~50 lines) replaced by one getTooltipExtras call (~3 lines); imports cleaned up (computeWaterfallPositions, WaterfallSeriesOptions no longer needed directly)

T3 architecturally closed#

T3 phase 1 (v1.4.0): TooltipContext + Finding A clean-up + Y2 + P1. T3 phase 2 (v1.4.1): getTooltipExtras + OHLC brought along.

Positioning: T3-v2 (T2+T9 contract untouched; no T2+T9-v3). No open T3 sub-triggers remain.

C1 stays parked#

getTooltipExtras has no AxisContext need; it only touches s + point + allData. No C1 consumer delivered.

Remaining open T topics (after T3 closed)#

  • T4 — Tooltip-positioning unification (~2 sessions, standalone); QR-7 ⊆ T4

  • T6 — Defaults-policy review (~1 session, standalone)

  • T7-v3-mini — populate FlatPoint.xAxisId/yAxisId (~1 session, mechanical, not critical)

Loose observations: OHLC hit-test inconsistency (a different layer), useVoronoi → useHitTest rename, computeWaterfallPositions 3-call-site duplication.

[1.4.0] — 2026-05-22#

Added (internal)#

  • T3 phase 1 — TooltipContext in src/hooks/use-chart-tooltip/tooltip-context.ts. Bundles axis info (xAxisType, yAxisType) and formatter defaults (valueFormatter) for the tooltip layer, on top of T7's AxesContextValue/geometry context (G3 route). No second parallel axis shape — geometry stays the single source. Kept narrow per stop-moment-3 discipline: only fields with a demonstrable consumer in this session.

  • T3-Y2 — formatYValue log-aware. Under yAxisType === "log" the default formatter uses Intl.NumberFormat's notation: "compact" for values ≥1000 (10k/100k/1M) and maximumSignificantDigits instead of fraction precision. Other axis types: pre-T3 behaviour unchanged (locale default with 0 or 2 decimals).

  • T3 phase 1 test chart in apps/admin/app/(main)/test-charts-extended/_components/extended-charts-view.tsx — a log-axis line chart (stock index 1900-2020, log-scale domain). The hover tooltip now shows compact notation for large values without the consumer writing a custom valueFormatter. Proves Y2 in the production tree.

Changed (internal)#

  • Finding A clean-up (T7-v3 part) — 12 plumbing locations gone. Before T3:

    • xAxisType? field on SingleTooltipData/SharedTooltipData/SplitTooltipData (3 locations)

    • useChartTooltip derived xAxisType from options.axes (1 location) + 3 assign sites

    • 5 read sites in tooltip.tsx + split-tooltip-layer.tsx

    After T3: all three TooltipData types carry a context?: TooltipContext field (optional for standalone wrappers that have no Cartesian geometry). useChartTooltip(geometry) builds context via useChartGeometry().axes.x.type. Read sites read data.context?.xAxisType (optional chain with an undefined fallback for the 11 standalone wrappers).

    Single source of truth achieved: axes.x.type lives in one place (T7-v2's geometry), the tooltip layer consumes it via the context.

  • useChartTooltip(geometry: ChartGeometry) — signature extended with a geometry parameter. <Chart> call site updated. Drift test pixel-identical.

  • T3-P1 — TooltipRendererParams restructured (BREAKING for renderer? consumers):

    Pre-T3 (11-field bag)Post-T3 (grouped)
    params.datum, params.xKey, params.yKey, params.xValue, params.yValue, params.seriesName, params.color, params.sizeValue?, params.sizeKey?params.point.{datum, xKey, yKey, xValue, yValue, seriesName, color, sizeValue?, sizeKey?}
    (no context)params.context?: TooltipContext
    params.entries?, params.groupMeta?unchanged

    In-package consumers migrated in the same commit (per §7.14):

    • bar-chart.tsx's errorBars default formatter (params.datum → params.point.datum, params.yKey → params.point.yKey)

    • tooltip.test.tsx assertions

    • 3 params build sites in tooltip.tsx + split-tooltip-layer.tsx

    renderer? consumer inventory: 11 files, all within packages/active-charts/. No external consumer in apps/admin/web/auth/atlas/archives or other packages. Stop moment 2 (renderer? consumer inventory larger than expected) not triggered.

Drift test pixel-identical#

The Finding A clean-up is a pure refactor: tooltips render bit-for-bit identically before and after for the existing test suite. 2094/2094 tests green, no assertion adjusted. The deliberate behaviour change is Y2's log-aware formatting for yAxisType === "log", verified via the new test chart (existing tests use no log axes for tooltip assertions, so no test fell over).

Stop moments — outcome#

  • Stop moment 1 (Finding A clean-up not pixel-identical): ❌ not triggered. The geometry route delivers exactly the same info as the pre-T3 options.axes derivation.

  • Stop moment 2 (renderer? consumer inventory larger than expected): ❌ not triggered. 11 files, all within the package.

  • Stop moment 3 (TooltipContext field bloat): ✅ active — three fields added (xAxisType, yAxisType, valueFormatter), each with a demonstrable consumer in this session. No preventive fields.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2094/2094 green on identical assertions. No test adjusted.

  • pnpm --filter @workspace/active-charts lint — 18 warnings (same baseline).

  • pnpm turbo run typecheck (monorepo) — 28/28 tasks green. The migrated bar-chart consumer compiles.

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files (3 new + 7 changed)#

New:

  • src/hooks/use-chart-tooltip/tooltip-context.ts — TooltipContext type

  • Log-axis test chart (section added to apps/admin/.../extended-charts-view.tsx)

  • (No separate test file — TooltipContext is tested via existing tooltip tests that use it in fixtures)

Changed:

  • package.json — 1.3.3 → 1.4.0

  • src/types/interaction.ts — 3 xAxisType? fields replaced by context?: TooltipContext

  • src/types/tooltip-options.ts — TooltipRendererParams restructured (P1)

  • src/hooks/use-chart-tooltip/use-chart-tooltip.ts — Finding A derivation gone; now builds TooltipContext via geometry; signature useChartTooltip(geometry)

  • src/hooks/use-chart-tooltip/use-chart-tooltip.test.ts — makeGeometry fixture added; 12 hook calls updated

  • src/core/overlays/tooltip.tsx — formatYValue log-aware; 5 read sites + 2 params build sites updated

  • src/core/overlays/split-tooltip-layer.tsx — 1 read site + 1 params build site updated

  • src/core/overlays/tooltip.test.tsx — makeContext fixture; 7 TooltipData fixtures updated

  • src/components/chart/chart.tsx — useChartTooltip(geometry) call

  • src/components/bar-chart/bar-chart.tsx — errorBars valueFormatter params.datum → params.point.datum

T3 status after this session#

T3 phase 1 completed. Open:

  • T3 phase 2 (an open decision point after this session): series-type data extraction in useChartTooltip — the two inline branches for candlestick OHLC extraction and the waterfall positions recompute. Deserves its own capability-vs-record weighing analogous to T5-v2. Does it belong to T3-v2, or does it become T2+T9-v3? Decision follows after the verification phase.

  • T7-v3 mini (populate FlatPoint.xAxisId/yAxisId) — open, not handled via T3 (separate per analysis §1.2). Mechanical work across 10+ plug-in paths.

The T7-v3 Finding A part is hereby completed. The xAxisType stopgap from T7-v1 has been permanently removed; the tooltip layer reads axes.x.type via T7-v2's geometry context.

C1 stays parked#

T3 phase 1 delivers no consumer for an AxisContext field extension — axes.x.type and axes.ys.get(id).type are already in T7's 5-field AxisContext. C1 waits for T2+T9-v3 or T7-v3 if those introduce concrete consumers.

[1.3.3] — 2026-05-22#

Added (internal)#

  • T5-v2 — getSeriesYKeys utility in src/utils/series-y-keys.ts. Per series type the utility supplies the yKeys array that builds the y-domain (4 fixed-keys mappings for candlestick/ohlc/box-plot/range-area/range-bar/dumbbell + 1 yKey default branch with a bar-errorBars sub-discriminator). Outside the T2+T9 contract — Option B from t5-use-scales-analyse.md §12, weighed on an equal footing with Option A (a 4th capability) and chosen for consistency with the T2+T9 precedent (functionally equivalent to defaultApproximationForType(s) in use-prepared-data.ts, which T2+T9 also kept outside the contract).

    Test coverage: 18 unit tests in series-y-keys.test.ts for all five branches, the bar-errorBars sub-discriminator (incl. the stacked gate), and the yKey default fall-through for 6 series types.

Changed (internal)#

  • use-scales.ts series-type switch fully consolidated. Three call sites consume the shared getSeriesYKeys:

    • Main y-loop (lines 156-205 → 5 lines): 5 inline branches with an if/else chain + the bar-errorBars sub-discriminator gone; one yKeys.push(...getSeriesYKeys(s)) call.

    • Fallback y-loop (lines 258-272 → 5 lines): the pre-T5-v2 candlestick-only branch + yKey default gone; one yKeys.push(...getSeriesYKeys(s)) call.

    • X-scale special (lines 100-108 → 4 lines): the per-series dumbbell/yKey branch gone; one xYKeys.push(...getSeriesYKeys(s)) call. The cross-series hasHorizontalNumericValueAxis detection stays upstream (a per-series lookup cannot carry a cross-series condition).

Behaviour improvements (latent bug fixes)#

Two places gained, via the shared helper, extra types under their care that were silently skipped before T5-v2:

  1. The fallback loop now covers all types. Before T5-v2 only candlestick + the yKey default; now complete (ohlc, range-area, range-bar, box-plot, dumbbell, bar-errorBars sub-discriminator). The fallback loop is activated when options.axes defines no yAxes — an edge-case path, but before T5-v2 the 5 excluded types would fall onto the default [0, 1] domain.

  2. X-scale horizontal mode now covers bar's errorBars. Before T5-v2 the errorBars sub-discriminator was missing for horizontal-mode bar; the error whisker would be cut off at yKey's range. By using getSeriesYKeys, the x-axis extent and the y-axis extent consume the same set of keys.

Both are deliberate behaviour improvements, not unintended regressions. Existing tests cover neither combination — pixel-identical relative to pre-T5-v2 for all existing scenarios.

Main loop pixel-identical#

The main loop produces bit-for-bit the same yKeys as before T5-v2 for every type. Verified by 2094/2094 tests green on identical assertions.

Stop moments — outcome#

  • Stop moment 1 (bar's errorBars condition strains): ❌ not triggered. The mapper absorbs the condition cleanly.

  • Stop moment 2 (fallback-loop behaviour change): ✅ in effect, two behaviour improvements reported.

  • Stop moment 3 (main loop not pixel-identical): ❌ not triggered.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2094/2094 green (was 2076; +18 new tests).

  • pnpm --filter @workspace/active-charts lint — 18 warnings (same baseline).

  • pnpm typecheck (full monorepo) — 38/38 tasks green.

  • grep "✅" docs/ROADMAP.md — 0 hits.

The main chain is closed#

With T5-v2 the main chain T7 → T2+T9 → T1 → T5 closes. Five workstreams completed one after another without shape strain, without breaking impact for external consumers:

  • T7 — chart-context shape + projection helpers

  • T2+T9 — series-type plug-in contract (capability pattern)

  • T1 — baseline resolution + bar-geometry consolidation

  • T5 — orientation-swap helper + yKeys extraction

Remaining open T topics fall outside the main chain: T3, T4, T6, T7-v3.

[1.3.2] — 2026-05-22#

Added (internal)#

  • T5-v1 — compileAxisPair helper. Consolidates the orientation swap for the 4 horizontal-supporting Cartesian wrappers (bar-chart, dumbbell-chart, range-bar-chart, lollipop-chart). Replaces the pre-T5-v1 pattern in which each wrapper did an inline horizontal ? valueAxis : xAxis swap with as never casts (QR-1).

    Signature (internal): compileAxisPair(categoryConfig, valueConfig, horizontal): { x: AxisOptions, y: AxisOptions }. The most compact form from t5-axis-analyse §6 (Option A1). Per-orientation defaults (bottom/left × category/number) live in the helper; the consumer passes only the two configs + the orientation flag.

    Location: src/utils/compile-axis-config.ts, next to the existing compileAxisConfig. Not exported from src/index.ts.

Changed (internal)#

  • 4 wrapper swap locations migrated to compileAxisPair. Per wrapper one 12-line block (inline swap + 4×as never cast in bar, no casts in the other 3) is replaced by one 5-line helper call:

    WrapperPre-T5-v1Post-T5-v1
    bar-chart.tsx (lines 336-347)12 lines swap + 2× as never5 lines helper call
    dumbbell-chart.tsx (lines 174-185)12 lines swap5 lines helper call
    range-bar-chart.tsx (lines 147-158)12 lines swap5 lines helper call
    lollipop-chart.tsx (lines 184-195)12 lines swap5 lines helper call
  • compile-axis-config.ts itself unchanged for its existing compileAxisConfig export (it is reused internally by compileAxisPair). The 9 non-swap wrappers keep importing compileAxisConfig directly — unchanged.

QR-1 handled#

QR-1's as never casts lived only in bar-chart.tsx's two swap branches (the other three wrappers had no casts but did have the inversion-prone ternary). Both main risks — the casts and the 4× duplication of the ternary — are resolved:

  • No more as never in the wrappers; the cast does not exist in the helper implementation either, because compileAxisPair's loose AxisConfigInput type (AxisConfig | AxisType | Record<string, unknown> | undefined) accepts bar's valueAxisMerged spread without a bypass.

  • One place for the swap, four wrappers consume it — a ternary inversion can only be made in the helper, no longer spread 4× across the code.

Stop moment 2 — C1 (AxisContext extension) not carried out in this session#

The briefing contained C1 as a second deliverable, gated by stop moment 2: do not add AxisContext fields without a demonstrable consumer in this session. Inventory of consumers in this session:

  • compileAxisPair produces AxisOptions, not AxisContext — no field need.

  • The 4 migrated wrappers consume AxisOptions (downstream pipe via <Chart> → useScales → buildChartGeometry), not AxisContext directly.

  • Existing T7 consumers (useChartGeometry, T2+T9 plug-ins, T1 helpers) read only the 5 current AxisContext fields — no need for an extension.

No demonstrable consumer → no extension. Stop-moment-2 discipline works as intended. C1 waits for a natural occasion when T3 (tooltip context flow), T7-v3 (FlatPoint.xAxisId), or T5 phase 2 (sub-problem B) introduces a concrete config-field consumer.

Behaviour preserved#

A pure internal refactor — all 4 swap wrappers produce identical compiledXAxis/compiledYAxis as before T5-v1. 2076/2076 tests green on identical assertions. The 9 non-swap wrappers untouched. No consumer-visible change.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors (and the as never casts gone from the 4 wrappers — QR-1's type hole closed).

  • pnpm --filter @workspace/active-charts test — 2076/2076 green, no assertion adjusted.

  • pnpm --filter @workspace/active-charts lint — 18 warnings (same baseline as pre-T5).

  • pnpm typecheck (full monorepo) — 38/38 tasks green — consumers unchanged (confirms the §6.3 analysis: not breaking).

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files (5 changed, 0 new)#

  • package.json — version 1.3.1 → 1.3.2.

  • src/utils/compile-axis-config.ts — compileAxisPair added + AxisConfigInput type.

  • src/components/bar-chart/bar-chart.tsx — swap block via helper.

  • src/components/dumbbell-chart/dumbbell-chart.tsx — swap block via helper.

  • src/components/range-bar-chart/range-bar-chart.tsx — swap block via helper.

  • src/components/lollipop-chart/lollipop-chart.tsx — swap block via helper.

T5 status after this session#

T5-v1 (sub-problem A) completed. T5-v2 = sub-problem B (the useScales series-type switch refactor as a possible T2+T9 capability extension) is open. T5-v1.x = sub-problem C (AxisContext extension) waits for a demonstrable consumer and is therefore NOT a separate open trigger, but a "to be carried out when T3/T7-v3/phase 2 introduces a consumer" trigger.

[1.3.1] — 2026-05-22#

Added (internal)#

  • T1-v2 — computeBarGeometry + computeStackBaselineMap. Bar's three-layer coupling (renderer + hit-test + animation anchor) is consolidated into one computeBarGeometry helper. The computeBaselineByRow ↔ stackBaselineMap duplication between bar-plugin.tsx and <SvgRenderer> has been eliminated — both now use the shared computeStackBaselineMap helper. Calls resolveBaseline internally (RC1 — the helpers stay separate).

    Both helpers live in src/core/series-plugins/_shared.ts next to resolveBaseline from v1.3.0. Shape: GB (bulk geometry builder) from t1-analyse §6 — one call delivers all per-row rects. No fallback to GC was needed: the three consumers could read the same bulk output cleanly. The renderer-versus-hit-test asymmetry (a cosmetic gap of 3 px between grouped bars) is modelled via a cosmeticGap parameter: the renderer passes 3, the hit-test passes 0.

  • 15 new unit tests for the two helpers (computeStackBaselineMap: 5 tests; computeBarGeometry: 10 tests covering all four orientation×stacked combinations, the gap parameter, and validity edge cases).

Changed (internal)#

  • bar-series.tsx's targetPositions useMemo — before T1-v2 this was a 4-branch if-chain (horizontal × stacked) with the baseline bug repaired per branch in v1.3.0. v1.3.1 replaces it with one computeBarGeometry call with cosmeticGap: 3. 110 lines of inline logic gone.

  • bar-plugin.tsx's emitHitTestElements — before T1-v2 this plug-in did its own rect building + had its own computeBaselineByRow helper. v1.3.1 replaces both with a computeBarGeometry call (with cosmeticGap: 0 for the Path-C hit zone) and a computeStackBaselineMap call. ~70 lines of inline logic gone; the Path-B flatpoint and Path-C BarRect emit stay the plug-in's responsibility (geometry + bounds checks).

  • <SvgRenderer>'s stackBaselineMap useMemo — before T1-v2 this was a ~25-line inline implementation. v1.3.1 replaces it with one computeStackBaselineMap call. The duplication with bar-plugin's pre-T1-v1 computeBaselineByRow (~19 lines) is thereby fully cleaned up.

Pixel-identical#

A pure refactor — bar rendering and the bar hit-test produce bit-for-bit the same output as v1.3.0. 2076/2076 tests green on identical assertions; no test adjusted. Stop moment 1 (shape strains) not triggered: the three consumers consumed the bulk output (BarRectPosition[]) cleanly, with the cosmeticGap parameter as the only caller decision for modelling the renderer-vs-hit-test asymmetry.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2076/2076 green (was 2061; +15 new helper tests).

  • pnpm --filter @workspace/active-charts lint — 18 warnings (the same number as before T1-v2).

  • pnpm typecheck (full monorepo) — 38/38 tasks green.

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files#

Changed:

  • package.json — version 1.3.0 → 1.3.1.

  • src/core/series-plugins/_shared.ts — computeStackBaselineMap + computeBarGeometry added. ~150 lines of new helper code.

  • src/core/series-plugins/_shared.test.ts — 15 new unit tests.

  • src/core/series-plugins/bar-plugin.tsx — emitHitTestElements consumes the helpers; its own computeBaselineByRow removed. ~70 lines inline gone, ~50 lines via helpers.

  • src/core/renderer/series/bar-series.tsx — targetPositions useMemo via computeBarGeometry. ~110 lines inline gone.

  • src/core/renderer/svg-renderer.tsx — stackBaselineMap useMemo via computeStackBaselineMap. ~25 lines inline gone.

T1 — fully completed#

With v1.3.1, T1 closes. Both helpers are live:

  • resolveBaseline (v1.3.0) — three consumers (bar non-stacked, histogram, lollipop stick). Fixes QR-6 (mixed-sign / non-zero-min y-domain rendering).

  • computeBarGeometry + computeStackBaselineMap (v1.3.1) — bar's three-layer coupling + the stack-baseline duplication cleaned up. A pure refactor without a consumer-visible change.

No open T1 follow-up triggers. Roadmap order T2+T9 → T1 → T5 — only T5 is still open in this chain.

[1.3.0] — 2026-05-21#

Fixed (consumer-visible)#

  • QR-6 — negative-value rendering. Bar charts, histograms and lollipop charts with a mixed-sign y-domain (e.g. [-50, +100]) or a non-zero-min y-domain (e.g. [20, 80]) now render visually correctly. Before T1 all three drew upward from the plot floor (barHeight = plotHeight - yPos, or projectX(0)/projectY(0) as a hardcoded baseline), which gave wrong results when 0 was not in the domain:

    • Mixed-sign data: negative values appeared above the 0 line instead of below it.

    • Non-zero-min data: bars grew from the plot floor instead of from the natural bottom of the domain.

    After T1, resolveBaseline picks the natural baseline (0 if 0∈domain, otherwise the domain min) and bars/sticks grow from there. Existing charts with domains that contain 0 (by far the majority) see no change — pixel-identical to v1.2.0.

Added (internal)#

  • T1-v1 — resolveBaseline. A pure number resolver in src/core/series-plugins/_shared.ts. Input: a numeric domain + an optional caller override. Output: the baseline value (caller override → that; otherwise 0 if 0∈domain; otherwise the domain min). Pure, type-light, independently testable — consistent with the bandwidthOf(axisId) shape from T7-v2 (the caller does the projection itself).

    Test coverage: 14 new unit tests in _shared.test.ts for mixed-sign, non-zero-min, caller override (including the NaN fallback and an override outside the domain), an inverted-range domain, and a defensive non-numeric-domain fallback.

Changed (internal)#

  • bar-series.tsx non-stacked branches (lines ~186-242 in the targetPositions useMemo) — replaced by the resolveBaseline pattern. Horizontal non-stacked: barX = Math.min(valuePos, baselinePixel) + barWidth = abs(baselinePixel - valuePos). Vertical non-stacked: likewise on the y-axis. Stacked branches unchanged (they were already correct via the explicit stackBaseline prop).

  • bar-plugin.tsx emitHitTestElements (~line 176): baseVal = isStackedSeries ? baselineVal : 0 → baseVal = isStackedSeries ? baselineVal : nonStackedBaseline where nonStackedBaseline = resolveBaseline(value-axis-domain). The value-axis domain is resolved via geometry.axes.x (horizontal mode) or geometry.axes.ys.get(barYAxisId) (vertical mode).

  • histogram-series.tsx targetPositions (~lines 111-122): barHeight = plotHeight - barY → a baseline-aware Math.min/Math.abs pattern via resolveBaseline(yScale.domain()). The same pattern as bar's vertical non-stacked.

  • lollipop-series.tsx (~line 64): baseline = (isHorizontal ? projectX(0) : projectY(0)) ?? 0 → baseline = (isHorizontal ? projectX(resolveBaseline(xScale.domain())) : projectY(resolveBaseline(yScale.domain()))) ?? 0. The stick start is now baseline-aware instead of a hardcoded 0.

Drift-test finding#

T1's first implementation session is a drift test on the sharpest capability point (bar's non-stacked + hit-test, where the baseline bug and bar's geometry meet). Passed cleanly:

  • Stop moment 1 (shape strains): not triggered. The BA shape (pure number resolver) fitted all three consumers without bending.

  • Stop moment 2 (contract grows): not triggered. No 4th input needed; no reason for a BB or BC shape.

  • Histogram + lollipop could come along in the same session because the drift test on bar validated the shape; their call form turned out to be trivial (~2 lines of diff per call site).

Behaviour preserved (where 0 ∈ domain)#

2061/2061 tests green on exactly the same assertions as before T1. Existing bar, histogram and lollipop tests use "happy path" domains that contain 0; for those cases the old and new behaviour produce pixel-identical output (verified via projecting projectY(0) = plotHeight in inverted scales). The behaviour change is thereby strictly isolated to mixed-sign / non-zero-min domains — exactly the intended fix.

No test adjusted to mask the behaviour change.

Lint side effect#

Lint warnings: 20 → 18. The useMemo deps extension in histogram-series.tsx (for projectX/projectY) cleared the pre-existing react-hooks/exhaustive-deps warning. Other warnings unchanged.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2061/2061 green, no assertion adjusted.

  • pnpm --filter @workspace/active-charts lint — 18 warnings (was 20).

  • pnpm typecheck (full monorepo) — 38/38 tasks green.

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files#

New:

  • src/core/series-plugins/_shared.test.ts — 14 unit tests for resolveBaseline.

Changed:

  • package.json — version 1.2.0 → 1.3.0.

  • src/core/series-plugins/_shared.ts — resolveBaseline added.

  • src/core/series-plugins/bar-plugin.tsx — baseVal = 0 → resolveBaseline(...).

  • src/core/renderer/series/bar-series.tsx — non-stacked branches baseline-aware.

  • src/core/renderer/series/histogram-series.tsx — baseline-aware Y rendering.

  • src/core/renderer/series/lollipop-series.tsx — baseline-aware stick.

What T1 still leaves open (for the next session)#

  • computeBarGeometry — bar's three-layer coupling (renderer + hit-test + animation anchor) + the computeBaselineByRow/stackBaselineMap duplication between the bar plugin and <SvgRenderer>. A separate session, planned as M2 (see t1-analyse §8). Do not mark T1 as completed until that has also been carried out.

QR-6 status#

QR-6 ⊆ T1, handled in v1.3.0. Negative-value rendering now works visually correctly for the three consumers (bar/histogram/lollipop). QR-6 removed as an open roadmap item.

[1.2.0] — 2026-05-21#

Added (internal)#

  • T2+T9-v2 — full Cartesian migration to the SeriesTypePlugin contract. All 13 Cartesian series types now go through the plug-in registry. Twelve new plug-ins on top of the bar plugin (v1.1.2):

    Plug-inhitTestrenderaccessibleColumnsSub-discrimination inside plug-in
    line✓ Path B✓✓ single-yKey—
    area✓ Path B✓✓ single-yKey—
    scatter✓ Path B + sizeKey✓✓ single-yKeysizeKey passthrough
    histogram✓ Path B✓✓ single-yKey—
    waterfall✓ Path B✓✓ single-yKey—
    candlestick✓ Path B with closeKey override✓✓ fallback row (no yKey)closeKey resolution
    dumbbell✓ 2 flatpoints + halfBand + horizontal swap✓✓ 2 columns (low + high)horizontal mode
    lollipop✓ 1 flatpoint + halfBand + horizontal swap✓✓ single-yKeyhorizontal mode
    range-bar— (Walt decision: no hit-test)✓✓ fallback row (no yKey)—
    range-area— (no yKey, no hit-test pre-T2+T9)✓✓ fallback row—
    ohlc— (no yKey, no hit-test pre-T2+T9)✓✓ fallback row—
    box-plot— (no yKey, no hit-test pre-T2+T9)✓✓ fallback row—

    Four types omit HitTestCapability — exactly the capability pattern that the phase-1 analysis §6.2 foresaw. The other capabilities (render + cols) are delivered. No forced no-ops.

  • src/core/series-plugins/_shared.ts — resolveDefaultLeftYAxisId() + emitPadBFlatPoints(). Six plug-ins (line, area, scatter, histogram, waterfall, candlestick) share the same Path-B implementation through this helper, parameterised with a yKey override + an optional sizeKey.

Changed (internal)#

  • use-voronoi.ts — all inline series-type branches (dumbbell ~117 lines, lollipop ~70 lines, fall-through Path B ~70 lines, plus the earlier removed bar branch) are gone. The hook is now a thin dispatch-and-Delaunay shell of ~140 lines (was ~541). getSeriesColor, the ChartScale type, the projectX/projectY/bandwidthOf destructure, and the local yScales Map are gone — no longer used outside the plug-ins.

  • svg-renderer.tsx — switch (seriesOpt.type) { case ... } (13 cases, ~280 lines) has been fully replaced by one plug-in dispatch (~50 lines). Bar's cross-series pre-compute (barSeriesIndices, stackBaselineMap) stays in <SvgRenderer> and is passed to the bar plug-in via RenderContext. 12 direct series-component imports gone; 10 unused *SeriesOptions type imports gone.

  • accessible-data-table.tsx — getSeriesColumns (was ~78 lines with dumbbell + bar-stackedPercentage + bar-errorBars + default) is now a 3-line plug-in dispatch with a fallback for unexpected types.

Behaviour preserved (pixel-identical)#

2047/2047 tests green on identical assertions. No test adjusted to legitimise a refactor outcome. Stop moments not triggered:

  • No shape strain — all 12 types fitted the bar-template contract without a capability extension.

  • No contract growth — HitTestCapability/RenderCapability/AccessibleColumnsCapability + the R1 registry from v1.1.2 cover everything.

  • No knock-on changes to other types — each type migration isolated itself within its plug-in.

Per-type findings (outliers)#

  • range-bar — has no HitTestCapability. Walt decision from phase 1: Gantt-like use does not need flatpoint emission. The capability pattern handles this naturally.

  • range-area, ohlc, box-plot — also no HitTestCapability. Before T2+T9 these types were silently skipped by useVoronoi's fall-through path (no yKey field, and only candlestick had an explicit sub-condition). Behaviour preserved: the plug-ins do not implement hitTest.

  • OHLC observation (parked, no fix) — a pre-T2+T9 inconsistency: candlestick gets a hit-test (via an explicit isCandlestick check on closeKey), but OHLC does not — while the two are comparable. Pixel-identical discipline keeps this inconsistency in v1.2.0; flagged in the DONE entry for future consideration.

  • lollipop stroke field — before T2+T9, useVoronoi read lol.stroke ?? lol.fill ?? color defensively via a loose type cast, while LollipopSeriesOptions declares no stroke field. The plug-in keeps that defensive lookup (cast through a union with { stroke?: string }) for exactly equivalent behaviour.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2047/2047 green, identical assertions.

  • pnpm --filter @workspace/active-charts lint — 20 warnings (same baseline as pre-T2+T9-v2).

  • pnpm typecheck (full monorepo) — 38/38 tasks green.

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files (13 new + 1 update + 3 changed)#

New:

  • src/core/series-plugins/_shared.ts — shared Path-B helper + axis resolver

  • src/core/series-plugins/{line,area,scatter,histogram,waterfall,candlestick,ohlc,range-area,range-bar,box-plot,dumbbell,lollipop}-plugin.tsx — 12 plug-ins

Update:

  • src/core/series-plugins/registry.ts — all 13 plug-ins registered

Changed:

  • package.json — version 1.1.2 → 1.2.0

  • src/hooks/use-voronoi/use-voronoi.ts — switch body removed; now a thin dispatch shell (541 → ~140 lines)

  • src/core/renderer/svg-renderer.tsx — 13-case switch replaced by plug-in dispatch; unused imports gone

  • src/core/accessibility/accessible-data-table.tsx — getSeriesColumns now a pure plug-in dispatch

useVoronoi → useHitTest rename — postponed#

The API-impact suggestion from the roadmap (useVoronoi → useHitTest, VoronoiResult → HitTestResult) was not included in v1.2.0. The rename did not fall naturally out of the migration — useVoronoi is now substantively a hit-test dispatcher, but the Delaunay build it also performs still justifies the Voronoi name. A separate decision for later.

[1.1.2] — 2026-05-21#

Added (internal)#

  • T2+T9-v1 — the SeriesTypePlugin contract as a drift test on bar. The first implementation of the T2+T9 plug-in contract (see DECISIONS.md § ADR-010). A capability pattern for the three if/switch-style switches:

    • HitTestCapability — for use-voronoi.ts flatpoint/BarRect emission

    • RenderCapability — for svg-renderer.tsx series-element dispatch

    • AccessibleColumnsCapability — for accessible-data-table.tsx sr-only columns

    A plug-in implements the capabilities relevant to its type (optional fields). Records (animation-types.ts, use-prepared-data.ts) keep their declarative form — not part of the contract, in line with §6/§7.2.

  • R1 registry (compile-time barrel) in src/core/series-plugins/registry.ts. Static, tree-shakeable. A runtime register call is not supported in v1 (the public "Plugin contract" feature from roadmap §6 is a later, separate decision).

  • bar-plugin.tsx as the first implementation. Bar touches all three plug-in-side capabilities and all three sub-discrimination variants (stacked, stackedPercentage, errorBars, plus the horizontal orientation). Sub-discrimination is handled inside the plug-in (no separate plug-ins per variant).

Changed (internal)#

  • use-voronoi.ts — plug-in dispatch added at the front of the series loop. The bar branch (~162 inline lines) was removed; bar is now handled by barPlugin.hitTest.emitHitTestElements(). Other series types (dumbbell, lollipop, candlestick fall-through, scatter/line) stay inline — drift-test isolation.

  • svg-renderer.tsx — case "bar": in the series switch now reads from barPlugin.render.renderSeriesElement(). The cross-series pre-compute (barSeriesIndices, stackBaselineMap) stays in <SvgRenderer> (passes the array via RenderContext.stackBaseline). The direct BarSeries import was removed. The other 12 cases unchanged.

  • accessible-data-table.tsx — getSeriesColumns now reads from barPlugin.accessibleColumns.emitAccessibleColumns(). The bar sub-branches (stackedPercentage + errorBars formatters, ~50 inline lines) are gone. The dumbbell branch and default fall-through unchanged.

Drift-test finding#

Passed cleanly. Stop moments 1/2/3 not triggered:

  • Stop moment 1 (shape strains): not triggered. The three capabilities carried the heterogeneous characters (a thick imperative hit-test, a thin declarative render, divergent sr-only columns) without bending. Optional fields on RenderContext (stackBaseline, barSeriesCount, barSeriesOrdinal) for bar-specific pre-computes fitted naturally; other plug-ins do not consume them.

  • Stop moment 2 (contract grows): not triggered. The three capabilities + the R1 registry covered bar's full responsibility. No fourth capability needed.

  • Stop moment 3 (touches other series types): not triggered. The plug-in dispatch in each switch is a branch added at the front with an early return/continue; other inline branches unchanged.

Natural convergence (input for T1)#

computeBaselineByRow() in the bar plugin is consumed by both emitHitTestElements and (implicitly) <BarSeries>. Before T2+T9 the baseline computation was duplicated word for word in use-voronoi.ts (~19 lines) and in svg-renderer.tsx's stackBaselineMap useMemo (~25 lines). In the plug-in it now lives as one implementation. <SvgRenderer> keeps its own stackBaselineMap for the cross-series pre-compute (it passes the result via RenderContext to the plug-in). The use-voronoi side uses the plug-in's helper directly.

This is input for T1: the natural convergence proves that the contract carries the shared-helper form that T1's computeBarGeometry() is looking for. Not cleaned up in v1.1.2 (T1 scope), but reported as an architectural observation.

Tests#

No test assertions adjusted. 2047/2047 green — bar rendering and the bar hit-test pixel-identical. No new tests added; existing series tests cover the plug-in output via the same paths.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2047/2047 green, identical assertions.

  • pnpm --filter @workspace/active-charts lint — 20 warnings (same baseline as pre-T2+T9).

  • pnpm typecheck (full monorepo) — 38/38 tasks green.

  • grep "✅" docs/ROADMAP.md — 0 hits.

Files (3 new + 4 changed)#

New:

  • src/core/series-plugins/types.ts — capability interfaces + SeriesTypePlugin

  • src/core/series-plugins/registry.ts — R1 compile-time barrel

  • src/core/series-plugins/bar-plugin.tsx — bar plugin with 3 capabilities

Changed:

  • package.json — version 1.1.1 → 1.1.2

  • src/hooks/use-voronoi/use-voronoi.ts — plug-in dispatch + bar-branch removal

  • src/core/renderer/svg-renderer.tsx — bar-case dispatch + BarSeries import removal

  • src/core/accessibility/accessible-data-table.tsx — getSeriesColumns dispatch + bar sub-branch removal

[1.1.1] — 2026-05-21#

Changed (internal)#

  • T7-v2.1 — clean-up of redundant local scale variables and utilities in use-voronoi.ts. Removed:

    • categoryScale + valueScale local vars in the dumbbell path (lines 121-122 pre-v2.1).

    • lolCategoryScale + lolValueScale local vars in the lollipop path (lines 236-237 pre-v2.1).

    • categoryScale + valueScale local vars in bar Path C (lines 328-329 pre-v2.1).

    • The xScale local var at the top of the useMemo body (line 54 pre-v2.1, only still referenced by comment text).

    • The local halfBandOf(scale) utility (lines 22-29 pre-v2.1).

    • The local bandwidthOf(scale, fallback, count) utility (lines 36-47 pre-v2.1).

    Replaced by an inline route via the T7-v2 useChartGeometry() helpers. The correct axis id is passed per series path: the dumbbell path uses yAxisId for the horizontal-mode category band, the lollipop path lolYAxisId, bar Path C barYAxisId. For bar Path C: an inline fallback (rawBandwidth > 0 ? rawBandwidth : plotDim / dataLength) reproduces the semantics of the pre-v2.1 local bandwidthOf utility — exactly the pattern that T7-v2 already introduced for dumbbell-series, lollipop-series and range-bar-series.

  • The 4 lint warnings that T7-v2 parked as an observation are gone — lint output from 23 → 20 warnings.

Behaviour preserved#

A behaviour-preserving refactor. 2047/2047 tests green on exactly the same assertions as pre-v2.1; no test assertion adjusted. The stop moment was not triggered after the structural difference between the local bandwidthOf and the geometry helper bandwidthOf had been clarified beforehand (the local utility synthesised fallbackPlotDim / dataLength for non-band scales; the geometry helper returns 0). The inline fallback in bar Path C reproduces the pre-v2.1 semantics.

Remaining lint observations (out of scope for v2.1)#

20 lint warnings remain, mainly in svg-renderer.tsx (a leftover xAxisType variable) and waterfall-series.tsx (useMemo missing deps) — both after the T7-v2 introduction. Not touched in v2.1 because the briefing explicitly named "only use-voronoi.ts" as the scope boundary. Candidates for a possible later mini clean-up or to be picked up naturally with T2+T9 / T5.

[1.1.0] — 2026-05-21#

Added (internal)#

  • T7-v2 — projection helpers on useChartGeometry(). The (internal) ChartGeometry shape now carries three functions alongside the data:

    • projectX(value): number | undefined — projects a value onto the x-axis, with internal category-string coercion and ordinal-time key conversion based on axes.x.type.

    • projectY(value, axisId?): number | undefined — likewise for y-axes, with an optional axisId for multi-y-axis charts (default-left fallback).

    • bandwidthOf(axisId?): number — the bandwidth of a band/point scale, 0 for continuous scales.

    The helpers live next to the data (the S2 shape from the analysis): useChartGeometry() returns {axes, plot, projectX, projectY, bandwidthOf}. Internal only — not exported from src/index.ts. Test coverage: 14 unit tests in the new use-chart-geometry.test.ts (all axis-type branches + edge cases).

  • ChartGeometryProviders + ChartGeometryProvidersFromOptions in src/__test-utils__/chart-providers.tsx — test helpers that set up the provider tree for tests that render series renderers, overlays or <SvgRenderer> directly without going through <Chart>. Replaced hand-written providers per test.

Changed (internal)#

  • T7-v2 — 94-site callScale migration. All production call sites of callScale(scale, value) have been replaced by projectX(value) / projectY(value), internalising the category-string coercion + ordinal-time key conversion. 14 files touched:

    FileCalls before → after
    core/overlays/cross-lines.tsx2 → 0 (+ local resolveX helper removed)
    core/overlays/annotations.tsx3 → 0 (+ local resolveX helper removed)
    core/renderer/series/series-accessors.ts3 → 0 (the factory now accepts projectX/projectY as parameters)
    core/renderer/series/histogram-series.tsx3 → 0
    core/renderer/series/range-area-series.tsx4 → 0 (+ inline ordinal-time branch removed)
    core/renderer/series/lollipop-series.tsx5 → 0
    core/renderer/series/dumbbell-series.tsx6 → 0
    core/renderer/series/candlestick-series.tsx6 → 0 (+ ordinal-time ternary removed)
    core/renderer/series/ohlc-series.tsx6 → 0 (+ ordinal-time ternary removed)
    core/renderer/series/range-bar-series.tsx6 → 0
    core/renderer/series/box-plot-series.tsx7 → 0
    core/renderer/series/waterfall-series.tsx8 → 0
    core/renderer/series/bar-series.tsx14 → 0 (stack baseline stays a caller decision, per T1 scope)
    hooks/use-voronoi/use-voronoi.ts18 → 0 (4 composite ternary blocks became 1 line each)

    callScale stays as a low-level primitive in types/scales.ts — used internally by projectX/projectY. No production call sites consume it directly any more.

  • useVoronoi signature — (data, options, xScale, yScales, layout, visibleIndices?) → (data, options, geometry, visibleIndices?). It receives the full ChartGeometry (axes + plot + helpers). Breaking for the exported hook; the only consumer is <Chart> (migrated in the same commit) + the test fixture in chart-interaction.test.tsx.

  • Series-renderer props for LineSeries, AreaSeries, ScatterSeries, CandlestickSeries, OhlcSeries, RangeAreaSeries — the xAxisType: AxisType prop was removed. Series renderers now read the type via useChartGeometry().axes.x.type (where needed) or via the helpers (which process it internally). <SvgRenderer> no longer passes the type to these 6 wrappers.

  • CrossLines props + Annotations props — xScale, yScale, xAxisType removed. The components use useChartGeometry(). Sub-components (CrossLineLine, CrossLineBand) get a bound project: ProjectFn prop instead of scale + isXOrdinalTime.

  • createSeriesAccessors factory — signature (xKey, yKey, xScale, yScale, xAxisType) → (xKey, yKey, projectX, projectY). The pure function now receives the helpers directly; no more internal AxisType discrimination (handled by the helpers).

  • Local resolveX helpers removed in 2 files (cross-lines.tsx, annotations.tsx) — the central helper layer now covers the pre-processing. No "TODO later"; removed immediately per §7.14.

  • toOrdinalTimeKey imports removed from all production files except use-chart-geometry.tsx itself (where it is used by projectThrough).

Tests#

  • 14 new unit tests in use-chart-geometry.test.ts for projectX/projectY/bandwidthOf.

  • 12 test files updated with a ChartGeometryProviders / ChartGeometryProvidersFromOptions wrap so that their TestChart/svg helpers set up the hook tree that useChartGeometry() expects. No test assertion adjusted to mask a refactor behaviour change (stop moment 3): 2047 tests run on exactly the same assertions as pre-v2.

Parked (observation)#

  • The categoryScale/valueScale/lolCategoryScale/lolValueScale local variables in use-voronoi.ts are unused after the migration (4 lint warnings). Per stop moment 1 from the briefing not cleaned up — the variables are still used by halfBandOf(categoryScale) calls, and the broader restructuring falls outside the T7-v2 scope. New trigger T7-v2.1 opened on the roadmap for this clean-up.

Verification#

  • pnpm --filter @workspace/active-charts typecheck — 0 errors.

  • pnpm --filter @workspace/active-charts test — 2047/2047 green.

  • pnpm --filter @workspace/active-charts lint — 23 warnings (the 4 unused locals above plus 19 pre-existing), well within the 100-cap.

  • pnpm typecheck (full monorepo) — 38/38 tasks green, admin/web/auth/archives/atlas unchanged.

  • grep "✅" docs/ROADMAP.md — 0 hits.

[1.0.4] — 2026-05-21#

Added (internal)#

  • T7-v1 — foundation shape for the architecture roadmap (see DECISIONS.md § ADR-009). The new internal module src/hooks/use-chart-geometry/use-chart-geometry.tsx contains:

    • AxisContext (per-axis metadata: id, type, position, scale, reversed).

    • AxesContextValue (x + ys: Map<AxisId, AxisContext>).

    • PlotContextValue (plotWidth, plotHeight, margin, adaptive).

    • AxesContext + PlotContext (React contexts) and the useChartGeometry() hook for future downstream consumers.

    • buildChartGeometry(options, scales, layout), a pure builder that <Chart> synthesises from the useLayout + useScales output.

    Not publicly exported via src/index.ts — an internal API until the shape has been validated through v2/v3.

Changed (internal)#

  • T7-v1 drift test — useVoronoi now consumes AxesContextValue + PlotContextValue instead of loose (xScale, yScales, layout) parameters plus its own options.axes lookup. Concrete deduplication:

    • The xAxisType derivation (options.axes?.find(a => a.position === "bottom" || a.position === "top")?.type) now lives once in buildChartGeometry. useVoronoi reads axes.x.type.

    • Four yAxes.find(a => a.position === "left")?.id sites replaced by one locally derived defaultLeftYAxisId.

    Signature: useVoronoi(data, options, xScale, yScales, layout, visibleIndices) → useVoronoi(data, options, axes, plot, visibleIndices). Breaking for external consumers of the exported hook; the only consumer in the monorepo is <Chart> (migrated in the same commit) plus the chart-interaction.test.tsx fixture (likewise). Runtime behaviour unchanged: 2033/2033 tests green without fixture changes to the assertions.

Type additions#

  • T7-v1 F2 — FlatPoint gets optional fields xAxisId?: string and yAxisId?: string. An additive type extension; no producer populates them yet (that is T7-v3, coupled to T3 for cleaning up the v9.0.0 Finding A xAxisType stopgap in TooltipData). Consumers that do not know the fields keep working; consumers that want to read them get undefined.

Unblocked#

  • T7-v2 (helpers + 119-site migration) and T7-v3 (populate the FlatPoint fields + Finding A clean-up) are open on the roadmap. v1 validated the shape via the sharpest duplication point (the useScales ↔ useVoronoi xAxisType derivation), so v2/v3 can build on it without a shape revision.

[1.0.3] — 2026-05-21#

Added#

  • MP-7 — seven options types now publicly exported via src/index.ts. External adopters could not import these types before because they lived only in the internal ./types barrel: ParentLevelOptions (axis options), DumbbellSeriesOptions + LollipopSeriesOptions (range series), SeriesAnimationOptions (series), NavigatorHandleOptions + NavigatorOutlineOptions (navigator), LabelBackgroundOptions (marker). A purely additive export — no contract change on existing types.

  • TEST-2 — ten .test-d.tsx files added for the generic-preserving wrappers (PieChart, FunnelChart, NightingaleChart, RadialBarChart, RadarChart, HeatmapChart, TreemapChart, SunburstChart, OrgChart, SankeyChart). Verifies at compile time that the generic parameter propagates to the contextMenu item handler and to the data prop. Catches regressions where the runtime stays unchanged but the public type contract would silently break. Compiled by tsc --noEmit; no runtime tests.

Changed#

  • MP-9 — GENEAX_CHART_THEME renamed to ACTIVE_CHARTS_THEME and the corresponding type GeneaxChartTheme to ActiveChartsTheme. The public named-export symbol carried a project brand name in a library API — resolved ahead of any external publication. File move: src/themes/geneax-theme.ts → src/themes/active-charts-theme.ts (plus the test file). 11 source imports, 7 JSDoc references, README.md and docs/DECISIONS.md migrated in the same commit per §7.14. Breaking for the named-export consumer; zero external consumers outside the package (verified with a monorepo grep), so in-package-only impact. No backwards-compat shim left behind.

  • MP-10 — CHANGELOG header extended with an explicit "A note on version-numbering" section documenting the version reset of 2026-05-18 (1.0.0 → … → 9.1.0 → reset → 1.0.0). Prevents confusion on first reading for anyone consulting the git history or old tags.

Internal observations#

  • The three scale/rect primitives from MP-7 (BarRect, BandableScale, ChartScale) are deliberately not publicly exported. Setting up an @workspace/active-charts/internals subpath for these three types would be the first step of the MP-11 workstream (restructuring over-exposed internals); that structural decision falls outside the v1.0.3 scope and will be tackled properly in one go when MP-11 is picked up as a whole. Status quo: the three types remain available within the package via the internal ./types barrel; external consumers cannot import them.

[1.0.2] — 2026-05-21#

Fixed#

  • QR-9 — package.json "sideEffects": false clashed with CSS imports in wrapper components (import "../../styles/accessibility.css" etc.) and in core/overlays/context-menu.tsx. Bundlers were allowed to tree-shake these imports → focus rings, animations, forced-colors and print styles could break silently for external consumers. Replaced by "sideEffects": ["**/*.css"] so that only CSS imports are treated as side effects; JS/TS imports stay tree-shakeable.

  • QR-12 — the heatmap min/max scan in components/heatmap-chart/heatmap-chart.tsx did an O(n) data.map().filter() + two Math.min/max(...spread) per render, regardless of whether data or colorKey changed. Wrapped in a useMemo on [data, colorKey]. No behaviour change — the gradient legend gets the same labels, just no longer recomputed per render. Relevant for large heatmaps (10k+ cells).

Changed (internal)#

  • QR-8 — the generic-preserving cast rolled out to nine standalone wrappers following the HeatmapChart template (PieChart, FunnelChart, NightingaleChart, RadialBarChart, RadarChart, TreemapChart, SunburstChart, OrgChart, SankeyChart). forwardRef strips the generic parameter; the cast reinstates it so that <PieChart<MyRow>> compiles again and context-menu items receive a typed element. No runtime change (the same forwardRef binding), only the public TypeScript type. Five Categorical wrappers cast on TDatum extends ChartDatum; Treemap/Sunburst/OrgChart cast on TExtra extends Record<string, unknown> (OrgChart default { title?: string }); SankeyChart casts on two generics TNodeExtra, TLinkExtra. TimelineChart deliberately excluded — TimelineChartProps has no generic parameter and TimelineEvent is a fixed interface; introducing a generic is an API extension, not a mechanical cast, and falls outside the QR-8 scope (a candidate for a later mini-trigger).

  • QR-11 — pre-1.0.0 BREAKING comment removed from types/chart-options.ts on ChartThemeOverride. The inline v6.0.0 NEW semantics/v6.0.0 NEW field markers on the background/card fields have been replaced by pure semantic descriptions ("Maps to --color-background (outer chart canvas surface)" and "Maps to --color-card (tooltip/label surface)" respectively). The current semantics are already in the bullet list above; the version noise only confused readers of a v1.x baseline.

Unblocked#

  • TEST-2 — type-d tests for the 9 standalone wrappers are now triggerable (without the generic-preserving cast they would have been pointless — they would match all non-generic forwardRef types). Not part of this release.

[1.0.1] — 2026-05-20#

Changed (internal)#

Type-cleanup: @typescript-eslint/no-explicit-any warnings reduced from 26 → 0 across the package. No API surface changes, no runtime behaviour changes, no test changes. Two categories of fix:

  • Drop-as-unnecessary (5 occurrences in utils/indicators.ts): the as any casts on indicator-value assignments to ChartDatum were redundant — number | null | undefined (returned by computeSma/computeEma/macdLine/signalLine) is already a valid ChartDatum value-type. Casts removed; TypeScript still accepts the assignments.

  • Refactor: core/overlays/cross-lines.tsx now uses the centralized callScale helper from types/scales.ts instead of duplicating a local ScaleFn = (value: any) => … type-alias and the as unknown as ScaleFn cast. Same call-shape, one less inline-any.

  • Motivated eslint-disable directives on intentional escape-hatches that cannot be expressed without any:

    • types/scales.ts ChartScale union (block /* eslint-disable */) — d3-scale Range/Output generics are deliberately unconstrained; narrowing to number would forbid colour-output scales, narrowing to unknown would break call-signatures at consumer sites. Runtime discriminators (typeof scale.bandwidth === 'function', …) remain the source of truth.

    • types/scales.ts callScale helper (2 directives) — the polymorphic call-signature on a heterogeneous ChartScale union needs any for both input-domain and invocation. This is the centralized escape-hatch the helper exists to provide.

    • core/axes/axis.tsx (4 directives) — duck-typing access on ChartScale ('ticks' in s, 'domain' in s, 'tickFormat' in s); runtime typeof === 'function' guards remain the discriminator.

    • components/sparkline/sparkline.tsx ScaleFn (1 directive) — bare scaleLinear() infers Range as {}, so the call-site coercion ?? number needs the any-typed indirection.

    • components/nightingale-chart/nightingale-chart.tsx (2 directives) and components/radial-bar-chart/radial-bar-chart.tsx (2 directives) — d3-arc generators require a datum argument in their call-signature, but the generators here have all properties statically configured; null as any is the documented datum-less invocation.

Fixed#

  • QR-4 cascade: useCountUp interpolates 0 → target unconditionally; when consumers passed min > 0 to LinearGauge or GaugeChart combined with animation.type: 'count-up', the unclamped animatedValue flowed into the geometry derivation and produced visual artifacts during early animation frames:

    • LinearGauge: frac = (animatedValue - min) / (max - min) went negative → <rect> width/height attributes went negative → SVG rejected with a console warning.

    • GaugeChart: effectiveValueForAngle flowed through valueToAngle() → needle angle below svgStart → needle rendered far outside the gauge arc.

    • Same mechanism as the fixed BulletChart bug (v9.0.0 Issue 3D). Fix: clamp animatedValue to [min, max] before deriving geometry. Visually-identical end-state (count-up still snaps to exact target at completion); only intermediate animation frames change.

    • Latent bug — only triggered by min > 0 + count-up; default consumer paths (min: 0) unaffected.

Added#

  • LinearGauge regression-test: asserts non-negative <rect> width/height on first count-up frame when min > 0.

  • GaugeChart regression-test: asserts needle angle stays within [svgStart, svgEnd] on first count-up frame when min > 0.

  • QR-1 scan report (horizontal-mode axis merges, ternary inversions, as never casts in all Cartesian wrappers) drawn up as input for the T5 workstream. The findings were handled in T5-v1 (v1.3.2) via compileAxisPair; the scan report was removed in the docs clean-up of 2026-05-22.

[1.0.0] — 2026-05-18#

External-publishability milestone. Active-charts is now conceptually clean, has one unified API for every cross-cutting concern, and commits to an API-stability promise from this version onward. The version reset from 9.1.0 to 1.0.0 marks the transition from internal-only iteration to a publishable v1 baseline; the 1.x–9.x internal history lives on in the README opening section.

The headline change is the unified, element-aware context-menu API: every chart-type in active-charts now surfaces a typed ContextMenuItem<TElement> whose action(element) handler receives the chart-element under the cursor (cell, slice, point, node, link, hierarchy node, …) at right-click time. Previously, only HeatmapChart carried this — and via heatmap-specific types (T-GX-AC-01). This release removes those specific types and reroutes every standalone chart and the low-level <Chart> through one helper.

BREAKING#

  • ContextMenuItem and ContextMenuOptions are now generic on a TElement parameter that defaults to void. Predicate-style hidden and disabled accept the element. Boolean values continue to work unchanged. Item handlers may continue to be written as () => void — TypeScript accepts the narrower signature thanks to parameter contravariance, so existing call sites compile without change.

  • Removed: HeatmapCell-specific item types HeatmapContextMenuItem<TDatum> and HeatmapContextMenuOptions<TDatum>. Consumers use ContextMenuItem<HeatmapCell<TDatum>> and ContextMenuOptions<HeatmapCell<TDatum>> instead. HeatmapCell<TDatum> itself is retained and exported.

  • Archetype mixins now narrow contextMenu on their TDatum/TExtra parameter: CartesianChartProps<TDatum> → ContextMenuOptions<CartesianPoint<TDatum>>; CategoricalChartProps<TDatum> → ContextMenuOptions<CategoricalSlice<TDatum>>; HierarchicalChartProps<TExtra> → ContextMenuOptions<HierarchyElement<TExtra>>; GraphChartProps → ContextMenuOptions<SankeyElement<TNodeExtra, TLinkExtra>>. BaseChartProps.contextMenu stays element-agnostic (ContextMenuOptions<any>) so scalar charts and <ChartDefaults> can pass plain toggles.

  • useStandaloneContextMenu signature: was positional (contextMenuOpt, chartName, externalSvgRef?), is now a single options object {contextMenu, chartName, externalSvgRef?, resolveElement?}. Wrappers that delegate to this hook supply a resolveElement callback that maps a MouseEvent (or the keyboard-focused index) to the chart's TElement.

Added#

  • Element-shape exports for every archetype that publishes context data: HeatmapCell, CategoricalSlice, CartesianPoint, HierarchyElement, SankeyElement, TimelineElement, RadarPoint. See MIGRATION-NOTES.md for the recipe and the README "Context menus" chapter for usage examples per chart-type.

  • All eleven standalone wrappers (Heatmap, Pie, Funnel, Nightingale, RadialBar, Radar, Treemap, Sunburst, OrgChart, Sankey, Timeline) now resolve a typed element at right-click and at keyboard-activation time. Custom items with predicate-style hidden / disabled can branch on the element; sparse positions (no element under the cursor) hide the item by default, mirroring the original heatmap rule.

  • All thirteen cartesian wrappers (Line, Area, Bar, Scatter, Histogram, Waterfall, Box-plot, Candlestick, Ohlc, Dumbbell, Lollipop, Range-area, Range-bar) surface a CartesianPoint<TDatum> element resolved from the active hover point.

Changed#

  • <Chart> (low-level) binds the active hover point into custom contextMenu.items[] handlers as CartesianPoint. The default export submenu (PNG/SVG/CSV/PDF) and zoom/pan items are unchanged.

  • ChartDefaultsValue.contextMenu is FeatureOption<ContextMenuOptions<any>> — the cross-chart provider can carry common toggles (enabled, hideDefaults, pngScale) for charts of any archetype.

Notes for consumers#

The 9.1 migration recipe for heatmap is now obsolete; see MIGRATION-NOTES.md § "0.x / 9.x → 1.0.0" for the consolidated recipe (covers heatmap and every other chart-type).

[9.1.0] — 2026-05-18#

Adds cell-data pass-through to HeatmapChart's context menu (T-GX-AC-01). Item handlers now receive the right-clicked cell ({datum, x, y, value}), enabling drill-down menus such as "open records for this cell" or "open administrative place" without the former <details> cell-list workaround under the chart.

See MIGRATION-NOTES.md for the consumer migration recipe.

Added#

  • HeatmapCell<TDatum> type on the public surface: {datum, x, y, value}. datum is null for sparse cells (no data point at the hovered grid intersection).

  • HeatmapContextMenuItem<TDatum> — mirrors the cross-chart ContextMenuItem but with cell-aware handlers: action: (cell) => void, hidden: boolean | ((cell) => boolean), disabled: boolean | ((cell) => boolean).

  • HeatmapContextMenuOptions<TDatum> — the new shape for HeatmapChartProps.contextMenu. Carries items: HeatmapContextMenuItem<TDatum>[] plus the cross-chart enabled, hideDefaults, pngScale toggles.

  • HeatmapChart is now generic-preserving via the standard forwardRef + as-cast pattern used by AreaChart, BarChart, etc. Consumers write <HeatmapChart<MyDatum>> to get typed cells in their menu handlers.

Changed#

  • HeatmapChartProps.contextMenu is now FeatureOption<HeatmapContextMenuOptions<TDatum>> (was FeatureOption<ContextMenuOptions>). The shape change is opt-in: charts that only pass true / false / {enabled, hideDefaults, pngScale} are unaffected.

  • HeatmapChart resolves the right-clicked cell from the SVG event coordinates (using getBoundingClientRect + MARGIN + cellWidth / cellHeight), captures it in component state, and binds it to every user-supplied item's action, hidden, and disabled callback at menu-render time.

  • HeatmapChart is excluded from <ChartDefaults> context-merge for the contextMenu field — its cell-aware items have no meaningful cross-chart baseline. Other fields (tooltip, animation, accessibility, …) continue to merge normally.

Notes for consumers#

  • contextMenu={true} continues to produce the export defaults only — no behaviour change for the common case.

  • The default <ChartContextMenu> rendering, the long-press touch handler, and the existing keyboard-navigation paths (Matrix archetype) are unchanged.

  • Sparse-cell handling: when a user right-clicks a grid intersection with no datum (the heatmap's data array doesn't carry that combination), cell.datum is null. Use hidden: (cell) => cell.datum === null to omit drill-down items that need a real row, or check cell.datum != null inside the action.

  • The legacy <details> cell-list drill-down workaround under the gap-analysis heatmap (apps/archives/.../gap-analysis-heatmap.tsx) was removed in this release. The same destinations are now reached via the cell-aware context menu.

Inventory (for follow-up triggers)#

Other chart types still ship a contextMenu: FeatureOption<ContextMenuOptions> prop whose items lack click-element context. Potential follow-up triggers (none filed yet):

  • PieChart / Donut / Funnel / Nightingale — could carry a PieSlice<TDatum> shape (label, value, datum, fraction) on items.

  • BarChart / Histogram / WaterfallChart — could carry a BarPoint<TDatum> (x, y, datum, series).

  • ScatterChart / BubbleChart / LineChart — could carry a Point<TDatum> (x, y, datum, series).

  • SankeyChart — node-vs-link click context.

  • TreemapChart / SunburstChart / OrgChart — node-with-path context.

  • GraphChart — node-vs-link click context.

The standalone useStandaloneContextMenu hook could grow a generic element parameter to absorb this pattern across charts — same "capture element at right-click, wrap items at render time" shape used here.

This page as Markdown