---
id: "packages/active-charts/changelog"
title: "Changelog"
description: "What changed for users of active-charts, per version."
url: "/packages/active-charts/changelog"
locale: "en"
revision: "1cd8f16a90355f5c"
---

# Changelog

> [!NOTE]
> **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](https://keepachangelog.com/en/1.1.0/); versioning per
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

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 `yAxisId`s, 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.

### Fixed (gallery findings, ADR-080, 2026-10-03)

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:

| Type | Pre-v1.7.0 status | QR-5 outcome |
| --- | --- | --- |
| **Histogram** | Path B via `emitPadBFlatPoints` | Path C added via shared helper |
| **Waterfall** | Path B via `emitPadBFlatPoints` | Path C added via shared helper |
| **RangeBar** | **No** 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 |
| **BoxPlot** | **No** 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

| Location | Pre-v1.6.3 | Post-v1.6.3 |
| --- | --- | --- |
| 11 wrappers × inline 22-line block | ~242 lines copy-paste | 11 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

| Location | Pre-v1.6.2 | Post-v1.6.2 |
| --- | --- | --- |
| HeatmapChart `useEffect` block for `onAnimationComplete` | ~10 lines | 6 lines (`useAnimationCompleteTimer` call) |
| PieChart likewise | ~10 lines | 6 lines |
| HeatmapChart inline dark-mode detection + fallback | ~30 lines (`useState`+`useEffect`+fallback branch) | gone |
| HeatmapSeries dark fallback (`DEFAULT_COLOR_RANGE_DARK` + `isDark` choice) | ~6 lines | gone |
| `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

| Category | Count | Effect | Migration needed? |
| --- | --- | --- | --- |
| Production dashboard (no tooltip prop) | 2 (`session-stats-chart.tsx`, `login-stats-chart.tsx` in admin) | Default-on tooltip on hover | **No — desired behaviour** |
| Production explicit (analytics + heatmap) | 4 | No change | n/a |
| Test-chart demos | ~92 chart usages across 3 view files | Most already had an explicit tooltip; charts without one get default-on in a demo context | **No — exactly the demo's goal** |
| 11 standalone wrappers (internal) | n/a | Flip automatically via `normalizeTooltipOption` | **No — automatic** |
| Active-charts unit tests | 2102 tests | No test asserted `normalizeTooltipOption(undefined) === undefined` | **No — 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

| Location | Pre-T4 | Post-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.ts` | 67 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:

  | Wrapper | Pre-T5-v1 | Post-T5-v1 |
  | --- | --- | --- |
  | `bar-chart.tsx` (lines 336-347) | 12 lines swap + 2× `as never` | 5 lines helper call |
  | `dumbbell-chart.tsx` (lines 174-185) | 12 lines swap | 5 lines helper call |
  | `range-bar-chart.tsx` (lines 147-158) | 12 lines swap | 5 lines helper call |
  | `lollipop-chart.tsx` (lines 184-195) | 12 lines swap | 5 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-in | hitTest | render | accessibleColumns | Sub-discrimination inside plug-in |
  | --- | --- | --- | --- | --- |
  | `line` | ✓ Path B | ✓ | ✓ single-yKey | — |
  | `area` | ✓ Path B | ✓ | ✓ single-yKey | — |
  | `scatter` | ✓ Path B + sizeKey | ✓ | ✓ single-yKey | sizeKey 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-yKey | horizontal 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:

  | File | Calls before → after |
  | --- | --- |
  | `core/overlays/cross-lines.tsx` | 2 → 0 (+ local resolveX helper removed) |
  | `core/overlays/annotations.tsx` | 3 → 0 (+ local resolveX helper removed) |
  | `core/renderer/series/series-accessors.ts` | 3 → 0 (the factory now accepts `projectX`/`projectY` as parameters) |
  | `core/renderer/series/histogram-series.tsx` | 3 → 0 |
  | `core/renderer/series/range-area-series.tsx` | 4 → 0 (+ inline ordinal-time branch removed) |
  | `core/renderer/series/lollipop-series.tsx` | 5 → 0 |
  | `core/renderer/series/dumbbell-series.tsx` | 6 → 0 |
  | `core/renderer/series/candlestick-series.tsx` | 6 → 0 (+ ordinal-time ternary removed) |
  | `core/renderer/series/ohlc-series.tsx` | 6 → 0 (+ ordinal-time ternary removed) |
  | `core/renderer/series/range-bar-series.tsx` | 6 → 0 |
  | `core/renderer/series/box-plot-series.tsx` | 7 → 0 |
  | `core/renderer/series/waterfall-series.tsx` | 8 → 0 |
  | `core/renderer/series/bar-series.tsx` | 14 → 0 (stack baseline stays a caller decision, per T1 scope) |
  | `hooks/use-voronoi/use-voronoi.ts` | 18 → 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.

---
