{"spec_id":"bar-spine","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nbar-spine: Spine Plot for Two-Variable Proportions\nLibrary: plotly 7.1.0 | Python 3.13.15\nQuality: 92/100 | Updated: 2026-09-27\n\"\"\"\n\nimport os\n\nimport plotly.graph_objects as go\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\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Data: subscription outcomes by acquisition channel\nchannels = [\"Direct\", \"Organic Search\", \"Social Media\", \"Email\", \"Referral\"]\nchannel_sizes = [400, 620, 830, 310, 240]\n\noutcome_counts = {\n    \"Active\": [220, 380, 350, 170, 185],\n    \"On Trial\": [110, 155, 215, 80, 40],\n    \"Cancelled\": [70, 85, 265, 60, 15],\n}\n\n# Bar widths proportional to marginal counts\ntotal = sum(channel_sizes)\nwidths = [c / total for c in channel_sizes]\n\n# Bar centers along the x-axis (cumulative)\ncenters = []\npos = 0.0\nfor w in widths:\n    centers.append(pos + w / 2)\n    pos += w\n\n# Conditional proportions within each bar (heights sum to 1)\noutcome_props = {k: [cnt / m for cnt, m in zip(v, channel_sizes)] for k, v in outcome_counts.items()}\n\n# Imprint semantic exception: the fill categories are literal subscription\n# statuses (Active/On Trial/Cancelled), so we map to the status anchors a\n# reader already expects (good=green, warning=amber, bad=red) instead of the\n# canonical 1->2->3 ordinal sequence.\nCOLORS = {\"Active\": \"#009E73\", \"On Trial\": \"#DDCC77\", \"Cancelled\": \"#AE3030\"}\n# Amber is light — dark ink reads better than white inside that segment.\nTEXT_COLOR = {\"Active\": \"white\", \"On Trial\": INK, \"Cancelled\": \"white\"}\n\n# Plot\nfig = go.Figure()\n\nfor outcome in [\"Active\", \"On Trial\", \"Cancelled\"]:\n    props = outcome_props[outcome]\n    labels = [f\"{p:.0%}\" if p >= 0.09 else \"\" for p in props]\n    fig.add_trace(\n        go.Bar(\n            name=outcome,\n            x=centers,\n            y=props,\n            width=widths,\n            marker_color=COLORS[outcome],\n            marker_line_width=0,\n            text=labels,\n            textposition=\"inside\",\n            textfont=dict(size=11, color=TEXT_COLOR[outcome]),\n            hovertemplate=[\n                f\"<b>{ch}</b><br>{outcome}: {p:.1%}<br>n={m:,}<extra></extra>\"\n                for ch, p, m in zip(channels, props, channel_sizes)\n            ],\n        )\n    )\n\nticktext = [f\"<b>{ch}</b><br>n={m:,}\" for ch, m in zip(channels, channel_sizes)]\n\nfig.update_layout(\n    autosize=False,\n    barmode=\"stack\",\n    bargap=0,\n    title=dict(\n        text=\"Subscription Outcomes by Channel · bar-spine · plotly · anyplot.ai\",\n        font=dict(size=16, color=INK),\n        x=0.5,\n        xanchor=\"center\",\n    ),\n    xaxis=dict(\n        title=dict(text=\"Acquisition Channel\", font=dict(size=12, color=INK)),\n        tickmode=\"array\",\n        tickvals=centers,\n        ticktext=ticktext,\n        tickfont=dict(size=10, color=INK_SOFT),\n        range=[0, 1],\n        showgrid=False,\n        zeroline=False,\n        linecolor=INK_SOFT,\n        showline=True,\n        ticks=\"\",\n    ),\n    yaxis=dict(\n        title=dict(text=\"Proportion of Customers\", font=dict(size=12, color=INK)),\n        tickformat=\".0%\",\n        tickfont=dict(size=10, color=INK_SOFT),\n        tickvals=[0, 0.2, 0.4, 0.6, 0.8, 1.0],\n        # Headroom above the 100% bar tops for the two storytelling callouts.\n        range=[0, 1.22],\n        gridcolor=GRID,\n        showgrid=True,\n        linecolor=INK_SOFT,\n        showline=True,\n        zeroline=False,\n    ),\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font=dict(color=INK),\n    legend=dict(\n        title=dict(text=\"Status\", font=dict(color=INK, size=11)),\n        bgcolor=ELEVATED_BG,\n        bordercolor=INK_SOFT,\n        borderwidth=1,\n        font=dict(color=INK_SOFT, size=10),\n        traceorder=\"normal\",\n        x=1.02,\n        y=1,\n        xanchor=\"left\",\n        yanchor=\"top\",\n    ),\n    margin=dict(l=90, r=150, t=90, b=90),\n)\n\n# Storytelling callouts pointing at the two most interesting segments.\nfig.add_annotation(\n    x=centers[2],\n    y=1.0,\n    xref=\"x\",\n    yref=\"y\",\n    text=\"<b>32% churned</b><br>highest cancellation\",\n    showarrow=True,\n    arrowhead=2,\n    arrowwidth=1.5,\n    arrowcolor=INK_SOFT,\n    ax=0,\n    ay=-40,\n    align=\"center\",\n    font=dict(size=11, color=INK),\n    bgcolor=ELEVATED_BG,\n    bordercolor=INK_SOFT,\n    borderwidth=1,\n    borderpad=6,\n)\nfig.add_annotation(\n    x=centers[4],\n    y=1.0,\n    xref=\"x\",\n    yref=\"y\",\n    text=\"<b>77% active</b><br>best retention\",\n    showarrow=True,\n    arrowhead=2,\n    arrowwidth=1.5,\n    arrowcolor=INK_SOFT,\n    ax=0,\n    ay=-40,\n    align=\"center\",\n    font=dict(size=11, color=INK),\n    bgcolor=ELEVATED_BG,\n    bordercolor=INK_SOFT,\n    borderwidth=1,\n    borderpad=6,\n)\n\n# Save — hard target: 3200 x 1800 (landscape).\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}