# Chart family — tokens, events, and the a11y contract

## Token roster (`chart-ui`, `@scope (chart-ui)`)

| Token | Controls | Default |
| --- | --- | --- |
| `--chart-fg` / `--chart-label` / `--chart-value` | axis text, tick labels, value overlays | `--md-sys-color-neutral-*` roles |
| `--chart-bar` / `--chart-line` / `--chart-dot` | series fill/stroke when NOT per-series-keyed (single-series types) | `--md-sys-color-primary` |
| `--chart-bar-hover` | bar hover fill | `--a-primary-bg-hover` |
| `--chart-grid` / `--chart-axis` / `--chart-border` | gridlines, axis line, empty-state dashed border | `--md-sys-color-neutral-outline-variant` family |
| `--chart-avg` | the optional overlaid average line (bar/line) | `--md-sys-color-warning` |
| `--chart-area-opacity` | line-chart area fill opacity (`type="line"`'s `[data-area]`) | `0.15` |
| `--chart-area-fill-opacity` | `type="area"`'s OWN fill opacity — overrides `--chart-area-opacity` for that type so the fill reads as the dominant series, not a soft accent | `0.35` |
| `--chart-line-width` | line/multi-line stroke width | `2` |
| `--chart-radius` | internal-legend swatch DOT corner radius only (`[data-legend-dot]`) — NOT the bar/donut/treemap corner radius; that fallback chain is `radius` PROP → `--a-radius` (computed, ancestor-set) → `6`, resolved per-render by `#resolveRadius()` | `4` |
| `--chart-max-height` | responsive height cap | `28rem` |
| `--chart-dot-stroke` | line-chart dot outline color | `--md-sys-color-neutral-background` |
| `--chart-duration` / `--chart-easing` | hover/transition timing | `--a-duration-fast` / `--a-easing` |
| `--chart-legend-size` / `--chart-legend-dot-size` | internal auto-legend sizing (pie/donut/stacked-bar/grouped-bar/multi-line/radial-bar) | — |
| `--chart-segments-gap` / `--chart-pie-gap` | gap between segments/pie slices | `2px` / `1.25px` |
| `--chart-currency-prefix` | `format="currency"` prefix string | `"$"` (falls back in JS — don't rely on a `:where(:scope)` default cascading) |
| `--chart-perf-budget` | row count before the one-shot perf warning fires | `5000` |
| `--chart-0` … `--chart-9` | the 10-slot categorical ramp, aliasing `--a-data-0`…`--a-data-9` | theme-stable data palette |

Every color-bearing token above resolves through `--md-sys-color-*` or `--a-*` — never author a
raw hex/`oklch()` override; recolor by overriding the alias, same law `token-selection` states for
every other surface.

## Per-series color override — `--color-{key}`

For multi-series charts (`y="revenue,users"`), `chart-ui` injects one inline custom property per
declared series onto the HOST at render time: `--color-revenue: var(--chart-0)`,
`--color-users: var(--chart-1)`, etc. (slot = index into the 10-slot ramp, wrapping past 10).
Every series-keyed SVG element's fill/stroke reads `var(--color-{key}, var(--chart-{slot}))`.

To recolor ONE named series without touching the rest, set `--color-{key}` on the chart or any
ancestor:

```css
chart-ui#revenue-trend { --color-users: var(--md-sys-color-tertiary); }
```

`chart-legend-ui` reads the SAME `--color-{key}` variable for its swatches (falling back to
`--a-data-{slot}` directly, since a standalone legend may have no chart-ui ancestor to inherit
the chart-scoped `--chart-{N}` alias from) — override once, chart and legend swatch stay in
sync automatically.

## Event contract

All four event NAMES bubble and are shared by `chart-ui` and `heatmap-ui` (heatmap re-emits its
own `cell-hover`/`cell-click` under these same names so a listener never needs to branch on
chart kind) — the DETAIL shape differs in the two ways called out below the table, so treat
`chart-ui`'s shape as the fuller case, not the shared contract:

| Event | Fires | Detail |
| --- | --- | --- |
| `chart-hover` | pointer enters a datum (bar/dot/slice/cell); re-fires only when the hovered datum CHANGES | `{ label, value, pct, series, slot, payload, pointerX, pointerY }` |
| `chart-leave` | pointer leaves the plot area, or a previously-hovered datum with nothing new entering | none |
| `chart-select` | click/tap on a datum, or Enter/Space on the keyboard-focused datum | same shape as `chart-hover` (pointer coords are synthesized at the focused datum's center for keyboard-triggered selects, so `pointerX`/`pointerY` are always populated) |
| `legend-update` | the chart's internal `legendData` payload regenerates | none — signal-only; `chart-legend-ui[for]` listens for this to repaint (chart-ui only — see the wiring section below) |

`detail.payload` is an array, one entry per series AT THE SAME X COLUMN (multi-series types) —
`{ series, label, value, pct, slot, hovered }` per entry — so a single `tooltip-ui` can render
every series' value for the hovered category, not just the one datum the pointer is literally
over. Single-series types populate `payload` with one entry mirroring the top-level fields.
**`heatmap-ui`'s detail has no `payload` field at all** (cells are independent, not columns of a
shared series) — a shared tooltip must treat `payload` as optional. `heatmap-ui` also re-fires
`chart-hover` on every pointer move inside the SAME cell (to reposition a pointer-follow
tooltip), which the "only fires when the hovered datum changes" row above describes for
`chart-ui`, not for `heatmap-ui` — dedupe on `(r, c)` if a listener needs change-only semantics
for heatmap cells.

Wire ONE `tooltip-ui[follows="pointer"][for="chart-id"]` rather than a per-chart-kind listener —
its presence suppresses `chart-ui`'s internal tooltip popup automatically (detected via
`document.querySelector('tooltip-ui[follows="pointer"][for=...]')`), so there's no double-render
to guard against.

## `chart-legend-ui` wiring

- `[for="chart-id"]` auto-mirrors **`chart-ui` targets only**: the legend reads the target's
  `.legendData` property and re-paints on that target's `legend-update` event. Requires the
  chart to have a matching `id`. Pointing `[for]` at a `heatmap-ui` id silently resolves to an
  empty legend — heatmap-ui never sets `.legendData` or dispatches `legend-update`; use its own
  built-in Less/More strip (`no-legend` prop to hide it) instead.
- `items='[{"key","label","slot?","pct?"}, …]'` is explicit data and ALWAYS wins over `[for]`
  when both are present — use it for a legend with no live chart-ui counterpart.
- Clicking a row fires `toggle` (`{ key, active, mode }`); when wired via `[for]`, `chart-ui`
  listens for this on `document` and hides (`on-toggle="hide"`, default) or fades
  (`on-toggle="opacity"`) that series — no manual event handler needed for the common case.
- `static` renders non-interactive `badge-ui` rows (no `role`/`tabindex`, no toggle affordance)
  for a legend that's pure labeling, not a filter control — rows are `badge-ui` chips in both
  modes (composed, not hand-rolled `<span>`s, since 2026-05-01).
- `shape="dot|square|line|dashed"` — `line`/`dashed` exist because those swatch shapes aren't
  representable as an icon glyph; match `line`/`multi-line` charts to `shape="line"`,
  categorical charts to `dot` or `square`.

## Responsive sizing detail

Font/label sizing auto-classes to `sm`/`md`/`lg` off the smallest rendered dimension — the
`size` prop overrides the auto-class when a fixed density is wanted regardless of container.
Currency's `$` prefix is itself a token (`--chart-currency-prefix`), so locale retuning never
touches markup.

## Accessibility & keyboard

`chart-ui` sets `role="img"` and an auto-generated `aria-label` ("`{type} chart`") unless the
author supplies their own. It is natively keyboard-navigable (`tabindex="0"` auto-applied):

| Key | Action |
| --- | --- |
| `ArrowRight` / `ArrowDown` | move virtual focus to the next datum |
| `ArrowLeft` / `ArrowUp` | move virtual focus to the previous datum |
| `Home` / `End` | jump to first / last datum |
| `Enter` / `Space` | fire `chart-select` for the focused datum |
| `Escape` | clear focus, fire `chart-leave` |

Keyboard focus emits the SAME `chart-hover` event the pointer path uses, so a
`tooltip-ui[follows="pointer"][for]` tracks keyboard navigation for free — author it once and
both interaction modes work. Per-datum focus paints via `[data-a11y-focus]` + the standard
`--a-focus-ring` recipe; nothing extra to author.
