{"spec_id":"funnel-basic","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nfunnel-basic: Basic Funnel Chart\nLibrary: seaborn 0.13.2 | Python 3.14.4\nQuality: 90/100 | Updated: 2026-04-26\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom matplotlib.patches import Polygon\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Okabe-Ito palette — first series always #009E73\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\n\n# Sales funnel data\nstages = [\"Awareness\", \"Interest\", \"Consideration\", \"Intent\", \"Purchase\"]\nvalues = [1000, 600, 400, 200, 100]\nmax_value = values[0]\npercentages = [v / max_value * 100 for v in values]\nconversions = [values[i + 1] / values[i] * 100 for i in range(len(values) - 1)]\n# Stage transition with the largest drop-off (lowest retention)\nworst_idx = min(range(len(conversions)), key=conversions.__getitem__)\n\nsns.set_theme(\n    style=\"white\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"text.color\": INK,\n        \"axes.labelcolor\": INK,\n        \"ytick.color\": INK,\n        \"xtick.color\": INK_SOFT,\n    },\n)\n\nfig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Seaborn draws the rectangular core of each stage; trapezoidal panels added\n# below tie the cores into a continuous funnel silhouette in stage colors.\nsns.barplot(\n    x=values,\n    y=stages,\n    hue=stages,\n    order=stages,\n    palette=IMPRINT[: len(stages)],\n    ax=ax,\n    legend=False,\n    width=0.50,\n    edgecolor=\"none\",\n)\n\n# Center each bar on x=0 so the silhouette narrows symmetrically\nbars = list(ax.patches)[: len(stages)]\nfor patch in bars:\n    patch.set_x(-patch.get_width() / 2)\n\n# Trapezoidal panels between stages — each panel inherits the upper stage color\n# so visually each stage = rectangle + tapering trapezoid below it.\nfor i in range(len(bars) - 1):\n    p_top, p_bot = bars[i], bars[i + 1]\n    top_y = p_top.get_y() + p_top.get_height()\n    bot_y = p_bot.get_y()\n    ax.add_patch(\n        Polygon(\n            [\n                (p_top.get_x(), top_y),\n                (p_top.get_x() + p_top.get_width(), top_y),\n                (p_bot.get_x() + p_bot.get_width(), bot_y),\n                (p_bot.get_x(), bot_y),\n            ],\n            facecolor=IMPRINT[i],\n            edgecolor=\"none\",\n            zorder=1,\n        )\n    )\n\n# Closing tail below the last stage so the funnel ends with a proper taper\nlast_bar = bars[-1]\nlast_top_y = last_bar.get_y() + last_bar.get_height()\ntail_height = 0.50\ntail_bot_w = last_bar.get_width() * 0.5\nax.add_patch(\n    Polygon(\n        [\n            (last_bar.get_x(), last_top_y),\n            (last_bar.get_x() + last_bar.get_width(), last_top_y),\n            (tail_bot_w / 2, last_top_y + tail_height),\n            (-tail_bot_w / 2, last_top_y + tail_height),\n        ],\n        facecolor=IMPRINT[-1],\n        edgecolor=\"none\",\n        zorder=1,\n    )\n)\n\n# Emphasise the bar after the worst drop-off with a thicker outline accent\nworst_bar = bars[worst_idx + 1]\nworst_bar.set_edgecolor(INK)\nworst_bar.set_linewidth(2.5)\nworst_bar.set_zorder(3)\n\n# Value + percentage labels are placed OUTSIDE bars (right) so narrow stages\n# never overflow onto the page background.\nright_offset = max_value * 0.04\nfor i, patch in enumerate(bars):\n    cy = patch.get_y() + patch.get_height() / 2\n    x_right = patch.get_x() + patch.get_width()\n    ax.text(\n        x_right + right_offset,\n        cy,\n        f\"{values[i]:,}  ·  {percentages[i]:.0f}%\",\n        ha=\"left\",\n        va=\"center\",\n        fontsize=18,\n        fontweight=\"medium\",\n        color=INK,\n    )\n\n# Conversion-rate annotations on the LEFT, with the largest drop-off\n# rendered bolder and in full-strength ink for visual emphasis.\nleft_anchor = -max_value / 2 - max_value * 0.06\nfor i in range(len(conversions)):\n    p_top, p_bot = bars[i], bars[i + 1]\n    y_mid = (p_top.get_y() + p_top.get_height() + p_bot.get_y()) / 2\n    is_worst = i == worst_idx\n    ax.text(\n        left_anchor,\n        y_mid,\n        f\"↓ {conversions[i]:.0f}%\",\n        ha=\"right\",\n        va=\"center\",\n        fontsize=16 if is_worst else 13,\n        fontweight=\"bold\" if is_worst else \"normal\",\n        style=\"italic\",\n        color=INK if is_worst else INK_MUTED,\n    )\n\n# Awareness on top — matplotlib's default places the first category at the bottom\nax.invert_yaxis()\nax.set_ylim(len(stages) - 1 + tail_height + 0.25, -0.45)\n\nsns.despine(ax=ax, left=True, bottom=True)\nax.set_xticks([])\nax.set_xlabel(\"\")\nax.set_ylabel(\"\")\nax.tick_params(axis=\"y\", labelsize=20, length=0, pad=10)\n\nax.set_xlim(-max_value * 0.95, max_value * 0.85)\n\nax.set_title(\"funnel-basic · seaborn · anyplot.ai\", fontsize=24, fontweight=\"medium\", color=INK, pad=20)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}