Waterfall (signed P&L)

type: waterfall renders each bar starting where the previous one ended. waterfallTotals marks subtotal/total bars that span from 0. Bars colour-code by sign (green / red); total bars get a neutral slate. Connector lines link bar tops so the cumulative trend reads at a glance.

A live, editable Svelte 5 data grid example. Open the interactive demo or read the documentation.

What this example shows

`type: 'waterfall'` renders a running-total bar chart - each bar starts where the previous one ended. Mark a bar as a total via `waterfallTotals: boolean[]` (parallel to `categories`) and that bar spans from 0 instead of stacking, so it reads as a subtotal / grand total. Bars colour-code by sign (green positive, red negative) and total bars get a neutral slate by default - override via `waterfallColors`. Thin connector lines link bar tops so the cumulative trend reads at a glance.

This demo: a quarterly P&L driven by the grid. Toggle a row's `isTotal` checkbox to mark it as a subtotal; the chart re-renders with the new running calculation.

Source code (158-chart-waterfall.svelte)

<!-- Documented in: docs/help/charts.md -->
<script lang="ts">
  /**
   * 158. Waterfall chart (signed P&L)
   * ----------------------------------
   * `type: 'waterfall'` renders a running-total bar chart - each bar
   * starts where the previous one ended. Mark a bar as a total via
   * `waterfallTotals: boolean[]` (parallel to `categories`) and that bar
   * spans from 0 instead of stacking, so it reads as a subtotal /
   * grand total. Bars colour-code by sign (green positive, red negative)
   * and total bars get a neutral slate by default - override via
   * `waterfallColors`. Thin connector lines link bar tops so the
   * cumulative trend reads at a glance.
   *
   * This demo: a quarterly P&L driven by the grid. Toggle a row's
   * `isTotal` checkbox to mark it as a subtotal; the chart re-renders
   * with the new running calculation.
   */
  import {
    SvGrid,
    SvGridChart,
    tableFeatures,
    rowSortingFeature,
    columnFilteringFeature,
    type ColumnDef,
    type SvGridApi,
    type ChartSpec,
  } from '@svgrid/grid'

  const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })

  type Row = { id: number; label: string; value: number; isTotal: boolean }
  const seed: Row[] = [
    { id: 0, label: 'Revenue',          value: 1_240_000, isTotal: false },
    { id: 1, label: 'Discounts',        value:   -85_000, isTotal: false },
    { id: 2, label: 'Refunds',          value:   -32_000, isTotal: false },
    { id: 3, label: 'Net revenue',      value:         0, isTotal: true  },
    { id: 4, label: 'COGS',             value:  -480_000, isTotal: false },
    { id: 5, label: 'Gross profit',     value:         0, isTotal: true  },
    { id: 6, label: 'Marketing',        value:  -180_000, isTotal: false },
    { id: 7, label: 'R&D',              value:  -210_000, isTotal: false },
    { id: 8, label: 'G&A',              value:   -95_000, isTotal: false },
    { id: 9, label: 'Operating income', value:         0, isTotal: true  },
  ]
  let rows = $state<Row[]>(seed)

  const columns: ColumnDef<typeof features, Row>[] = [
    { field: 'label',   header: 'Line item', width: 220 },
    { field: 'value',   header: 'Amount',    width: 150, align: 'right',
      format: { type: 'currency', currency: 'USD', options: { maximumFractionDigits: 0 } } },
    { field: 'isTotal', header: 'Total?',    width: 100 },
  ]

  let api = $state<SvGridApi<typeof features, Row> | null>(null)
  let displayed = $state<Row[]>(rows)
  function sync() { displayed = (api?.getDisplayedRows() as Row[]) ?? rows }

  const spec = $derived.by<ChartSpec>(() => ({
    type: 'waterfall',
    categories: displayed.map((r) => r.label),
    series: [{
      label: 'Amount',
      values: displayed.map((r) => r.value),
    }],
    waterfallTotals: displayed.map((r) => r.isTotal),
    width: 720,
    height: 360,
    yAxisTitle: 'USD',
  }))

  const compact = (v: number) => {
    const a = Math.abs(v)
    if (a >= 1e6) return (v / 1e6).toFixed(1) + 'M'
    if (a >= 1e3) return (v / 1e3).toFixed(0) + 'k'
    return String(Math.round(v))
  }

  function toggleTotal(id: number) {
    rows = rows.map((r) => (r.id === id ? { ...r, isTotal: !r.isTotal } : r))
    sync()
  }
</script>

<section class="flex flex-col flex-1 min-h-0 gap-3">
  <div class="shrink-0 rounded-lg border px-4 py-3" style="border-color: var(--sg-border); background: var(--sg-header-bg);">
    <p class="text-sm font-semibold" style="color: var(--sg-fg);">
      Waterfall chart for a quarterly P&amp;L
    </p>
    <p class="mt-0.5 text-xs" style="color: var(--sg-muted);">
      Each green/red bar starts where the previous one ended. The slate "total" bars span from 0,
      showing the cumulative position. Click a row's <em>Total?</em> badge to flip its role - the
      waterfall reflows in real time.
    </p>
  </div>

  <div class="flex flex-1 min-h-0 gap-3">
    <div class="flex-1 min-h-0">
      <SvGrid responsive={true}
        data={rows}
        columns={columns}
        features={features}
        sortable
        filterable
        selectionMode="none"
        enableRowSummaries={false}
        rowHeight={32}
        containerHeight="100%"
        fitColumns={true}
        onApiReady={(a) => { api = a; sync() }}
        onCellClick={(e) => {
          if (e.columnId === 'isTotal' && e.row) toggleTotal((e.row as Row).id)
        }}
      />
    </div>
    <div class="shrink-0 rounded-lg border p-3" style="width: 780px; border-color: var(--sg-border); background: var(--sg-bg);">
      <SvGridChart {spec} formatValue={compact} dataLabels />
    </div>
  </div>
</section>

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.
  • Integrated charts (no deps) - Chart the grid data with no external charting library. SvGridChart renders a ChartSpec; rowsToChartSpec aggregates the grid current (filtered/sorted) rows into one. Bar, line, area, pie - plus 100% stacked, top-N + Other, an average reference line, and double-click-to-isolate a series. Filter the grid and the chart re-aggregates live.
  • Scatter / bubble chart - A scatter plot maps two numeric measures (x vs y); a bubble chart adds a third via dot radius. type: scatter with series points [{ x, y, r }]. Spend vs revenue, sized by deals, coloured by region, with an average-revenue reference line. Filter the grid and the cloud re-plots.
  • Time-series chart (date axis) - xType: time spaces points by ACTUAL time - irregular date gaps render proportionally - and shows real date ticks. A referenceLines target/SLA line spans the plot; toggle 100% stacked to read each day as a share of its total. Line, stacked area, or stacked bar.