Waterfall Chart for a Gross to Net Bridge
If you are a controller, month end ends with the same question from the finance director: we sold this much, we kept this much, so where did the difference go? A table of steps holds the answer and hides it. Returns, discounts, freight and tax are all in there, but you have to add them up in your head to see which one took the most.
A waterfall chart answers it at a glance. Gross sales is the first bar, each deduction is a step down from the running total, and the last bar is net revenue. The chart above is bound to the grid beneath it, so filtering the rows redraws the bridge. Filter the Region column to Online and the steps shrink to what the online channel gave up between gross and net.
Why a waterfall and not a bar chart
A bar chart puts every amount on the same baseline, so the returns bar and the tax bar sit side by side and the reader has to subtract them from the gross bar in their head. A waterfall starts each step where the last one ended, so the gap between the first bar and the last is the sum of the steps, drawn. Use bars to compare amounts with each other, and a waterfall when the amounts add up to something and you want to see how.
The chart over a grid
The chart reads the grid’s rows and adds up the rows that share a step, so there is nothing to keep in step. Here is the whole example in plain JavaScript. It builds the grid from thirty inline rows, one per step and region, then draws the bridge from it.
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
const regions = ['North', 'South', 'East', 'West', 'Online', 'Wholesale'];
const steps = {
'Gross sales': [412000, 358000, 391000, 337000, 276000, 189000],
Returns: [-18400, -21500, -14200, -12900, -15800, -9100],
Discounts: [-24600, -19800, -22100, -18400, -16200, -21700],
Freight: [-9200, -11400, -8600, -10100, -7300, -5200],
Tax: [-31500, -27200, -29800, -25600, -21100, -14400],
};
const rows = Object.entries(steps).flatMap(([step, amounts], s) =>
amounts.map((amount, r) => ({ id: s * 6 + r, step, region: regions[r], amount })));
const gridEl = document.createElement('div');
const chartEl = document.createElement('div');
gridEl.style.height = chartEl.style.height = '300px';
document.body.append(gridEl, chartEl);
const grid = createGrid(gridEl, {
rowKey: 'id',
rows,
columns: [
{ field: 'step', title: 'Step' },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'amount', title: 'Amount', type: 'number' },
],
});
createChart({ grid, container: chartEl, type: 'waterfall', x: 'step', y: 'amount',
waterfall: { total: 'Net revenue' }, title: 'Gross sales to net revenue' });
The same thing in React
The React adapter is a factory, so you build the component once at module scope. The ref’s grid is the real grid, which is what the chart binds to.
import React, { useEffect, useRef } from 'react';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createLatticeGrid } from '@toclocoinc/lattice-grid/modules/react';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
const LatticeGrid = createLatticeGrid({ React, createGrid });
const regions = ['North', 'South', 'East'];
const steps = {
'Gross sales': [412000, 358000, 391000],
Returns: [-18400, -21500, -14200],
Discounts: [-24600, -19800, -22100],
Freight: [-9200, -11400, -8600],
Tax: [-31500, -27200, -29800],
};
const rows = Object.entries(steps).flatMap(([step, amounts], s) =>
amounts.map((amount, r) => ({ id: s * 3 + r, step, region: regions[r], amount })));
const columns = [
{ field: 'step', title: 'Step' },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'amount', title: 'Amount', type: 'number' },
];
export function Bridge() {
const gridRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
const chart = createChart({ grid: gridRef.current.grid, container: chartRef.current,
type: 'waterfall', x: 'step', y: 'amount', waterfall: { total: 'Net revenue' } });
return () => chart.destroy();
}, []);
return (<>
<LatticeGrid ref={gridRef} rowKey="id" columns={columns} rows={rows} style={{ height: 300 }} />
<div ref={chartRef} style={{ height: 300 }} />
</>);
}
The options used
These are the rows of the charts reference for the options in the listings above.
| Option | Type | What it does |
|---|---|---|
grid |
Grid | Required. The grid to read. |
container |
Element or string | Required. Where to draw. |
type |
string | 'waterfall' here. |
x / y |
string | The category and measure columns. |
waterfall |
{ total?: boolean or string } |
total adds a closing bar after the last step, drawn from the baseline to the running total. A string labels it, so a label such as Net revenue names the bridge’s end. |
title |
string | Drawn above the plot. |
Keep the order of the steps in the rows
Each category is a step from the running total, in the order the rows list them. Put gross sales first and the deductions after it, as the sample rows do, and the closing bar is the net.
Next
See every Cartesian type on one dataset in the Cartesian chart demo, read the full list of chart types on the charts page, or follow the how-to for a chart that follows the grid.