{"spec_id":"mosaic-categorical","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nmosaic-categorical: Mosaic Plot for Categorical Association Analysis\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_rect,\n    geom_text,\n    ggplot,\n    labs,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Data - Titanic survival data by passenger class\ndata = {\n    \"class\": [\"First\", \"First\", \"Second\", \"Second\", \"Third\", \"Third\"],\n    \"survival\": [\"Survived\", \"Did Not Survive\", \"Survived\", \"Did Not Survive\", \"Survived\", \"Did Not Survive\"],\n    \"count\": [203, 122, 118, 167, 178, 528],\n}\ndf = pd.DataFrame(data)\nsurvival_order = [\"Survived\", \"Did Not Survive\"]\n\n# Calculate proportions for mosaic geometry\ntotal = df[\"count\"].sum()\nclass_totals = df.groupby(\"class\")[\"count\"].sum()\nclass_order = [\"First\", \"Second\", \"Third\"]\nwidths = {c: class_totals[c] / total for c in class_order}\n\n# Build rectangle data for each cell — widths ∝ class size, heights ∝ conditional proportion\ngap = 0.02\nrects = []\nx_pos = 0\nx_centers = {}\nsurvival_rates = {}\n\nfor cls in class_order:\n    class_data = df[df[\"class\"] == cls]\n    class_total = class_data[\"count\"].sum()\n    width = widths[cls] - gap\n    x_centers[cls] = x_pos + width / 2\n    survived_n = class_data[class_data[\"survival\"] == \"Survived\"][\"count\"].values[0]\n    survival_rates[cls] = survived_n / class_total\n\n    y_pos = 0\n    for surv in survival_order:\n        row = class_data[class_data[\"survival\"] == surv].iloc[0]\n        height = (row[\"count\"] / class_total) * (1 - gap)\n        rects.append(\n            {\n                \"xmin\": x_pos,\n                \"xmax\": x_pos + width,\n                \"ymin\": y_pos,\n                \"ymax\": y_pos + height,\n                \"survival\": row[\"survival\"],\n                \"count\": row[\"count\"],\n                \"x_center\": x_pos + width / 2,\n                \"y_center\": y_pos + height / 2,\n            }\n        )\n        y_pos += height + gap / 2\n\n    x_pos += widths[cls]\n\nrect_df = pd.DataFrame(rects)\n\n# Class name labels and per-class survival rate annotations below the mosaic\nclass_labels = pd.DataFrame({\"x\": [x_centers[c] for c in class_order], \"y\": [-0.055] * 3, \"label\": class_order})\nrate_labels = pd.DataFrame(\n    {\n        \"x\": [x_centers[c] for c in class_order],\n        \"y\": [-0.115] * 3,\n        \"label\": [f\"{survival_rates[c]:.0%} survived\" for c in class_order],\n    }\n)\n\n# Okabe-Ito: green → Survived (first series), vermillion → Did Not Survive\ncolors = {\"Survived\": IMPRINT[0], \"Did Not Survive\": IMPRINT[1]}\n\n# scale_x_continuous / scale_y_continuous give explicit domain + expansion control —\n# avoids clipping the below-axis class and rate labels\nplot = (\n    ggplot(rect_df)\n    + geom_rect(aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", fill=\"survival\"), color=PAGE_BG, size=1.5)\n    + geom_text(aes(x=\"x_center\", y=\"y_center\", label=\"count\"), color=\"white\", size=14, fontweight=\"bold\")\n    + geom_text(data=class_labels, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), size=14, color=INK, fontweight=\"bold\")\n    + geom_text(data=rate_labels, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), size=12, color=INK_SOFT)\n    + scale_fill_manual(values=colors, breaks=[\"Survived\", \"Did Not Survive\"])\n    + scale_x_continuous(expand=(0, 0.01), limits=(0, 1.01))\n    + scale_y_continuous(expand=(0, 0), limits=(-0.17, 1.04))\n    + labs(\n        title=\"mosaic-categorical · python · plotnine · anyplot.ai\",\n        subtitle=\"First-class survival rate (62%) was 2.5× higher than Third-class (25%)\",\n        y=\"Conditional Survival Proportion\",\n        fill=\"Outcome\",\n    )\n    + theme_minimal()\n    + theme(\n        figure_size=(16, 9),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_border=element_blank(),\n        panel_grid=element_blank(),\n        axis_line_x=element_line(color=INK_SOFT, size=0.5),\n        axis_line_y=element_line(color=INK_SOFT, size=0.5),\n        plot_title=element_text(size=24, ha=\"center\", color=INK, fontweight=\"bold\"),\n        plot_subtitle=element_text(size=17, ha=\"center\", color=INK_SOFT),\n        axis_title_x=element_blank(),\n        axis_title_y=element_text(size=20, color=INK),\n        axis_text_x=element_blank(),\n        axis_ticks_major_x=element_blank(),\n        axis_text_y=element_text(size=16, color=INK_SOFT),\n        legend_title=element_text(size=18, color=INK),\n        legend_text=element_text(size=16, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_position=\"right\",\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=300)\n"}