The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →A waterfall chart shows how a starting value changes through a sequence of increases and decreases to reach a final value. Plotly’s go.Waterfall trace handles the running-total logic for you; with Matplotlib, you calculate each bar’s position and draw it with the regular bar API. This guide builds the same revenue bridge in both libraries and explains how to handle subtotals, labels, missing data, and chart choice.
What a waterfall chart shows
A waterfall chart, also called a bridge chart, makes the cumulative effect of sequential changes visible. Its basic relationship is:
ending value = starting value + sum of positive changes + sum of negative changes
For example, revenue starts at 100, increases by 60 and 80, then decreases by 40 and 20. The ending value is 180. Unlike a conventional bar chart, the change bars begin at the running total rather than at zero.
#1 Best Overall
- Wiley
- Language: english
- Book - storytelling with data: a data visualization guide for business professionals
Waterfalls are useful for revenue and profit bridges, budget-to-actual analysis, cash flow, headcount movements, and variance analysis. They are less suited to ranking unrelated categories, where a sorted bar chart is usually clearer.
Prepare the data and classify each bar
Each row needs a category, a value, and—especially when using Plotly—a measure type. The three types have different meanings:
- Absolute: Sets the running total to a specified value, typically the opening bar.
- Relative: Adds or subtracts a change from the current running total.
- Total: Shows the current running total as a bar from zero without changing it. Use it for a closing value or an intermediate subtotal.
Here is a DataFrame for the example. The final row is marked as a total; its value is 0 because Plotly derives the displayed total from the running changes.
import pandas as pd
df = pd.DataFrame({
"label": [
"Starting revenue", "New sales", "Consulting",
"Returns", "Operating costs", "Ending revenue",
],
"value": [100, 60, 80, -40, -20, 0],
"measure": [
"absolute", "relative", "relative",
"relative", "relative", "total",
],
})
allowed_measures = {"absolute", "relative", "total"}
if not (len(df["label"]) == len(df["value"]) == len(df["measure"])):
raise ValueError("All chart columns must have the same length")
if not set(df["measure"]).issubset(allowed_measures):
raise ValueError("Invalid waterfall measure")
Keep rows in the order in which changes occur. A misplaced or missing total marker can make a chart look reasonable while communicating the wrong calculation. Do not silently treat a missing value as zero: first decide whether it means no change, unavailable data, or something else.
Create a waterfall chart with Matplotlib
Matplotlib’s standard plotting API does not provide the same dedicated waterfall trace as Plotly. Compose the chart from bars, connector lines, and text. For an increase, the bar begins at the previous running total; for a decrease, it begins at the new, lower total and has a positive height. A total begins at zero.
These positions are the key calculations for the example:
| Label | Change | Running total | Bottom | Height |
|---|---|---|---|---|
| Starting revenue | 100 | 100 | 0 | 100 |
| New sales | +60 | 160 | 100 | 60 |
| Consulting | +80 | 240 | 160 | 80 |
| Returns | -40 | 200 | 200 | 40 |
| Operating costs | -20 | 180 | 180 | 20 |
| Ending revenue | total | 180 | 0 | 180 |
The following implementation derives the closing total from the changes rather than hard-coding it. It uses Axes.bar(bottom=...) to position each rectangle, and adds connectors and labels separately.
import matplotlib.pyplot as plt
import numpy as np
labels = [
"Starting revenue", "New sales", "Consulting",
"Returns", "Operating costs", "Ending revenue",
]
changes = [100, 60, 80, -40, -20]
running_total = changes[0]
bottoms = [0]
heights = [changes[0]]
colors = ["#4C78A8"]
shown_values = [changes[0]]
for change in changes[1:]:
previous_total = running_total
running_total += change
if change >= 0:
bottoms.append(previous_total)
colors.append("#2CA02C")
else:
bottoms.append(running_total)
colors.append("#D62728")
heights.append(abs(change))
shown_values.append(change)
# Final total is a standalone bar from zero.
bottoms.append(0)
heights.append(running_total)
colors.append("#2F4B7C")
shown_values.append(running_total)
x = np.arange(len(labels))
fig, ax = plt.subplots(figsize=(10, 6))
ax.bar(x, heights, bottom=bottoms, color=colors, width=0.7,
edgecolor="black", linewidth=0.7)
# Connect each bar's top to the next bar.
for i in range(len(labels) - 1):
previous_top = bottoms[i] + heights[i]
ax.plot([x[i] + 0.35, x[i + 1] - 0.35],
[previous_top, previous_top],
color="gray", linewidth=1, linestyle="--")
for i, (bottom, height, value) in enumerate(zip(bottoms, heights, shown_values)):
if i == len(labels) - 1:
y, text = height, f"{value:,.0f}"
elif value < 0:
y, text = bottom, f"{value:,.0f}"
else:
y = bottom + height
text = f"+{value:,.0f}" if i else f"{value:,.0f}"
ax.annotate(text, (x[i], y), xytext=(0, 4),
textcoords="offset points", ha="center", va="bottom")
ax.set_xticks(x, labels, rotation=25, ha="right")
ax.set_ylabel("Revenue")
ax.set_title("Revenue waterfall")
ax.axhline(0, color="black", linewidth=0.8)
ax.grid(axis="y", linestyle=":", alpha=0.5)
ax.set_axisbelow(True)
ax.margins(y=0.12)
fig.tight_layout()
plt.show()
The negative bars use bottom=running_total after the decrease and height=abs(change). Using the previous total as the bottom with a negative height is a common source of misplaced bars. The total bar is drawn from zero, not treated as another change.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Adding an intermediate subtotal
To show a subtotal partway through a bridge, stop the relative sequence at that point and add a bar whose bottom is zero and whose height is the current running total. Then continue accumulating later changes from that same running total. This total bar is a visual checkpoint, not an additional change.
Create an interactive waterfall chart with Plotly
Plotly has a dedicated go.Waterfall trace. Provide the categories, values, and measures in matching order; the measure array tells the trace which bars reset, change, or display the running total.
import plotly.graph_objects as go
fig = go.Figure(go.Waterfall(
name="Revenue",
orientation="v",
measure=df["measure"],
x=df["label"],
y=df["value"],
text=["100", "+60", "+80", "-40", "-20", "180"],
textposition="outside",
connector={"line": {"color": "gray", "width": 1, "dash": "dot"}},
increasing={"marker": {"color": "#2CA02C"}},
decreasing={"marker": {"color": "#D62728"}},
totals={"marker": {"color": "#2F4B7C"}},
))
fig.update_layout(
title="Revenue waterfall",
yaxis_title="Revenue",
showlegend=False,
waterfallgap=0.35,
)
fig.update_traces(hovertemplate="<b>%{x}</b><br>Amount: %{y:,.0f}<extra></extra>")
fig.show()
The trace API documents measure, connector styling, increasing/decreasing/total styling, text positioning, and hover templates in the Plotly waterfall reference. The Plotly waterfall tutorial includes examples of basic waterfalls, multiple totals, and figures used in Dash applications.
Horizontal orientation
For a horizontal waterfall, put category names on y and numeric values on x, and set orientation="h". This can help when category labels are long.
Recommended Free Tools
Best Value
fig = go.Figure(go.Waterfall(
orientation="h",
measure=["absolute", "relative", "relative", "total"],
y=["Opening balance", "Sales", "Costs", "Closing balance"],
x=[100, 50, -30, 0],
connector={"line": {"color": "gray"}},
increasing={"marker": {"color": "seagreen"}},
decreasing={"marker": {"color": "indianred"}},
totals={"marker": {"color": "steelblue"}},
))
fig.update_layout(title="Balance movement")
fig.show()
Multiple subtotals and comparisons
Use "total" at any checkpoint that should show the running balance, not just at the final category. For example, a sequence of ["absolute", "relative", "relative", "total", "relative", "relative", "total"] shows an opening amount, two changes, a subtotal, two more changes, and a final total.
Plotly also supports multiple waterfall traces and grouped category labels for comparisons such as years, regions, or scenarios. That can become visually crowded; separate small charts may be easier to read than several bridges overlaid or grouped together.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Make the chart accurate and readable
- Use explicit signs. Label changes with a plus or minus sign so a reader can distinguish movements from balances.
- Label units and source. Identify whether values are dollars, units, percentages, or another measure, and include the data source in the chart caption or accompanying report.
- Format values consistently. Plotly hover templates accept numeric formatting such as
%{y:,.0f}; a currency label can use$%{y:,.0f}. Matplotlib labels can use Python formatting such asf"${value:,.0f}". - Give outside labels room. Matplotlib needs y-axis headroom when annotations sit above bars. In Plotly, outside labels can collide or be clipped in a dense chart; try a different text position or omit nonessential labels.
- Do not rely on color alone. Green for increases and red for decreases is familiar, but use signs and labels too. A blue/orange or other accessible palette can distinguish direction without relying solely on red and green.
- Keep calculations and display rounding distinct. Calculate the running total at full precision and round only for presentation. If the displayed components appear not to equal the displayed total, state the rounding convention.
- Limit the number of steps. Group immaterial changes into an “Other” category, use a horizontal layout, or pair a concise waterfall with a detailed table when labels become hard to scan.
If a value is missing, do not quietly substitute zero. Handle it explicitly in the data pipeline, for example by raising an error with if pd.isna(value): raise ValueError("Missing waterfall value; decide whether it means zero or unknown"), or by applying a documented business rule.
Export and publish the result
Matplotlib figures can be saved to static formats such as PNG, SVG, or PDF using fig.savefig("revenue-waterfall.png", dpi=300, bbox_inches="tight"); choose the format and resolution that suit the report or publication.
Plotly figures are interactive in notebooks and browsers and can be embedded in applications. Static image export depends on the Plotly and renderer setup; a static export renderer such as Kaleido may be required, so check the current Plotly static image export documentation for the environment-specific requirements. A Plotly figure can also be used in a Dash Graph component when the chart is part of a Python web application.
Matplotlib or Plotly?
| Need | Matplotlib | Plotly |
|---|---|---|
| Waterfall construction | Build from bars, positions, lines, and labels | Dedicated go.Waterfall trace |
| Interactivity | Not built into this static chart workflow | Hover, zoom, and browser interaction built in |
| Typical fit | Static reports, papers, and print figures | Notebooks, web pages, and dashboards |
| Control | Direct control over bar geometry and annotations | Declarative styling with built-in waterfall semantics |
| Dash application integration | Requires additional integration | Plotly figures can be placed in Dash Graph components |
Choose Matplotlib when the deliverable is a carefully styled static figure or the surrounding workflow already uses Matplotlib. Choose Plotly when readers need interactive exploration or the chart belongs in a browser-based dashboard. The core Plotly.py library is described by Plotly as free and open source; hosting and publishing services are separate and are not required just to build a chart locally.
Quick Recap
When a different chart is clearer
- Use a standard bar chart to rank independent categories.
- Use a line chart to show a value changing over time.
- Use a stacked bar chart to show composition at a point in time.
- Use a tornado chart for sensitivity comparisons.
- Use a Sankey diagram when the emphasis is flow between entities rather than sequential net changes.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

