{"spec_id":"bar-spine","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nbar-spine: Spine Plot for Two-Variable Proportions\nLibrary: letsplot 4.11.0 | Python 3.13.15\nQuality: 90/100 | Updated: 2026-09-27\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_hline,\n    geom_rect,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\n\n\nLetsPlot.setup_html()\n\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\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\nANYPLOT_AMBER = \"#DDCC77\"\nBORDER_COLOR = \"rgba(128, 128, 128, 0.35)\"  # theme-neutral separator, stays visible on both surfaces\n\n# Data: customer churn by subscription tier\ntiers = [\"Basic\", \"Standard\", \"Premium\", \"Enterprise\"]\nretained_counts = [120, 400, 320, 180]\nchurned_counts = [180, 200, 80, 20]\n\ntotals = [r + c for r, c in zip(retained_counts, churned_counts, strict=True)]\ngrand_total = sum(totals)\n\nwidths = [t / grand_total for t in totals]\nx_starts = np.concatenate([[0.0], np.cumsum(widths[:-1])])\nx_ends = np.cumsum(widths)\nx_mids = (x_starts + x_ends) / 2\n\n# Build rectangle segments for each tier x status combination\nrecords = []\nfor i, tier in enumerate(tiers):\n    total = totals[i]\n    retain_prop = retained_counts[i] / total\n    churn_prop = churned_counts[i] / total\n\n    records.append(\n        {\n            \"tier\": tier,\n            \"status\": \"Retained\",\n            \"xmin\": float(x_starts[i]),\n            \"xmax\": float(x_ends[i]),\n            \"ymin\": 0.0,\n            \"ymax\": retain_prop,\n            \"x_mid\": float(x_mids[i]),\n            \"y_mid\": retain_prop / 2,\n            \"segment_height\": retain_prop,\n            \"label\": f\"{retain_prop:.0%}\",\n        }\n    )\n    records.append(\n        {\n            \"tier\": tier,\n            \"status\": \"Churned\",\n            \"xmin\": float(x_starts[i]),\n            \"xmax\": float(x_ends[i]),\n            \"ymin\": retain_prop,\n            \"ymax\": 1.0,\n            \"x_mid\": float(x_mids[i]),\n            \"y_mid\": retain_prop + churn_prop / 2,\n            \"segment_height\": churn_prop,\n            \"label\": f\"{churn_prop:.0%}\",\n        }\n    )\n\ndf = pd.DataFrame(records)\ndf_labels = df[df[\"segment_height\"] >= 0.08].copy()\n\n# Enterprise has the lowest churn rate; a benchmark line at that level lets\n# every other tier's much taller churned segment be read against it directly.\nbest_churn_rate = churned_counts[-1] / totals[-1]\nbenchmark_y = 1.0 - best_churn_rate\nbenchmark_label = pd.DataFrame(\n    {\n        \"x\": [x_starts[0] + 0.01],\n        \"y\": [benchmark_y + 0.035],\n        \"label\": [f\"Enterprise-level churn ({best_churn_rate:.0%})\"],\n    }\n)\n\n# Theme\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid=element_blank(),\n    axis_title=element_text(color=INK, size=12),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    axis_line=element_line(color=INK_SOFT),\n    axis_ticks=element_blank(),\n    plot_title=element_text(color=INK, size=16),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=10),\n    legend_title=element_text(color=INK, size=12),\n)\n\n# Plot\nplot = (\n    ggplot(df)\n    + geom_rect(aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", fill=\"status\"), color=BORDER_COLOR, size=1.2)\n    + geom_hline(yintercept=benchmark_y, linetype=\"dashed\", color=ANYPLOT_AMBER, size=1)\n    + geom_text(\n        data=benchmark_label, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), color=INK, size=4, hjust=0, fontface=\"bold\"\n    )\n    + geom_text(data=df_labels, mapping=aes(x=\"x_mid\", y=\"y_mid\", label=\"label\"), color=\"#FAFAFA\", size=5)\n    + scale_fill_manual(values={\"Retained\": IMPRINT[0], \"Churned\": IMPRINT[1]}, name=\"Status\")\n    + scale_x_continuous(expand=[0, 0], breaks=list(x_mids), labels=tiers)\n    + scale_y_continuous(expand=[0, 0], breaks=[0.0, 0.25, 0.5, 0.75, 1.0], labels=[\"0%\", \"25%\", \"50%\", \"75%\", \"100%\"])\n    + labs(x=\"Subscription Tier\", y=\"Customer Proportion\", title=\"bar-spine · python · letsplot · anyplot.ai\")\n    + anyplot_theme\n    + ggsize(800, 450)\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}