Comparison / Intermediate

Waterfall Chart

Trace how additions and removals connect a starting value to an ending balance.

Back to collection

Interactive Chart Preview

JAVASCRIPT

Chart.js draws floating bars; a custom plugin adds the connectors.

Live, interactive exampleChart.js

Chart.js Code Example

JAVASCRIPT
import Chart from 'chart.js/auto';

// Illustrative data used by the live preview.
const chartData = {
  "categoryLabel": "Stage",
  "waterfallHeaders": [
    "Stage",
    "Kind",
    "Tickets"
  ],
  "waterfallKinds": [
    "start",
    "change",
    "change",
    "total",
    "change",
    "change",
    "total"
  ],
  "labels": [
    "Opening",
    "Received",
    "Resolved",
    "Midpoint",
    "Reopened",
    "Merged",
    "Closing"
  ],
  "datasets": [
    {
      "label": "Tickets",
      "data": [
        120,
        85,
        -70,
        0,
        12,
        -7,
        0
      ]
    }
  ]
};

function createConfig(chartData) {
    let balance = 0;
    const rows = chartData.datasets[0].data.map((value, index) => {
        const kind = chartData.waterfallKinds[index];
        const from = kind === 'change' ? balance : 0;
        if (kind === 'start')
            balance = value;
        else if (kind === 'change')
            balance += value;
        return { from, to: balance, amount: kind === 'total' ? balance : value,
            color: kind !== 'change' ? '#3b82f6' : value < 0 ? '#dc2626' : '#0d9488' };
    });
    return {
        type: 'bar',
        data: { labels: chartData.labels, datasets: [{
                    label: chartData.datasets[0].label,
                    data: rows.map(row => [row.from, row.to]),
                    backgroundColor: rows.map(row => row.color),
                    barPercentage: 0.75, categoryPercentage: 0.9,
                }] },
        options: {
            responsive: true, maintainAspectRatio: false,
            plugins: {
                legend: { display: false },
                tooltip: { callbacks: { label: context => {
                            const row = rows[context.dataIndex];
                            return `${chartData.waterfallKinds[context.dataIndex]}: ${row.amount}; Balance: ${row.to}`;
                        } } },
            },
            scales: {
                x: { title: { display: true, text: chartData.categoryLabel }, grid: { display: false } },
                y: { beginAtZero: true, title: { display: true, text: chartData.datasets[0].label } },
            },
        },
        plugins: [{
                id: 'waterfall-connectors',
                afterDatasetsDraw: chart => {
                    const bars = chart.getDatasetMeta(0).data;
                    const { ctx } = chart;
                    ctx.save();
                    ctx.strokeStyle = '#94a3b8';
                    ctx.setLineDash([3, 3]);
                    for (let index = 0; index < rows.length - 1; index++) {
                        const left = bars[index].getProps(['x', 'width'], true);
                        const right = bars[index + 1].getProps(['x', 'width'], true);
                        const y = chart.scales.y.getPixelForValue(rows[index].to);
                        ctx.beginPath();
                        ctx.moveTo(left.x + left.width / 2, y);
                        ctx.lineTo(right.x - right.width / 2, y);
                        ctx.stroke();
                    }
                    ctx.restore();
                },
            }],
    };
}

const chart = new Chart(document.getElementById('myChart'), createConfig(chartData));

Preview and code follow the same library selection.

Run This Example

Install the package below in a browser project such as Vite. Put the copied code in a client JavaScript module and add this container to the page before the module runs.

npm install chart.js@4.5.1
<div style="height:320px"><canvas id="myChart"></canvas></div>

The example includes its sample data. When removing the chart, destroy or dispose of its instance and disconnect any resize observer.

Reading This Example

This illustrative ticket backlog opens at 120. Receiving 85 tickets and resolving 70 brings the midpoint to 135. Reopening 12 and merging away 7 brings the closing balance to 140. Midpoint and Closing are calculated totals, so they are not added to the balance again.

All library tabs use these illustrative values. Rendering styles and library APIs differ, so use the table to compare exact numbers.

About this example & sample data
Sample values used in the chart
StageKindTickets
Openingstart120
Receivedchange85
Resolvedchange-70
Midpointtotal135
Reopenedchange12
Mergedchange-7
Closingtotal140

READ THE CHART

Follow a Balance Through Each Change

A waterfall chart shows how a starting value changes through a sequence of additions and removals. Floating bars connect the previous balance to the next one. Total bars return to the zero baseline so you can read the accumulated result.

01

Separate Inputs From Totals

The first row is Start, where you enter the opening value. Change rows contain signed amounts: positive for additions and negative for removals. A Total row shows the current balance automatically. Use it for an intermediate subtotal or the closing total; it does not add anything or reset the running calculation.

02

Read Both Ends of a Floating Bar

A change bar begins at the previous balance and ends at the updated balance. Its length represents the change, not the final balance. Teal indicates an increase, red a decrease, and blue a starting or total value. The row type, signed amounts, connectors, and tooltips provide the same information without relying on color alone.

03

Keep the Sequence and Units Consistent

Rows follow table order. Every amount must use the same units and refer to the same balance. Negative balances and changes that cross zero are supported. A zero change has no bar height. Empty amounts in Start or Change rows are invalid drafts, so the last valid chart remains visible until they are completed.

04

Choose a View That Matches the Question

Use a bar chart for independent category values or a stacked bar chart for nonnegative parts of one total. A waterfall is useful when the changes explain a bridge between two balances. ECharts uses custom interval drawing here; the Seaborn example applies a theme to Matplotlib bars and connectors.

Edit Chart Data