Basic Funnel Chart — Seaborn

A funnel chart visualizes sequential stages of a process where values progressively decrease from one stage to the next. Each stage is represented as a trapezoidal segment that narrows from top to bottom, making it easy to identify drop-offs between stages. This chart is ideal for tracking conversion rates, sales pipelines, and multi-step processes where understanding stage-to-stage transitions is critical.

Basic Funnel Chart rendered with Seaborn

Python source (Seaborn)

""" anyplot.ai
funnel-basic: Basic Funnel Chart
Library: seaborn 0.13.2 | Python 3.14.4
Quality: 90/100 | Updated: 2026-04-26
"""

import os

import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.patches import Polygon


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Okabe-Ito palette — first series always #009E73
IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030"]

# Sales funnel data
stages = ["Awareness", "Interest", "Consideration", "Intent", "Purchase"]
values = [1000, 600, 400, 200, 100]
max_value = values[0]
percentages = [v / max_value * 100 for v in values]
conversions = [values[i + 1] / values[i] * 100 for i in range(len(values) - 1)]
# Stage transition with the largest drop-off (lowest retention)
worst_idx = min(range(len(conversions)), key=conversions.__getitem__)

sns.set_theme(
    style="white",
    rc={
        "figure.facecolor": PAGE_BG,
        "axes.facecolor": PAGE_BG,
        "text.color": INK,
        "axes.labelcolor": INK,
        "ytick.color": INK,
        "xtick.color": INK_SOFT,
    },
)

fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)

# Seaborn draws the rectangular core of each stage; trapezoidal panels added
# below tie the cores into a continuous funnel silhouette in stage colors.
sns.barplot(
    x=values,
    y=stages,
    hue=stages,
    order=stages,
    palette=IMPRINT[: len(stages)],
    ax=ax,
    legend=False,
    width=0.50,
    edgecolor="none",
)

# Center each bar on x=0 so the silhouette narrows symmetrically
bars = list(ax.patches)[: len(stages)]
for patch in bars:
    patch.set_x(-patch.get_width() / 2)

# Trapezoidal panels between stages — each panel inherits the upper stage color
# so visually each stage = rectangle + tapering trapezoid below it.
for i in range(len(bars) - 1):
    p_top, p_bot = bars[i], bars[i + 1]
    top_y = p_top.get_y() + p_top.get_height()
    bot_y = p_bot.get_y()
    ax.add_patch(
        Polygon(
            [
                (p_top.get_x(), top_y),
                (p_top.get_x() + p_top.get_width(), top_y),
                (p_bot.get_x() + p_bot.get_width(), bot_y),
                (p_bot.get_x(), bot_y),
            ],
            facecolor=IMPRINT[i],
            edgecolor="none",
            zorder=1,
        )
    )

# Closing tail below the last stage so the funnel ends with a proper taper
last_bar = bars[-1]
last_top_y = last_bar.get_y() + last_bar.get_height()
tail_height = 0.50
tail_bot_w = last_bar.get_width() * 0.5
ax.add_patch(
    Polygon(
        [
            (last_bar.get_x(), last_top_y),
            (last_bar.get_x() + last_bar.get_width(), last_top_y),
            (tail_bot_w / 2, last_top_y + tail_height),
            (-tail_bot_w / 2, last_top_y + tail_height),
        ],
        facecolor=IMPRINT[-1],
        edgecolor="none",
        zorder=1,
    )
)

# Emphasise the bar after the worst drop-off with a thicker outline accent
worst_bar = bars[worst_idx + 1]
worst_bar.set_edgecolor(INK)
worst_bar.set_linewidth(2.5)
worst_bar.set_zorder(3)

# Value + percentage labels are placed OUTSIDE bars (right) so narrow stages
# never overflow onto the page background.
right_offset = max_value * 0.04
for i, patch in enumerate(bars):
    cy = patch.get_y() + patch.get_height() / 2
    x_right = patch.get_x() + patch.get_width()
    ax.text(
        x_right + right_offset,
        cy,
        f"{values[i]:,}  ·  {percentages[i]:.0f}%",
        ha="left",
        va="center",
        fontsize=18,
        fontweight="medium",
        color=INK,
    )

# Conversion-rate annotations on the LEFT, with the largest drop-off
# rendered bolder and in full-strength ink for visual emphasis.
left_anchor = -max_value / 2 - max_value * 0.06
for i in range(len(conversions)):
    p_top, p_bot = bars[i], bars[i + 1]
    y_mid = (p_top.get_y() + p_top.get_height() + p_bot.get_y()) / 2
    is_worst = i == worst_idx
    ax.text(
        left_anchor,
        y_mid,
        f"↓ {conversions[i]:.0f}%",
        ha="right",
        va="center",
        fontsize=16 if is_worst else 13,
        fontweight="bold" if is_worst else "normal",
        style="italic",
        color=INK if is_worst else INK_MUTED,
    )

# Awareness on top — matplotlib's default places the first category at the bottom
ax.invert_yaxis()
ax.set_ylim(len(stages) - 1 + tail_height + 0.25, -0.45)

sns.despine(ax=ax, left=True, bottom=True)
ax.set_xticks([])
ax.set_xlabel("")
ax.set_ylabel("")
ax.tick_params(axis="y", labelsize=20, length=0, pad=10)

ax.set_xlim(-max_value * 0.95, max_value * 0.85)

ax.set_title("funnel-basic · seaborn · anyplot.ai", fontsize=24, fontweight="medium", color=INK, pad=20)

plt.tight_layout()
plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG)

Part of Basic Funnel Chart on anyplot.ai.

Other implementations