Waterfall Chart
Free2 orientations with floating bars, dashed connectors, increase/decrease/total color-coding, data labels, tooltips and per-bar entrance animations. Pure SVG.
import { SvWaterfallChartComponent } from '@styloviz/charts';Demo & usage
Basic usage
Usage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Titled P&L bridge
<sv-waterfall-chart [data]="data" title="Gross → net margin" orientation="vertical" [height]="320" />
Handle bar interactions
<sv-waterfall-chart [data]="data" (barSelect)="onSelect($event)" (barHover)="onHover($event)" />
When to use
- Explaining how a starting value reaches an ending value through gains/losses.
- Financial bridges (revenue → net), inventory changes, budget variance.
- For plain category totals, use a Bar Chart.
Async states
- loading
- empty
- error
- retry
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| loading | boolean | false | True while the series are being fetched. |
| error | SvDataError | string | null | null | Load failure. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`. |
| emptyMessage | string | 'No data to plot' | Copy for the empty state. |
| filtered | boolean | false | Whether a filter, not an absence of data, is why the chart is empty. |
| data | WaterfallPoint[] | [] | The waterfall series, **in the order it should be read**. Order is the meaning here, not a presentation choice (audit item CH-18-I3). Each bar starts where the previous one ended, so moving a step changes every running total after it: `[+100, −50, ×2]` and `[+100, ×2, −50]` end at 100 and 150 from the same three facts. This component therefore **never sorts** — not by label, not by magnitude, not to put totals last. What you pass is what is read, and a series assembled from an unordered source (a `Map`, an object's keys, a `Promise.all` result) has to be put in order by the caller before it gets here. |
| valuePolicy | SvValuePolicy | 'clamp' | What to do with a step this chart cannot draw — one whose `value` is absent, `NaN` or `Infinity`. A negative is not one of these: a waterfall is built out of falls as much as rises. - `clamp` (default) — the step moves the running total by nothing, is marked in the plot, and is named under it. Every later bar keeps its position. - `reject` — refuse the series and show the error state. `omit` is not available: a bar's position is its place in a sequence, so dropping one silently rewrites the arithmetic of every bar after it. `render` is not available either — there is no honest way to draw a step of unknown size. |
| totalTolerance | number | 0 | How far a stated `subtotal` / `total` may sit from the figure its steps add up to before the chart reports a discrepancy, in data units. `0` (the default) means they must agree, subject to a floating-point band that scales with the numbers involved — see `FLOAT_NOISE`. Raise it when the source rounds: a ledger kept in cents but charted in millions reconciles to within 0.000005, and that is a rounding artefact rather than a hole. |
| title | string | '' | Chart title displayed above the SVG. |
| orientation | WaterfallOrientation | 'vertical' | Layout direction. |
| height | number | 380 | SVG height in pixels. |
| options | WaterfallChartOptions | {} | Rendering and feature options. |
| customClass | string | '' | Extra CSS classes applied to the root wrapper. |
| ariaLabel | string | '' | Accessible-name override for the chart. When empty, a concise summary is generated; the full series is always exposed through a visually-hidden table. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| retry | void | The host should re-run the failed load. |
| barHover | WaterfallBarEvent | Emitted when a bar is hovered or keyboard-focused. |
| barSelect | WaterfallBarEvent | Emitted when a bar is clicked or activated via keyboard (Enter/Space). |
<sv-waterfall-chart-pro>
Also in this family — SvWaterfallChartProComponent from @styloviz-pro/charts.
Async states
- loading
- empty
- error
- retry
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| loading | boolean | false | True while the series are being fetched. |
| error | SvDataError | string | null | null | Load failure. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`. |
| emptyMessage | string | 'No data to plot' | Copy for the empty state. |
| filtered | boolean | false | Whether a filter, not an absence of data, is why the chart is empty. |
| data | WaterfallPoint[] | [] | The waterfall series, **in the order it should be read**. Order is the meaning here, not a presentation choice (audit item CH-18-I3). Each bar starts where the previous one ended, so moving a step changes every running total after it: `[+100, −50, ×2]` and `[+100, ×2, −50]` end at 100 and 150 from the same three facts. This component therefore **never sorts** — not by label, not by magnitude, not to put totals last. What you pass is what is read, and a series assembled from an unordered source (a `Map`, an object's keys, a `Promise.all` result) has to be put in order by the caller before it gets here. |
| valuePolicy | SvValuePolicy | 'clamp' | What to do with a step this chart cannot draw — one whose `value` is absent, `NaN` or `Infinity`. A negative is not one of these: a waterfall is built out of falls as much as rises. - `clamp` (default) — the step moves the running total by nothing, is marked in the plot, and is named under it. Every later bar keeps its position. - `reject` — refuse the series and show the error state. `omit` is not available: a bar's position is its place in a sequence, so dropping one silently rewrites the arithmetic of every bar after it. `render` is not available either — there is no honest way to draw a step of unknown size. |
| totalTolerance | number | 0 | How far a stated `subtotal` / `total` may sit from the figure its steps add up to before the chart reports a discrepancy, in data units. `0` (the default) means they must agree, subject to a floating-point band that scales with the numbers involved — see `FLOAT_NOISE`. Raise it when the source rounds: a ledger kept in cents but charted in millions reconciles to within 0.000005, and that is a rounding artefact rather than a hole. |
| title | string | '' | Chart title displayed above the SVG. |
| orientation | WaterfallOrientation | 'vertical' | Layout direction. |
| height | number | 380 | SVG height in pixels. |
| options | WaterfallChartOptions | {} | Rendering and feature options. |
| customClass | string | '' | Extra CSS classes applied to the root wrapper. |
| ariaLabel | string | '' | Accessible-name override for the chart. When empty, a concise summary is generated; the full series is always exposed through a visually-hidden table. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| retry | void | The host should re-run the failed load. |
| barHover | WaterfallBarEvent | Emitted when a bar is hovered or keyboard-focused. |
| barSelect | WaterfallBarEvent | Emitted when a bar is clicked or activated via keyboard (Enter/Space). |
Wire it to your API
This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.
Template
Bound to the names this component actually exposes.
<sv-waterfall-chart [data]="data.items()" [loading]="data.loading()" [error]="data.error()" [filtered]="data.filtered()" (retry)="data.reload()" />
Host class
Drive the three async inputs from your own fetch.
// This component reports its state but does not emit a query, so drive // it from your own fetch and bind the three async inputs. readonly items = signal<Row[]>([]); readonly loading = signal(false); readonly error = signal<SvDataError | null>(null); async load(): Promise<void> { this.loading.set(true); this.error.set(null); try { this.items.set(await this.api.fetchRows()); } catch (e) { this.error.set(toSvDataError(e)); } finally { this.loading.set(false); } }
- This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `data`, `loading` and `error`.
- Set `filtered` when a search or filter is active. It is the difference between "you have no orders" and "no orders match this filter" — telling a customer with 4,000 orders the first one reads as a bug.
- Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.
Full walkthrough: Connecting to a backend.
Customization & theming
- Vertical or horizontal orientation.
- Increase / decrease / total color coding.
- Dashed connectors and data labels.
- Every point declares what it is through `kind` — `'delta'` (a step that moves the running total), `'subtotal'` (a running sum drawn from zero that does not advance it) or `'total'`. `isTotal` / `isIntermediate` remain as shorthand for the last two. Nothing is inferred any more: a point whose `value` is missing used to be read as a total and drawn as the running sum, so a row that failed to load rendered a figure the data never claimed in the position a reader trusts most.
- `value` on a `subtotal` or `total` is the figure your source **states** for it, and it is optional. The bar is always drawn from the steps before it — the connectors and every preceding bar have to agree with it — and any disagreement is reported: on the bar (`480 ≠ 500` plus a dashed outline), in a sentence under the plot, in the bar's accessible name and in the data table. `totalTolerance` widens the band two figures may sit apart before that happens; it defaults to 0, subject to a floating-point band that scales with the numbers, so `0.1 + 0.2` against `0.3` is not reported as a hole.
- `valuePolicy` decides what happens to a step this chart cannot draw — one whose `value` is absent, `NaN` or `Infinity`. A negative is not one of these: a waterfall is built out of falls as much as rises. `clamp` (default) moves the running total by nothing, marks the bar and names it under the plot; `reject` refuses the series and shows the error state. `omit` is not offered, because a bar's position is its place in a sequence and dropping one silently rewrites every total after it. `render` is not offered either — there is no honest way to draw a step of unknown size.
- Order is part of the data. Each bar starts where the last one ended, so `[+100, −50, subtotal]` and `[+100, subtotal, −50]` are different statements, and the subtotal reports 50 in one and 100 in the other. Sort before passing the series in; the component will not do it for you, including when the source is an unordered one (a `Map`, an object's keys, a `Promise.all` result).
Best practices
- Do: Color-code increases, decreases and totals consistently.
- Do: Order steps in the real sequence so the running total makes sense. The component never sorts — order is the arithmetic, not a presentation choice.
- Do: Type each point with `kind`: `'delta'`, `'subtotal'` or `'total'`. An opening balance is a `delta`, not a total — it moves the running total from zero to itself.
- Do: Pass the figure your source states for a `total` as its `value`. The chart checks it against the steps and reports any difference rather than quietly drawing its own answer.
- Tip: Label each bar with its delta and the running total for clarity.
Accessibility
- Each bar is keyboard-focusable (`role="button"`, Tab) and its `aria-label` carries the delta AND the running total after it. The running total is drawn as a bar's vertical position and spoken by a hover tooltip, so without it a keyboard reader learned every step and never where the total stood — in a chart whose whole subject is where the total stands.
- Tooltip appears on hover and on focus; Enter/Space emits `barSelect`. Color coding is paired with labels.
- A visually-hidden table lists every step in order with its step number, type, change and running total — the sequence is the meaning here, so the number is data rather than decoration. It stays when `showLegend` is off: the legend is a display option, the alternative to the plot is not.
- Whatever the chart had to alter or could not reconcile is stated in the rendered output, not in `console.warn`. A total that does not add up is the most consequential thing this chart can discover, and the person it matters to is reading a dashboard, not a browser console. The dashed outline in the plot gets a legend key, so the mark is never colour-only.
Feedback on Waterfall Chart
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.