Waterfall, funnel and pareto

Three charts that explain how a total came to be: a waterfall that bridges revenue to net income with subtotals that restart from zero, connectors between the steps and named colours for up, down and total; a sign-up funnel with the conversion and drop-off per step in the tooltip and a trapezoid, pyramid or cone shape; and a pareto of return causes with its cumulative line and the 80% mark.

A live, editable Svelte 5 data grid example from the SvGrid gallery (Charts). See the SvGrid documentation for the full API.

About this example

Waterfall, funnel and pareto: waterfallTotals marks the bars that are subtotals and restart from zero, waterfallColors names the colours for up, down and total, and connectors join the steps; a funnel carries each step's count with the conversion from the top and the drop-off from the step before in the tooltip, drawn as a trapezoid, pyramid or cone through funnelShape; paretoSpec sorts the causes by value and runs a cumulative percentage line with the 80% mark. Free, in @svgrid/grid.

Three charts that explain how a total came to be:

  • The waterfall bridges revenue to net income: each bar starts where the last one ended, waterfallTotals marks the bars that are subtotals (they restart from zero), and waterfallColors names the colours for up, down and total. Connectors join the steps.
  • The funnel is a sign-up flow. Each segment carries its count, the tooltip its conversion from the top and its drop-off from the step before; funnelShape draws it as a trapezoid, a pyramid or a cone.
  • The pareto sorts the causes of returns by count and runs a cumulative percentage line over them, with the 80% line marked, so the few causes behind most of the returns stand out.

Free, in @svgrid/grid.

Imports, features and API used

Imports: @svgrid/grid, ../shared/ChartDataGrid.svelte, ../shared/ViewSwitch.svelte

Frequently asked questions

How do I mark subtotals in a waterfall?

waterfallTotals is a boolean per category. A true entry draws that bar from zero to the running total instead of as a step: give it a value of 0 to show the sum reached so far, or a value of its own to set the sum, which is how the bridge opens on the revenue figure.

What does the funnel tooltip show?

The step's value, its conversion as a share of the first step, and its drop-off from the step before.

How do I make a pareto chart?

Pass a bar spec to paretoSpec. It sorts the categories by value, adds the cumulative percentage as a line on a right axis and marks the threshold, 80% by default.

Related documentation

Source code (449-chart-waterfall-funnel.svelte)

<!-- Documented in: docs/help/charts/gallery.md -->
<script lang="ts">
  /**
   * 449. Waterfall, funnel and pareto
   * ---------------------------------
   * Three charts that explain how a total came to be:
   *
   * - The waterfall bridges revenue to net income: each bar starts where the
   *   last one ended, `waterfallTotals` marks the bars that are subtotals
   *   (they restart from zero), and `waterfallColors` names the colours for
   *   up, down and total. Connectors join the steps.
   * - The funnel is a sign-up flow. Each segment carries its count, the
   *   tooltip its conversion from the top and its drop-off from the step
   *   before; `funnelShape` draws it as a trapezoid, a pyramid or a cone.
   * - The pareto sorts the causes of returns by count and runs a cumulative
   *   percentage line over them, with the 80% line marked, so the few
   *   causes behind most of the returns stand out.
   *
   * Free, in @svgrid/grid.
   */
  import { SvChart, paretoSpec, type ChartSpec } from '@svgrid/grid'
  import ChartDataGrid from '../shared/ChartDataGrid.svelte'
  import ViewSwitch from '../shared/ViewSwitch.svelte'

  let shape = $state<'trapezoid' | 'pyramid' | 'cone'>('trapezoid')

  const waterfall: ChartSpec = {
    type: 'waterfall',
    categories: ['Revenue', 'Cost of sales', 'Gross profit', 'R&D', 'Sales & marketing', 'G&A', 'Operating income', 'Tax', 'Net income'],
    series: [{ label: 'FY26', values: [4300, -1840, 0, -620, -780, -310, 0, -180, 0] }],
    waterfallTotals: [true, false, true, false, false, false, true, false, true],
    waterfallColors: { positive: '#16a34a', negative: '#dc2626', total: '#2563eb' },
    valueFormat: 'currency',
    title: 'From revenue to net income',
    subtitle: 'USD thousands, FY26',
    yAxis: { gridLines: true },
    dataLabels: { show: true, placement: 'top' },
    height: 340,
  }

  const funnel = $derived<ChartSpec>({
    type: 'funnel',
    categories: ['Visited', 'Signed up', 'Activated', 'Upgraded', 'Renewed'],
    series: [{ label: 'Users', values: [48200, 12600, 7900, 2150, 1680] }],
    funnelShape: shape,
    title: 'Sign-up funnel',
    subtitle: 'Last quarter; hover a step for the conversion',
    valueFormat: 'compact',
    height: 340,
  })

  const pareto: ChartSpec = {
    ...paretoSpec({
      type: 'bar',
      categories: ['Wrong size', 'Damaged', 'Changed mind', 'Late delivery', 'Not as described', 'Defective', 'Other'],
      series: [{ label: 'Returns', values: [412, 268, 190, 96, 71, 44, 38] }],
    }),
    title: 'Why orders come back',
    subtitle: 'Returns by cause, with the cumulative share',
    yAxis: { title: 'Returns', gridLines: true },
    height: 340,
  }

  // Chart | Grid: the same spec as the chart or as the rows behind it.
  let view = $state<'chart' | 'grid'>('chart')
</script>

<section class="wrap">
  <header class="chrome">
    <ViewSwitch bind:value={view} options={[['chart', 'Chart'], ['grid', 'Grid']]} />
    <label class="ctl">
      Funnel shape
      <select bind:value={shape}>
        <option value="trapezoid">Trapezoid</option>
        <option value="pyramid">Pyramid</option>
        <option value="cone">Cone</option>
      </select>
    </label>
    <span class="note">
      A waterfall with subtotals and connectors, a funnel with conversion and drop-off in the tooltip,
      and a pareto with its cumulative line and the 80% mark.
    </span>
  </header>

  <div class="row">
    <div class="pane pane-wide">
      {#if view === 'chart'}
        <SvChart spec={waterfall} legend={false} autosize />
      {:else}
        <ChartDataGrid spec={waterfall} />
      {/if}
    </div>
    <div class="pane">
      {#if view === 'chart'}
        <SvChart spec={funnel} legend={false} autosize />
      {:else}
        <ChartDataGrid spec={funnel} />
      {/if}
    </div>
    <div class="pane">
      {#if view === 'chart'}
        <SvChart spec={pareto} legend={false} autosize />
      {:else}
        <ChartDataGrid spec={pareto} />
      {/if}
    </div>
  </div>
</section>

<style>
  .wrap {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    gap: 10px;
    overflow: auto;
  }
  .chrome {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    flex: none;
  }
  .note {
    font-size: 12px;
    color: var(--sg-muted, #64748b);
  }
  .ctl {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 12px;
    color: var(--sg-fg, #0f172a);
    white-space: nowrap;
  }
  .ctl select {
    font: inherit;
    border: 1px solid var(--sg-border, #e2e8f0);
    border-radius: 6px;
    background: var(--sg-bg, #fff);
    color: inherit;
    padding: 2px 6px;
  }
  .row {
    display: flex;
    flex: none;
    gap: 12px;
    flex-wrap: wrap;
  }
  .pane {
    flex: 1 1 360px;
    border: 1px solid var(--sg-border, #e2e8f0);
    border-radius: 10px;
    background: var(--sg-bg, #fff);
    padding: 8px 10px;
    min-width: 0;
  }
  .pane-wide {
    flex: 1 1 100%;
  }
</style>

View this example on GitHub

More Charts examples

  • Chart a selection (context menu) - With integrated charting enabled, the right-click menu gains a Chart selected range item: select a block of cells, right-click, and the chart panel opens scoped to that range - the Excel chart-this gesture, built in. The item appears only when charting is on and is appended to the default context menu automatically.
  • Chart view of the grid - The `chart` prop turns the same <SvGrid> into a chart, driven by the grid’s filtered + sorted rows (search + sort flow through). A view of the grid like board and scheduler, but the renderer is free: the grid lazy-loads a built-in view wrapping the standalone SvChart via rowsToChartSpec. Flip Table <-> Chart (bar / line / area) over one source of truth.
  • Candlestick / OHLC - Candlestick and OHLC price marks with an ordinal date axis. ChartSeries.ohlc carries the four prices while values keeps the closes, so the CSV export, the screen-reader table and a 10-day moving average all work with no candle-specific code. Toggle the axis: a real time axis opens a gap over every weekend, an ordinal one spaces sessions evenly and still labels them by date. The strip under the plot is the chart brush: drag its window to pan, drag an edge to resize.
  • Box plot + error bars - Distribution rather than average: box plots with the 1.5 IQR whisker rule and individual outliers, next to the same data as a bar chart of the means with error bars. two regions with nearly identical means sit side by side in the bars and look nothing alike in the boxes. boxStats() summarises a raw sample, rowsToBoxSpec() does it per group, and ChartSeries.errors annotates any existing mark without changing its type.
  • Axes, titles and styling - The chart hands the decisions back: a numeric x axis that spreads payload sizes the way the numbers do, pinned domains with a fixed tick interval and a formatter per axis, grid lines on or off, a shaded budget band, a vertical reference line, per-point markers and colours, a stepped dashed series on the right axis, a title / subtitle / caption, a legend on any side, and a tooltip snippet in single-series mode. Below it, 50,000 monitoring readings decimated to one point per pixel with LTTB or min / max - toggle it off to see the cost.