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 from the SvGrid gallery (Charts). See the SvGrid documentation for the full API.
About this example
A waterfall chart of a quarterly P&L in Svelte 5. type waterfall renders each bar starting where the previous one ended, waterfallTotals marks subtotal and total bars that span from zero, bars colour by sign with green for positive and red for negative and total bars a neutral slate overridable through waterfallColors, and thin connectors link bar tops so the cumulative trend reads at a glance. The grid drives it: tick a row's isTotal checkbox and the chart re-renders with the new running calculation.
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.
Imports, features and API used
Imports: @svgrid/grid
Table features registered: rowSortingFeature, columnFilteringFeature
Columns: label (Line item), value (Amount), isTotal (Total?)
SvGridApi methods called: api.getDisplayedRows()
Frequently asked questions
How do I mark a subtotal?
waterfallTotals is a boolean array parallel to categories; a true entry makes that bar span from zero to the running total instead of stacking on the previous bar.
How are negatives drawn?
A negative value draws downward from the previous top in the negative colour, and the running total continues from its bottom.
Can I change the colours?
Yes. waterfallColors takes positive, negative and total colours; by default they are green, red and slate.
Related documentation
Related articles
- Sparkline Cells in a Svelte Data Grid - Show inline trend sparklines inside grid cells using SvGrid's built-in sparkline column property - no charting library needed, just a field that holds a number array.
Source code (158-chart-waterfall.svelte)
<!-- Documented in: docs/help/charts/types.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 GridColumns,
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: GridColumns<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()
}
/** Pane size, so the chart fills its card rather than a fixed viewBox. */
let paneW = $state(0)
let paneH = $state(0)
</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&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-w-0 min-h-0">
<SvGrid responsive={true}
columnResize
data={rows}
columns={columns}
features={features}
sortable
filterable
selectionMode="none"
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="rounded-lg border p-3" style="flex: 0 1 780px; min-width: 0; min-height: 0; border-color: var(--sg-border); background: var(--sg-bg);">
<!-- Measured box, not the card: its height comes from the parent, so the
chart cannot push the thing it is sized against. -->
<div style="width: 100%; height: 100%; min-height: 0;" bind:clientWidth={paneW} bind:clientHeight={paneH}>
{#if paneW > 40 && paneH > 40}
<SvGridChart {spec} formatValue={compact} dataLabels width={paneW} height={paneH} />
{/if}
</div>
</div>
</div>
</section>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.