{"spec_id":"bar-spine","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nbar-spine: Spine Plot for Two-Variable Proportions\nLibrary: seaborn 0.13.2 | Python 3.13.15\nQuality: 89/100 | Updated: 2026-09-27\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport seaborn as sns\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Imprint palette — Retained uses the brand green, Churned uses the semantic\n# red anchor (bad/loss role), per the style guide's status semantic exception.\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.10,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\nsns.set_context(\"notebook\", font_scale=1.0)\n\n# Route the two fill colors through seaborn's palette machinery (matplotlib's\n# ax.bar draws the bars themselves — sns.barplot can't vary bar width per\n# category — but the color lookup for every segment is genuine seaborn usage).\nsns.set_palette(IMPRINT)\npalette = sns.color_palette()\n\n# Data — customers acquired per channel, six months later split into retained/churned\nchannels = [\"Referral\", \"Organic Search\", \"Email\", \"Direct\", \"Paid Social\"]\nretained_counts = np.array([663, 440, 280, 186, 122])\nchurned_counts = np.array([187, 180, 150, 124, 168])\nchannel_totals = retained_counts + churned_counts\ntotal_customers = channel_totals.sum()\n\nfill_order = [\"Retained\", \"Churned\"]\ncolors = [palette[0], palette[4]]\n\nwidths = channel_totals / total_customers\nx_positions = np.cumsum(np.concatenate([[0], widths[:-1]]))\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\nfor i in range(len(channels)):\n    proportions = [retained_counts[i] / channel_totals[i], churned_counts[i] / channel_totals[i]]\n    bottom = 0.0\n\n    for j, proportion in enumerate(proportions):\n        ax.bar(\n            x=x_positions[i],\n            height=proportion,\n            width=widths[i],\n            bottom=bottom,\n            color=colors[j],\n            align=\"edge\",\n            edgecolor=PAGE_BG,\n            linewidth=0.8,\n        )\n\n        if proportion > 0.06:\n            ax.text(\n                x_positions[i] + widths[i] / 2,\n                bottom + proportion / 2,\n                f\"{proportion:.0%}\",\n                ha=\"center\",\n                va=\"center\",\n                fontsize=11,\n                color=\"white\",\n                fontweight=\"bold\",\n            )\n\n        bottom += proportion\n\n# X-axis labels centered under variable-width bars\nax.set_ylim(-0.14, 1.05)\nfor i, channel in enumerate(channels):\n    center = x_positions[i] + widths[i] / 2\n    ax.text(center, -0.05, f\"{channel}\\n(n={channel_totals[i]})\", ha=\"center\", va=\"top\", fontsize=9, color=INK_SOFT)\n\n# Style\ntitle = \"Customer Retention by Channel · bar-spine · python · seaborn · anyplot.ai\"\ntitle_fontsize = round(12 * min(1.0, 67 / len(title)))\ntitle_fontsize = max(title_fontsize, 8)\n\nax.set_xlim(0, 1)\nax.set_xticks([])\nax.set_yticks([0, 0.25, 0.5, 0.75, 1.0])\nax.set_yticklabels([\"0%\", \"25%\", \"50%\", \"75%\", \"100%\"], fontsize=8, color=INK_SOFT)\nax.set_ylabel(\"Proportion of Customers\", fontsize=10, color=INK)\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK)\n\nsns.despine(ax=ax, bottom=True)\nax.spines[\"left\"].set_color(INK_SOFT)\n\nax.yaxis.grid(True, alpha=0.10, linewidth=0.8, color=INK)\n\nlegend_patches = [mpatches.Patch(color=colors[j], label=fill_order[j]) for j in range(len(fill_order))]\nax.legend(\n    handles=legend_patches, loc=\"upper right\", fontsize=8, frameon=True, facecolor=ELEVATED_BG, edgecolor=INK_SOFT\n)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}