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

# `lr-lite-chart`

- **Import** `import '@aceshooting/lyra-ui/components/lr-lite-chart.js';` (stable tag alias; registers the tag)
- **Class** `LyraLiteChart`, also available unregistered from `@aceshooting/lyra-ui/components/charts/chart/lite-chart.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** none
- **Themeable via** 18 parts, 19 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-lite-chart`

A dependency-free bar/line chart — plain SVG/DOM rendering, zero peer dependencies (unlike
`lr-chart`, which wraps `chart.js`). For a project whose architecture forbids a charting
dependency outright: covers grouped/stacked bars, multi-series lines, per-point click, and hover
tooltips (native SVG `<title>`, no positioning JS) — not a full `lr-chart` replacement (no
zoom/pan, no pie/doughnut/radar/scatter/bubble types, no horizontal/dual-y-axis, no raw-config
passthrough). Not a subclass of `LyraChart`.

Deliberate omissions, assessed and not implemented: `lr-chart`'s per-series `LyraChartSeries.stack`
group and per-axis `stackedAxes` have no `lr-lite-chart` counterpart. `stacked` here is already
chart-wide only (see below) and this component has exactly one value scale — no `y2` — so "an
unstacked overlay on a second axis," the motivating case for `stackedAxes`, has no equivalent
shape to express. A per-series stack-group id would also need the hand-rolled SVG bar-geometry
pass (linear/sqrt/log stack compression, `minBarHeight`) to track independent running offsets per
group instead of one per category, which is a materially larger, higher-risk change than this
component's existing single-stack model. `tooltipTitleFormatter`/`tooltipFooterFormatter` are
similarly absent: this component's hover tooltip is a native SVG `<title>` on each mark — one
self-contained string per mark, generated by `pointText` — not a Chart.js-style multi-item tooltip
with separate title/body/footer regions for several datasets sharing a hovered category, so there
is no "every item in the tooltip" surface to hook a title or footer formatter onto.

**Properties:**
- `type: LyraLiteChartType = 'bar'` — `'bar' | 'line'`
- `labels: readonly string[] = []` (attribute: false)
- `datasets: readonly LyraLiteChartSeries[] = []` (attribute: false) —
  `LyraLiteChartSeries { readonly label: string; readonly data: readonly (number|null)[];
  readonly color?: string }`. The legacy `LiteSeries` name was removed in 9.0.0 — import
  `LyraLiteChartSeries` instead.
  `color` accepts a valid CSS `color`, while invalid values,
  declaration-breaking input, and `url()` paint servers fall back to the built-in palette. A
  runtime entry whose required `data` member is not an array is dropped while valid siblings
  continue to render.
- `legend: boolean = false`
- `legendPosition: 'top'|'bottom'|'start'|'end' = 'bottom'` (attribute `legend-position`) — logical
  placement for the DOM legend; side positions are bounded and stack responsively in narrow hosts
- `label: string | null = null`, `description: string | null = null` — canonical accessible name
  and description; host `aria-label` wins by presence, including an explicit empty string
- `accessibleLabel?: string` (attribute `accessible-label`) — overrides the `<svg>`'s auto-derived
  `aria-label` (`datasets.map(d => d.label).join(', ') || 'Chart'`); a host `aria-label` still wins.
  Unset keeps the auto-derived (English-fallback) label. `lr-lite-chart` keeps this property under
  its original `accessible-label` name, unrelated to the deprecated `accessible-label` alias that
  `lr-chart`/`lr-box-plot` dropped in favor of their mirrored `label` property.
- `height: string = '280px'` — accepts a valid CSS `height` as a private fallback. A consumer-set
  `--lr-chart-height` always wins; invalid values, declaration-breaking input, and `url()` remove
  the fallback and leave the public token/default in control.
- `xLabel: string = ''` (attribute `x-label`)
- `yLabel: string = ''` (attribute `y-label`)
- `beginAtZero: boolean = true` (attribute `begin-at-zero`)
- `stacked: boolean = false` — sums each category's bars into one segmented bar instead of grouping
  them side by side; ignored for `type="line"`
- `tickFormat?: (value: number) => string` (attribute: false) — formats a y-axis tick value for
  display (e.g. `(v) => \`$${v.toFixed(2)}\`` for currency, or a duration formatter for `"42s"`).
  Falls back to the built-in "nice numbers" formatter when unset.
- `formatter?: LyraChartFormatter` (attribute: false) — family-wide context-object formatter used
  by visual/tooltips, spoken text, legends, tables, and CSV export. It takes precedence over the
  older surface-specific hooks, which remain available as compatibility fallbacks.
  Every surface names this chart's single value scale as `axis: 'y'`, and the `visual` surface now
  carries the category `index` as well as the series, so one formatter written against
  `lr-chart`'s dual-axis context serves both components unchanged.
- `tableCellFormatter?: LyraLiteChartTableCellFormatter` (attribute: false) — formats each finite
  numeric cell in the built-in multi-series accessible table. The callback receives `(value,
  context)`, where `context` is `{ kind: 'value' | 'total'; datasetIndex: number | null; index:
  number; label: string; seriesLabel: string | null }`; total cells have `datasetIndex` and
  `seriesLabel` set to `null`. Unset cells retain locale-aware `Intl.NumberFormat` output.
- `tableTotals: boolean = false` (attribute `table-totals`) — adds a localized total column to the
  multi-series accessible table when `type="bar"` and `stacked` are both active. Ignored for
  grouped bars, line charts, and the single-series `data-list`.
- `showDataTable: boolean = false` (attribute `show-data-table`, new in 11.1.0) — makes the
  generated accessible table visible rather than screen-reader-only. Same meaning as `lr-chart`'s
  property of the same name.
- `dataTableToggle: boolean = false` (attribute `data-table-toggle`, new in 11.1.0) — renders a
  localized disclosure button (`part="data-table-toggle"`, with `aria-expanded` and
  `aria-controls`) above the table, so a *sighted* reader can reveal the numbers on demand;
  `showDataTable` then 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. This matters
  more here than on `lr-chart`: this component exists to avoid the Chart.js peers, so without it an
  app that chose it for exactly that reason had to either hand-roll a `<details>` around a
  duplicated table or adopt `lr-chart` and pull in Chart.js for a button — the cheap component
  stuck with the expensive workaround. A supplied `slot="data-table"` follows the same disclosure
  state. Unset, nothing renders and behavior is unchanged.
  Themeable via `--lr-lite-chart-data-table-toggle-hover-bg` (default
  `var(--lr-color-brand-quiet)`) and `--lr-lite-chart-data-table-toggle-active-bg` (default: that
  hover colour mixed by `--lr-color-mix-active`).
- `layout: 'fit' | 'scroll' = 'fit'` (reflected) — `'fit'` (default) is the original squeeze-the-
  whole-plot-to-host-width behavior, unchanged. `'scroll'` gives bars a fixed `barWidth` instead: plot
  content width becomes `categoryCount * barWidth` (can exceed the host's measured width), and
  `[part='base']` becomes horizontally `overflow-x: auto` so the user scrolls to see every bar at a
  legible fixed width instead of them compressing as category count grows. The plot content width
  is capped at 1,000,000px, so hostile category counts or widths cannot produce
  unbounded geometry. Bar type only.
- `barWidth: number = 32` (attribute `bar-width`, px) — each bar's fixed width in `layout="scroll"`
  mode; ignored in the default `'fit'` mode. An excessive value is reduced as needed by the
  1,000,000px scroll-content ceiling.
- `maxLabels?: number | 'auto'` (attribute `max-labels`) — decimates which category axis labels
  actually render *text*: after the global record sampler runs, it selects from those retained
  categories, always shows the first and last sampled label, and roughly evenly distributes the
  rest between them. A number is authoritative up to the number of sampled categories. `'auto'`
  derives the cap after each resize from the resolved plot width and widest rendered caller label,
  using the same deterministic 7px-per-
  character estimate as label ellipsis plus 10px of lane breathing room. It therefore responds to
  either `layout` mode without DOM text measurement or browser-specific font metrics. Unset (the
  default) renders every label, unchanged. Each rendered category label is allocation-aware:
  narrow/long text is ellipsized before paint, with the complete caller label retained as its
  accessible name. Independently, the global 1,000-record safety sampler may bound both marks and
  labels for very large category×series input.
- `barX?: (index: number) => number` (attribute: false, bar type only) — overrides the internal
  per-category x-origin formula (`plotX + i * slot`) used by both bars and their axis labels, so a
  consumer can pixel-align this chart's bars with a sibling `<lr-heatmap>` calendar's week columns
  (see that component's own `columnX`) by supplying the same coordinate function to both. Unset (the
  default) is the original formula, unchanged. The callback runs once per rendered category per
  render and its finite result is shared by that category's bars and label; a non-finite result
  falls back to the normal slot position.
- `pointText?: (label: string, value: number, datasetIndex: number) => string` (attribute: false) —
  overrides the per-bar/per-point native SVG `<title>` text (mirrors `lr-heatmap`'s `cellText`).
  The same text is written to `aria-label`, because WebKit accessibility APIs do not consistently
  derive an ARIA command name from an SVG `<title>`; the title remains the native browser tooltip.
  Falls back to the built-in raw-value template when unset.
- `legendText?: (label: string, datasetIndex: number) => string` (attribute: false) — appends
  formatter-supplied text (e.g. a value or percentage share) after each series' label in the
  built-in legend row, mirroring `pointText`/`tickFormat`'s opt-in-hook convention. Falls back to
  the label alone when unset; no-op while `legend` is `false`.
- `axisLabelText?: (label: string, index: number) => string | null` (attribute: false) — a
  display-only override for one category-axis tick's text; returning `null` renders no tick there at
  all. `labels` stays the single authoritative source for the generated accessible table's row
  headers, the per-mark `<title>`/accessible name, the live announcement and CSV export, so blanking
  a tick never blanks the same category where a reader or a spreadsheet needs it. Complements
  `maxLabels` rather than replacing it: that even decimation is applied FIRST, so a category it
  already dropped never reaches this callback — use this one for ticks that must line up with an
  external grouping boundary (a month, a release, a shift change) and leave `maxLabels` unset there.
  The returned string is ellipsized to the tick's own slot exactly like a source label, with the
  full text kept as the tick's accessible name; a return value that is neither a string nor `null`
  falls back to the source label rather than reaching the DOM.
- `roundedBars: boolean = false` (attribute `rounded-bars`, bar type only) — draws each bar as a
  rounded-top-corner shape instead of a square-cornered `<rect>`.
- `skipZero: boolean = false` (attribute `skip-zero`, bar type only) — omits a bar entirely (no
  mark/tabindex/tooltip) for a value that is exactly `0`; `null`/non-finite values are always
  skipped regardless.
- `valueAxisGutter?: number | 'auto'` (attribute `value-axis-gutter`) — value-axis gutter width in
  CSS px. A finite number is authoritative (clamped to 0…1,000,000px as before). `'auto'` sizes the
  gutter from the exact value-tick strings rendered in that pass — `formatter` output first, then
  `tickFormat`, then the component's `effectiveLocale` number formatting — using the deterministic
  7px-per-character estimate plus 14px for the tick offset and font-width variance. Automatic
  sizing never shrinks below the legacy 36px default. In `layout="fit"` it is bounded to the smaller
  of 240px or 40% of the measured SVG width, so a pathological formatter result cannot consume the
  plot. In `layout="scroll"` the cap is 240px: that SVG has an explicit content width, so deriving a
  percentage cap from its own ResizeObserver result would create a shrinking feedback loop. The
  gutter remains at logical start under RTL. Unset keeps exactly 36px.
- `barGapRatio?: number` (attribute `bar-gap-ratio`) — overrides the internal 0.2 `BAR_GROUP_GAP`
  fraction of a category slot left as a gap between categories. Unset keeps the fixed 0.2. Internal
  grouped-bar gaps are bounded within the remaining category width so supported multi-series groups
  retain positive, nonoverlapping bars for ratios below 1. A ratio of 1 reserves the whole slot as
  gap and leaves zero-width bars.
- `scale: 'linear' | 'sqrt' | 'logarithmic' = 'linear'` — `'sqrt'` (**bar type only**) maps a bar's
  value to height via `Math.sqrt(value / domainMax)` instead of the standard linear `niceDomain`
  fraction (mirroring `lr-heatmap`'s matrix-mode `sqrt` scale), so a skewed dataset's smaller bars
  aren't washed out by one dominant value; under `'sqrt'` gridlines/tick labels stay on the linear
  domain and `type="line"` ignores it entirely.
  `'logarithmic'` is the base-10 value axis for data spanning several orders of magnitude, where a
  linear axis collapses everything below the maximum into the baseline. Unlike `'sqrt'` it applies
  to **bars, line points and gridlines alike**, since a log axis whose gridlines stayed linear
  would misrepresent the plot. Value ticks use positive, bounded steps within that same domain:
  powers of ten across whole decades, with positive numeric steps for spans smaller than a decade.
  Both domain bounds remain represented, with space reserved between interior ticks and the bounds.
  Linear and square-root tick selection is unchanged. Its lower bound is the smallest *positive* datum rather than the
  linear `lo`: `beginAtZero` defaults to true, so `lo` is normally `0`, which has no logarithm —
  deriving the floor from the data is what makes a 1…1000 series span three even decades instead of
  collapsing onto one. Values at or below that floor (including zero and negatives, which have no
  real logarithm) pin to the axis floor rather than producing `-Infinity` geometry, and a degenerate
  domain falls back to the linear fraction. `lr-chart`'s own `scaleType` is the Chart.js-backed
  equivalent for the full charts. With `stacked` and `'logarithmic'`, the finite sum of positive
  raw values determines the log-mapped total extent; positive raw fractions partition that extent.
  Nonpositive segments have zero natural log height. With `minBarHeight` unset, the stack stays
  within the plot and its top agrees with the log axis. The square-root mode retains its separate
  signed-total compression and proportional allocation, with linear gridlines.
- `withoutValueAxis: boolean = false` (attribute `without-value-axis`) — suppresses gridlines and
  value-axis tick labels; x-axis category labels remain.
- `selectedIndices: readonly number[] = []` (attribute: false) — names **source category indices**,
  not positions in the sampled SVG output. Finite integer entries whose source rows are represented
  in the bounded rendered sample select matching bars and line points, which receive
  `data-selected` and explicit `aria-pressed="true"`; all other marks render `aria-pressed="false"`.
  For a multi-series chart, a selected source category selects the matching rendered mark in every
  dataset. Empty is the default.
  Style the built-in highlight through `--lr-lite-chart-selected-outline-color` and
  `--lr-lite-chart-selected-outline-width`. Note
  `::part(bar)[data-selected]` and `::part(point)[data-selected]` are **invalid CSS** — Shadow Parts
  forbids an attribute selector after `::part()` — so they silently never match; the outline is
  painted inside the shadow root and exposed through that token instead.
- `labels`, `datasets`, and `selectedIndices` are clone-owned, bounded, frozen snapshots. Mutating
  a previously assigned array or nested series data has no effect; create and reassign a new
  collection.
- `minBarHeight?: number` (attribute `min-bar-height`) — optional minimum visible bar height for
  small non-zero values; finite input is capped at 1,000,000px before derived SVG geometry is
  calculated. Authored floors can exceed the available plot height. Linear and logarithmic stacks
  push subsequent segments along their signed pixel cursor; zero values remain unfloored.
- `appendData(label, values, maxPoints?)` — appends one aligned category and optionally trims the
  oldest categories

**Events:** `lr-datum-activate` — canonical family activation with `kind: 'bar'|'point'`,
`datasetIndex`, `index`, `label`, and `value`. The compatibility `lr-point-click` event is emitted
for the same pointer or Enter/Space activation. When different series' expanded
line-point targets overlap, pointer activation selects the closest rendered point in two-dimensional
screen space; an exact distance tie retains the point whose target received the click.

**Methods:** `exportData('csv' | 'svg')` returns a spreadsheet-safe CSV snapshot or the current SVG
markup. CSV rows cover the canonical record count — the maximum of `labels.length` and every
`dataset.data.length` — and use empty cells for missing labels/values, so a longer or ragged series
is never truncated or shifted. The method does not download a file; pair it with
`lr-export-button` for download UX.

Axis titles retain their complete `xLabel`/`yLabel` values and accessible names. In the browser,
visible titles fit their plot allocation using the rendered SVG font; long titles end with an
ellipsis. If even the ellipsis cannot fit, the title remains accessible without painting text.
Fitting refreshes after rendering, allocation changes, inherited or host font changes, font loading,
and reconnection. Server rendering retains the original title until browser layout is available.

The axis gutter/title and y-axis labels mirror to logical start under RTL. The first and last
rendered category-axis tick labels anchor toward the plot's interior (`text-anchor="start"`/`"end"`)
instead of centering, so a long boundary label (e.g. a wide date string) never overhangs past the
plot's own clipped edge; every other tick still centers. Because SVG's `start`/`end` anchors already
mirror with the inherited `direction: rtl`, and the plot's own boundary swaps sides with it too, this
anchoring keeps working correctly under RTL without inverting which rendered category gets which
keyword. Built-in mark summaries are complete localized templates and format values with
`effectiveLocale`.

**Performance:** `render()` recomputes the grid/marks on every update rather than memoizing against a
content signature — `datasets`/`labels` can hold callbacks (`tickFormat`, `barX`) or arbitrary,
possibly circular or BigInt-bearing application data that a fingerprint can't serialize safely, so a
fresh, small SVG render is cheaper and more correct than a lossy cache. The shared sampling path
keeps that render bounded to 1,000 category×series marks/keyboard records, retaining endpoints
instead of materializing an unbounded hidden DOM or SVG tree.

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

**CSS parts:** `base`, `description`, `grid-line`, `axis-label`, `axis-title`, `bar` and `point` (each carries
`data-selected` when its category index is in `selectedIndices`, with explicit pressed state on every
mark), `line`, `legend`, `legend-item`, `legend-swatch`, `legend-text` (extra per-item text after
the series label, rendered only when `legendText` is set), `live-region` (the current mark
announcement for keyboard users), `data-list` (a visually hidden sampled list of plotted data
points — single-series only), `data-table` (the generated/slotted alternative container),
`data-table-toggle` (the `dataTableToggle` disclosure button — new in 11.1.0), `table`
(the generated semantic category×series table rendered when there is more than one dataset), and
`data-truncation` (the
visible/announced sampling notice).

**Screen-reader data alternative:** a single dataset renders the flat `data-list` (one `<li>` per
plotted point, matching the roving-tabindex mark order). More than one dataset instead renders a
`data-table` — a category-labelled `<caption>` (the shared localized `chartData` string), one
`<th scope="col">` per series (plus a leading `chartCategory` corner header), and one
`<th scope="row">` per category label with its per-series values in the body — so a screen-reader
user hears the values grouped by series rather than one flattened N×M sequence.
Finite table cells use `tableCellFormatter` when supplied and otherwise use the component's
effective locale. A stacked multi-series bar chart with `tableTotals` adds a localized total
column; null/non-finite inputs are skipped, while an all-missing category leaves the total cell
blank instead of reporting a misleading zero. Built-in SVG marks, keyboard targets, and this data
alternative share one endpoint-preserving sample of at most 1,000 category×series records. When
sampling occurs, a localized `data-truncation` notice is shown and announced; provide
`slot="data-table"` for a complete paginated, virtualized, or application-owned alternative, which
suppresses the generated sample and notice.

**Themeable custom properties:** `--lr-chart-height` (same public host-level property and precedence
as `lr-chart`; it always wins over the `height` property's private fallback);
`--lr-chart-grid-color`, `--lr-chart-tick-color`, `--lr-chart-tick-font-size` (default
`var(--lr-font-size-2xs)`), `--lr-chart-legend-color` — same token
*names* as `lr-chart`, so a host already theming `lr-chart` themes this for free;
`--lr-chart-color-1`, `--lr-chart-color-2`, `--lr-chart-color-3`, `--lr-chart-color-4`,
`--lr-chart-color-5`, `--lr-chart-color-6`, `--lr-chart-color-7`, and `--lr-chart-color-8` (each
defaulting to the matching `var(--lr-color-chart-N)` ramp entry) — the per-series colors, so one element can be recolored
without moving the library-wide ramp; `--lr-chart-legend-side-max` (default
`var(--lr-size-15rem)`) caps side legend allocation; `--lr-lite-chart-selected-outline-color` (default
`var(--lr-color-brand)`) — the stroke drawn on
selected `[part='bar']` and `[part='point']` marks whose category index is in `selectedIndices`;
`--lr-lite-chart-selected-outline-width` (default `var(--lr-size-2px)`) — that stroke's width.
Unlike `lr-chart` (canvas-rendered, needs `getComputedStyle`-based re-theming on every draw), this
is plain SVG/DOM and reads these via native CSS `var()` — no JS-side resolution step, and no
`refreshTheme()` method needed (there's nothing to go stale). `--lr-chart-pattern-step`
(default `var(--lr-space-2xs)`) sizes the forced-colors legend texture, exactly as on `lr-chart`.

**Forced colors:** under `forced-colors: active` the `--lr-color-chart-*` ramp behind
`--lr-chart-color-1..8` 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. `lr-lite-chart` then
encodes each series a second way, using the same eight-way vocabulary as `lr-chart`: `[part='bar']`
takes a per-series SVG texture fill, `[part='line']` takes a per-series `stroke-dasharray`, and
`[part='legend-swatch']` carries a `data-encoding` attribute selecting the matching CSS texture.
Nothing is opt-in, the encodings exist only while the media query matches, and no author color is
substituted.

**Optional peer deps:** none. This is the point of the component.

```html
<lr-lite-chart type="bar" stacked legend x-label="Week" y-label="Commits"></lr-lite-chart>
<script>
  const c = document.querySelector('lr-lite-chart');
  c.labels = ['W1', 'W2', 'W3', 'W4'];
  c.datasets = [
    { label: 'Docs', data: [4, 6, 3, 8] },
    { label: 'Bugs', data: [3, 2, 5, 4] },
  ];
</script>
```

**Known gotchas:**
- No horizontal-bar mode (unlike `lr-chart`'s `index-axis="y"`) — deliberately cut from scope, not a
  stub: bars are always vertical.
- No dual y-axis (`Series.axis: 'y2'`) — every series shares one y-axis/domain.
- Series colors default to the shared categorical ramp (round-robin by dataset index) when `color`
  is unset or invalid — the same eight `--lr-color-chart-1..8` tokens `lr-chart` uses, so both
  chart implementations agree on the palette and both follow a `--lr-theme-color-chart-*` retheme.
  Being plain SVG, they resolve through native `var()` at paint time, so a theme or color-scheme
  change needs no JS-side redraw pass here.
- Bar/point elements are real focusable DOM nodes (`role="button"` with one roving `tabindex="0"`);
  each carries the same localized text as an explicit `aria-label` and native SVG `<title>`, giving
  every engine a command name while retaining the tooltip. The `<svg>` itself uses
  `role="group"`, not `role="img"` — an image role would conflict with genuinely interactive
  descendants (axe's `nested-interactive` rule).
- Dense transparent hit regions expand toward 24px only while remaining inside the neighboring
  mark's midpoint lane; stacked segments keep their own vertical region. This prevents a
  later-painted mark from stealing pointer input from an adjacent datum. Cross-series line targets
  are additionally arbitrated by two-dimensional screen distance.
- In narrow allocations, category labels are ellipsized to their available lane and retain the
  complete label through `aria-label`; SVG overflow is contained within the host.
- Tick values use a standard "nice numbers" (1/2/5 × 10ⁿ) rounding step, not exact data min/max —
  intentional (readable axis labels), matches how most charting libraries pick tick steps.

---
