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

# `lr-funnel`

- **Import** `import '@aceshooting/lyra-ui/components/lr-funnel.js';` (stable tag alias; registers the tag)
- **Class** `LyraFunnel`, also available unregistered from `@aceshooting/lyra-ui/components/data/funnel/funnel.class.js`
- **Family** `components/data/` — see `llms/index.md` for its siblings
- **Status** `experimental` since `12.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** 14 parts, 4 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-funnel`

Dependency-free conversion funnel (no charting library): an ordered set of stages, each drawn as a
bar whose length is that stage's share of the **first** stage, read top-to-bottom as progressive
drop-off. It sits beside `lr-gauge` and `lr-heatmap` as an analytics primitive rather than a general
chart type.

When a positive finite stage value divided by a positive finite baseline overflows to `Infinity`,
the main or comparison data fill clamps to 100%. Nonfinite input normalization, nonpositive-baseline
behavior, percentage and drop-off text, and overflow border styling keep their existing rules.

It is deliberately not a sorted bar chart: it normalizes to the first stage rather than to the data
maximum, draws no value axis, and reads as stage-to-stage retention rather than category comparison.
Reach for `lr-bar-chart`/`lr-lite-chart` when you actually want a value axis and category comparison.

The whole chart is plain HTML — stage names, absolute values, shares and drop-off percentages are
real text inside an `<ol>`, so there is no sighted-only drawing needing a separate transcript. The
list carries the accessible name; a host `aria-label` overrides `label`.

**Properties:**

- `stages: readonly LyraFunnelStage[] = []` (property only; every share is measured against
  `stages[0]`)
- `comparison: readonly LyraFunnelStage[] = []` (property only — an optional baseline/peer cohort
  drawn behind each bar as a dashed outline, normalized to **its own** first stage so a cohort's
  funnel *shape* stays comparable against a baseline whose absolute volumes are not)
- `comparisonLabel: string = ''` (attribute `comparison-label`; falls back to a localized generic
  label)
- `label: string = ''` (accessible name for the stage list)
- `dropoff: boolean = true` (reflected; `dropoff="false"` in markup really does turn it off —
  the property uses the explicit `false`-parsing converter, not attribute presence)
- `sharePrecision: number = 0` (attribute `share-precision`; fraction digits for every share and
  drop-off percentage, clamped to `0`–`20`)

```ts
interface LyraFunnelStage {
  readonly label: string;
  readonly value: number;
  readonly color?: string; // CSS color; unparseable values fall back to --lr-funnel-bar-color
}
```

**Events:** none.

**Slots:** none.

**CSS parts:** `base`, `stages` (the `<ol>`), `stage`, `dropoff`, `stage-header`, `stage-label`,
`stage-value`, `stage-share`, `comparison-value`, `track`, `bar`, `bar-overflow` (a second token on
`bar` when the stage exceeds the first stage), `comparison-bar`, `empty`

**Themeable custom properties:** `--lr-funnel-bar-color` (default `var(--lr-color-brand)`),
`--lr-funnel-comparison-color` (default `var(--lr-color-border-strong)`), `--lr-funnel-track-color`
(default `var(--lr-color-surface-raised)`), `--lr-funnel-bar-size` (track thickness, default
`var(--lr-size-1-5rem)`).

**Optional peer deps:** none.

```html
<script type="module">
  import '@aceshooting/lyra-ui/components/data/funnel/funnel.js';
</script>

<lr-funnel id="signup" label="Self-serve signup" share-precision="1"></lr-funnel>

<script type="module">
  const funnel = document.querySelector('#signup');
  funnel.stages = [
    { label: 'Visited pricing', value: 12480 },
    { label: 'Started trial', value: 4310 },
    { label: 'Converted to paid', value: 512 },
  ];
  // Normalized to ITS OWN first stage, so a 380-visitor account still compares against a
  // 12,480-visitor peer group.
  funnel.comparison = [
    { label: 'Visited pricing', value: 380 },
    { label: 'Started trial', value: 141 },
    { label: 'Converted to paid', value: 12 },
  ];
  funnel.comparisonLabel = 'Acme Corp';
</script>
```

**Known gotchas:**

- **Shares are of the first stage, not of the previous one.** `stage-share` answers "how much of the
  top of the funnel is left here?"; the separate `dropoff` row answers "what changed since the
  previous stage?". Both are rendered because the percentage is usually the interesting number and
  the absolute count is the credibility check.
- **A zero or negative first stage cannot define a share.** Every `stage-share` is omitted, every bar
  is zero-length, and only the absolute values render. Nothing is silently divided by zero and no
  `NaN` reaches the geometry.
- **A stage larger than its predecessor is legal** (funnel re-entry). Its share is reported
  truthfully above 100% in text, while the bar clamps to the track and gains the `bar-overflow` part
  token so you can style it. Drop-off for that stage reads as an increase.
- **Drop-off is omitted, not zeroed, when the previous stage is non-positive** — a change relative to
  zero is undefined.
- **A comparison series of a different length pairs by index.** Extra comparison entries are ignored;
  stages past its end simply get no comparison bar. A comparison series whose own first value is
  zero or negative draws nothing.
- **Malformed stage entries are omitted independently.** Non-record entries and records without a
  string `label` cannot suppress valid neighbors in either series. This is a render-only boundary:
  the public `stages` and `comparison` arrays retain caller identity and are never rewritten.
- **Non-finite `value`s are treated as `0`** rather than blanking the stage, so one bad row cannot
  take the chart with it.
- Values, shares and drop-off percentages all format through the component's effective locale
  (`locale` attribute, or an inherited one) — never a hardcoded `en`.
- Bars grow from the inline-start edge and every dimension is a logical property, so `dir="rtl"`
  needs no extra work. The component never sets its own `dir`.
- Layout responds to the **container** (`container-type: inline-size` on `base`), not the viewport:
  below roughly `18rem` of allocated width the stage name moves to its own line so the value and
  share stay together. Bar motion is a token-driven transition that stops under
  `prefers-reduced-motion`.
- No events, no interaction model, no zoom/pan. If you need a clickable funnel, wrap the element and
  handle clicks yourself.
