Waterfall Chart

Free

2 orientations with floating bars, dashed connectors, increase/decrease/total color-coding, data labels, tooltips and per-bar entrance animations. Pure SVG.

</>sv-waterfall-chart chartwaterfallfinance
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 demo

More examples

Titled P&L bridge

example.htmlAngular
<sv-waterfall-chart [data]="data" title="Gross → net margin" orientation="vertical" [height]="320" />

Handle bar interactions

example.htmlAngular
<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

NameTypeDefaultDescription
loadingbooleanfalseTrue while the series are being fetched.
errorSvDataError | string | nullnullLoad failure. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`.
emptyMessagestring'No data to plot'Copy for the empty state.
filteredbooleanfalseWhether a filter, not an absence of data, is why the chart is empty.
dataWaterfallPoint[][]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.
valuePolicySvValuePolicy'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.
totalTolerancenumber0How 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.
titlestring''Chart title displayed above the SVG.
orientationWaterfallOrientation'vertical'Layout direction.
heightnumber380SVG height in pixels.
optionsWaterfallChartOptions{}Rendering and feature options.
customClassstring''Extra CSS classes applied to the root wrapper.
ariaLabelstring''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

EventPayload typeDescription
retryvoidThe host should re-run the failed load.
barHoverWaterfallBarEventEmitted when a bar is hovered or keyboard-focused.
barSelectWaterfallBarEventEmitted 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

NameTypeDefaultDescription
loadingbooleanfalseTrue while the series are being fetched.
errorSvDataError | string | nullnullLoad failure. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`.
emptyMessagestring'No data to plot'Copy for the empty state.
filteredbooleanfalseWhether a filter, not an absence of data, is why the chart is empty.
dataWaterfallPoint[][]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.
valuePolicySvValuePolicy'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.
totalTolerancenumber0How 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.
titlestring''Chart title displayed above the SVG.
orientationWaterfallOrientation'vertical'Layout direction.
heightnumber380SVG height in pixels.
optionsWaterfallChartOptions{}Rendering and feature options.
customClassstring''Extra CSS classes applied to the root wrapper.
ariaLabelstring''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

EventPayload typeDescription
retryvoidThe host should re-run the failed load.
barHoverWaterfallBarEventEmitted when a bar is hovered or keyboard-focused.
barSelectWaterfallBarEventEmitted 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.

usage.htmlAngular
<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.

host.component.tsTypeScript
// 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.

Loads giscus.app and GitHub — nothing is fetched until you click.