<!-- GENERATED by scripts/build-llms.mjs from llms/charts.md — do not edit this file. -->

# `lr-box-plot`

- **Import** `import '@aceshooting/lyra-ui/components/lr-box-plot.js';` (stable tag alias; registers the tag)
- **Class** `LyraBoxPlot`, also available unregistered from `@aceshooting/lyra-ui/components/charts/chart/box-plot.class.js`
- **Family** `components/charts/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** `@sgratzl/chartjs-chart-boxplot`, `chart.js` — see `llms/peers.md`
- **Themeable via** 12 parts, 33 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-box-plot`

Box-and-whisker chart from a precomputed five-number summary (no raw sample data sent to the
browser). Does **not** extend `LyraChart` — a deliberately bespoke API.

With `IntersectionObserver` available, canvas construction waits for its first delivered visibility
decision. An off-screen box plot stays unconstructed until visible. Without the observer, drawing
starts when the peers and canvas are ready; an empty delivered callback retains the visible
fallback. Peer loading and accessible DOM can settle while visibility is pending. These rules also
apply when the component reconnects.

**Properties:**
- `labels: readonly string[] = []` (attribute: false)
- `datasets: readonly LyraBoxPlotSeries[] = []` (attribute: false) — each series contains readonly
  `LyraBoxPlotSummary { min, q1, median, q3, max }` values. Summaries must be finite and ordered
  `min <= q1 <= median <= q3 <= max`; invalid entries are omitted and caller objects are never
  passed to the mutating peer. A runtime series whose required `data` member is not an array is
  dropped while valid siblings continue to render.
- `hiddenDatasets?: readonly number[]` (attribute: false) — complete controlled visibility snapshot
  for the DOM legend. `undefined` leaves every box series visible; `[]` likewise explicitly makes
  every series visible, while a defined canonical list of zero-based indexes hides those series.
  Duplicate, non-integer, negative, and out-of-range indexes are discarded. Accepted user toggles
  write their complete next snapshot back to this property; programmatic writes reconcile silently.
- `labels`, `datasets`, and `hiddenDatasets` are clone-owned, bounded, frozen snapshots. Mutating a
  previously assigned array or nested series data has no effect; create and reassign a new
  collection.
- `legend: boolean = false` — renders a wrapping DOM legend whose buttons toggle box-series
  visibility without clipping long labels.
- `legendPosition: 'top'|'bottom'|'start'|'end' = 'bottom'` (attribute `legend-position`) — logical,
  responsive DOM legend placement
- `height: string = '280px'` — valid CSS height used as a private fallback only. A consumer-set
  `--lr-chart-height` always wins; invalid values remove the fallback and leave the public
  token/default in control.
- `yLabel: string = ''` (attribute `y-label`)
- `beginAtZero: boolean = true` (attribute `begin-at-zero`)
- `label: string | null = null`, `description: string | null = null` — canonical accessible name
  and description; host `aria-label` wins by presence, including an explicit empty string
- `formatter?: LyraChartFormatter`, `valueFormatter?: LyraChartValueFormatter` — numeric axis,
  tooltip, table, summary, and export formatting. The context-object formatter receives the
  family-wide `spoken` surface for the generated summary and `export` for CSV cells; the legacy
  positional formatter receives `table` for the spoken/export compatibility paths, its normal
  surface name for axis, tooltip, and table work, and no fallback for a `visual` context. The
  context-object formatter takes precedence. Tick calls name `axis: 'y'`, and a tooltip value now
  carries the hovered datum's `datasetIndex`, `index`, `label`, `seriesLabel` and
  `statistic: 'median'` instead of discarding what the callback was handed.
- `showDataTable: boolean = false` (attribute `show-data-table`) — reveals the accessible data table
- `dataTableToggle: boolean = false` (attribute `data-table-toggle`, new in 11.0.0) — renders a
  localized disclosure button (`part="data-table-toggle"`) above the data table so a *sighted*
  reader can reveal the numbers on demand. `showDataTable` alone is all-or-nothing, which left
  consumers wrapping a duplicated table in their own `<details>`. With the toggle on,
  `showDataTable` becomes the disclosure's **initial** state rather than its whole behavior; the
  table stays in the DOM in both states, so assistive technology never loses it, and the button
  carries `aria-expanded` plus `aria-controls` pointing at the `data-table` wrapper. A supplied
  `slot="data-table"` follows this same disclosure state. Unset, nothing renders and behavior is
  identical to before.

**Methods:** `exportData('csv'|'png')` returns spreadsheet-safe summary rows or the current canvas
PNG data URL. `refreshTheme()` re-reads canvas theme custom properties after an ancestor theme
change. Canvas work remains connected/visible-gated, while a rendered DOM legend also refreshes
its computed color swatches.

**Events:** `lr-datum-activate` (canonical detail with `kind: 'box'`), `lr-point-click`
(compatibility), `lr-before-legend-visibility-change` (cancelable proposed legend
toggle) and `lr-legend-visibility-change` (accepted commit). The two legend events carry
`{ datasetIndex: number, visible: boolean, hiddenDatasets: readonly number[] }`, where
`hiddenDatasets` is the complete sorted, valid next snapshot. Calling `preventDefault()` on the
proposal leaves state untouched and suppresses the commit event.

`lr-point-click` fires when pointer input lands on a box, or when Enter/Space activates the
keyboard-current box — the same event name and role `lr-chart` and `lr-lite-chart` expose. Its
`detail` is `{ datasetIndex: number, index: number, label: string | undefined, value: LyraBoxPlotSummary |
null }`, where `value` is a fresh copy of that box's five-number summary (never the object you
passed in `datasets`, which the underlying peer may annotate in place). A pointer click that misses every
box emits nothing rather than reporting the nearest one.

**Per-box keyboard access:** the `canvas` part is a focusable `role="application"` surface.
Arrow keys walk the boxes one at a time (Left/Right swap under RTL; Up/Down always mean
previous/next), Home/End jump to the first/last, and Enter or Space activates the current box. Each
move announces that box's series, category, and complete five-number summary through the shared
document-level light-DOM polite sink. The walk visits the same bounded, deterministic sample the
generated data table uses, so a very wide data set stays navigable.

**Slots:** `data-table` — an optional consumer-provided complete, paginated, or virtualized
accessible table alternative.

**Bounded data alternative:** the generated table, automatic canvas name, and generated per-series
description use at most 1,000 category×series records. When sampling is needed, its category and
series indexes are deterministic and retain the first and last endpoint; a localized
`data-truncation` notice is shown and announced. A slotted `data-table` replaces the generated
detailed sample and notice, making it the escape hatch for complete data.

**CSS parts:** `base`, `plot` (the fixed-height canvas region), `canvas`, `legend`,
`legend-item`, `legend-item-hidden` (added to a legend item while its box series is hidden),
`legend-swatch`, `description`, `data-table`, `data-table-toggle` (the `dataTableToggle` disclosure button), `error` (neutral visible message shown
instead of `canvas` when the optional box-plot peer fails to load; the failure transition is
announced through the shared document-level light-DOM assertive sink), `data-truncation` (the
bounded-alternative sampling notice)

**Themeable custom properties:** `--lr-chart-height`, `--lr-chart-grid-color`,
`--lr-chart-tick-color`, `--lr-chart-tick-font-size`, `--lr-chart-legend-color`, `--lr-chart-tooltip-bg`,
`--lr-chart-tooltip-text` — same public host-level precedence, token names, and defaults as `lr-chart`
(also `getComputedStyle`-resolved and CSS-color-validated on every draw; invalid expressions use
concrete semantic fallbacks rather than retaining a prior canvas paint), but declared in its own stylesheet, not a
re-export: `lr-box-plot` has no `zoom`, so no `reset-zoom-button` chrome exists here. A `BoxPlotSeries`
that sets no `color` is assigned an entry from the same `--lr-color-chart-1..8` ramp `lr-chart` uses,
so `--lr-theme-color-chart-*` retheming reaches box plots too. That resolved color then layers two
further per-series override tokens for the canvas paint, each wrapping modulo 8 like the underlying
ramp and mirroring `lr-chart`'s own `--border-color-N`/`--fill-color-N` palette-override mechanism
under a box-plot-namespaced name, since box-plot has no raw `config` passthrough to piggyback on:
`--lr-box-plot-border-color-1` through `--lr-box-plot-border-color-8` (defaulting respectively to
`--lr-color-chart-1` through `--lr-color-chart-8`) set the box-outline stroke color, and
`--lr-box-plot-fill-color-1` through `--lr-box-plot-fill-color-8` (the same eight defaults) set the
box fill and its legend swatch. In full, the stroke tokens are `--lr-box-plot-border-color-1`,
`--lr-box-plot-border-color-2`, `--lr-box-plot-border-color-3`, `--lr-box-plot-border-color-4`,
`--lr-box-plot-border-color-5`, `--lr-box-plot-border-color-6`, `--lr-box-plot-border-color-7` and
`--lr-box-plot-border-color-8`; the fill tokens are `--lr-box-plot-fill-color-1`,
`--lr-box-plot-fill-color-2`, `--lr-box-plot-fill-color-3`, `--lr-box-plot-fill-color-4`,
`--lr-box-plot-fill-color-5`, `--lr-box-plot-fill-color-6`, `--lr-box-plot-fill-color-7` and
`--lr-box-plot-fill-color-8`. `--lr-box-plot-border-width` (default `var(--lr-border-width-thin)`)
sets the canvas box-outline stroke width in pixels — the same override mechanism as `lr-chart`'s
`--border-width`. `--lr-box-plot-item-radius` (default `0`) sets the radius, in pixels, of the
individual raw-sample dots drawn alongside each box; `0` disables them. `--lr-chart-pattern-step`
(default `var(--lr-space-2xs)`) sizes the forced-colors legend texture and
`--lr-chart-canvas-hover-outline-width` (default `var(--lr-border-width-thin)`) sizes the `canvas`
hover outline; `--lr-chart-canvas-hover-outline-color` (default `var(--lr-chart-grid-color)`) sets
its color. `--lr-chart-legend-item-active-bg` and `--lr-chart-legend-item-hover-bg` retune the
pressed and hovered legend rows, and `--lr-chart-legend-side-max` caps a side legend — the same tokens and defaults as
`lr-chart`. Its own `dataTableToggle` disclosure button carries box-plot-namespaced hooks rather
than inheriting the chart pair, since its stylesheet is not a re-export:
`--lr-box-plot-data-table-toggle-hover-bg` (defaults to `--lr-color-brand-quiet`) and
`--lr-box-plot-data-table-toggle-active-bg` (defaults to its standard active color mix).

**Forced colors:** under `forced-colors: active` the eight-color ramp is remapped onto the small
repeating system-color cycle the platform exposes, so series 1/4/7 (and 2/5/8, 3/6) would otherwise
paint identically. Each box's fill is therefore textured with a per-series pattern, and its legend
swatch carries the matching CSS texture — the same eight-way encoding `lr-chart` applies to its own
repeated colors. Box-and-whisker elements expose no border-dash or point-style option, so texture is
the only channel here; nothing is opt-in and no author color is substituted.

**Optional peer deps:** `@sgratzl/chartjs-chart-boxplot` plus `chart.js`; both validated capability
loads are memoized per page.

```html
<lr-box-plot y-label="Latency (ms)"></lr-box-plot>
<script>
  const bp = document.querySelector('lr-box-plot');
  bp.labels = ['Run A', 'Run B'];
  bp.datasets = [{ label: 'p50–p99', data: [{ min: 10, q1: 20, median: 30, q3: 45, max: 90 }, { min: 12, q1: 18, median: 25, q3: 35, max: 60 }] }];
</script>
```

**Known gotchas:**
- no raw `config` passthrough — limited to the properties above, plus the `--lr-box-plot-border-width`
  and `--lr-box-plot-item-radius` CSS hooks; the underlying controller's other options
  (`outlierRadius`, `coef`) remain unreachable.
- Chart.js receives `effectiveLocale`; generated numeric summaries use it, the y axis moves to
  logical start in RTL, and live ancestor `lang`/`dir` changes redraw the already-mounted canvas
  without requiring another box property write. Canvas tooltip/axis colors are token-driven, and
  animation is disabled under reduced motion.
- `--lr-chart-height` fixes the `plot` height and the host's minimum height, not the complete host.
  It is consumer-owned and wins over the `height` property's private fallback. A visible or slotted
  table and the wrapping legend remain in normal document flow, grow the
  component, and cannot overlap following content; oversized tables scroll inside the host.
- If `@sgratzl/chartjs-chart-boxplot` fails to load, the component warns to the console and
  fails closed with a localized, neutral visible error part rather than leaving a blank canvas.
  The transition into that state is announced through the shared document-level light-DOM
  assertive sink.

---
