Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
Sale
Storytelling with Data: A Data Visualization Guide for Business Professionals
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.Support on Ko-Fi

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 as f"${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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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

SaleBestseller No. 1
Storytelling with Data: A Data Visualization Guide for Business Professionals
Storytelling with Data: A Data Visualization Guide for Business Professionals
Wiley; Language: english; Book - storytelling with data: a data visualization guide for business professionals
$14.87

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.