Funnel chart
B2B sales & marketing demand-generation pipeline
Example from the compendium of canonical charts
Python Code
"""Funnel chart — B2B sales & marketing demand-generation pipeline."""
import plotly.graph_objects as go
# A representative quarter of a B2B SaaS demand-gen → revenue funnel. Nine stages
# from top-of-funnel site traffic all the way down to closed-won deals, with the
# kind of stage-to-stage conversion rates a real RevOps team would recognise
# (~60% per step, leakiest around the marketing-to-sales handoff).
STAGES = [
("Website Visitors", 486_000),
("Engaged Sessions", 312_000),
("Marketing Qualified Leads", 121_000),
("Sales Accepted Leads", 79_500),
("Sales Qualified Leads", 49_200),
("Opportunities", 30_400),
("Proposals Sent", 18_600),
("Negotiations", 11_300),
("Closed Won", 6_920),
]
def gradient(c0, c1, n):
"""n hex colors interpolated from c0 to c1 (violet → teal funnel ramp)."""
a = [int(c0[i:i + 2], 16) for i in (1, 3, 5)]
b = [int(c1[i:i + 2], 16) for i in (1, 3, 5)]
return [
"#%02X%02X%02X" % tuple(
round(a[j] + (b[j] - a[j]) * (k / (n - 1))) for j in range(3)
)
for k in range(n)
]
def generate():
labels = [s[0] for s in STAGES]
counts = [s[1] for s in STAGES]
pct_prev = [100.0] + [counts[i] / counts[i - 1] * 100 for i in range(1, len(counts))]
drops = [100 - p for p in pct_prev[1:]]
leakiest_idx = drops.index(max(drops)) + 1
leakiest = labels[leakiest_idx]
pct_init = [c / counts[0] * 100 for c in counts]
overall = pct_init[-1]
print("stage-to-stage conversion:")
for lbl, p in zip(labels, pct_prev):
print(f" {lbl:<26} {p:5.1f}%")
print(f" end-to-end visitor → customer: {overall:.2f}%")
# Hand the hover its numbers as plain values via customdata — Plotly's built-in
# %{percentInitial}/%{percentPrevious} double-apply a "%" d3 format and render
# garbage (e.g. 6420%), so we format the percentages ourselves.
customdata = list(zip(pct_prev, pct_init))
fig = go.Figure(go.Funnel(
y=labels,
x=counts,
customdata=customdata,
textposition="auto",
textinfo="value+percent previous",
insidetextfont=dict(color="#ffffff"),
outsidetextfont=dict(color=MUTED),
marker=dict(color=gradient(VIOLET, TEAL, len(STAGES))),
connector=dict(line=dict(color="rgba(0,0,0,0.12)", width=1)),
hovertemplate=(
"<b>%{label}</b><br>"
"%{value:,}<br>"
"%{customdata[0]:.0f}% of previous stage<br>"
"%{customdata[1]:.1f}% of all visitors"
"<extra></extra>"
),
))
fig.add_annotation(
x=0.5, y=-0.08, xref="paper", yref="paper",
text=(
f"Leakiest handoff: <b>{leakiest}</b> "
f"— {drops[leakiest_idx - 1]:.0f}% drop-off · "
f"{overall:.1f}% visitor-to-customer overall"
),
showarrow=False,
font=dict(size=11, color=MUTED),
)
fig.update_layout(
title=dict(text="B2B Sales & Marketing Funnel", x=0.5),
margin=dict(t=60, b=90, l=170, r=40),
height=520,
)
return fig
if __name__ == "__main__":
generate()
Made with Plotly