{"spec_id":"mosaic-categorical","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nmosaic-categorical: Mosaic Plot for Categorical Association Analysis\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\n\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_rect,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\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# Okabe-Ito palette — first series always #009E73\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data — Titanic survival cross-tabulated by passenger class\ncategories_1 = [\"First Class\", \"Second Class\", \"Third Class\", \"Crew\"]\ncategories_2 = [\"Survived\", \"Did Not Survive\"]\n\nfrequencies = {\"First Class\": [202, 123], \"Second Class\": [118, 167], \"Third Class\": [178, 528], \"Crew\": [212, 673]}\n\n# Column widths proportional to category_1 marginals\ncat1_totals = {cat: sum(freqs) for cat, freqs in frequencies.items()}\ngrand_total = sum(cat1_totals.values())\ncat1_widths = {cat: total / grand_total * 100 for cat, total in cat1_totals.items()}\n\n# Build rectangle coordinates for the mosaic\nrects = []\nx_pos = 0\ngap = 0.8\n\nfor cat1 in categories_1:\n    col_width = cat1_widths[cat1] - gap\n    col_freqs = frequencies[cat1]\n    col_total = cat1_totals[cat1]\n\n    y_pos = 0\n    for i, cat2 in enumerate(categories_2):\n        freq = col_freqs[i]\n        segment_height = (freq / col_total) * 100\n        pct = round(freq / col_total * 100)\n\n        rects.append(\n            {\n                \"category_1\": cat1,\n                \"category_2\": cat2,\n                \"frequency\": freq,\n                \"pct\": pct,\n                \"pct_label\": f\"{pct}%\",\n                \"label\": f\"{freq}\\n({pct}%)\",\n                \"xmin\": x_pos + gap / 2,\n                \"xmax\": x_pos + col_width + gap / 2,\n                \"ymin\": y_pos + 0.3,\n                \"ymax\": y_pos + segment_height - 0.3,\n                \"x_center\": x_pos + cat1_widths[cat1] / 2,\n                \"y_center\": y_pos + segment_height / 2,\n                \"tile_height\": segment_height,\n            }\n        )\n        y_pos += segment_height\n\n    x_pos += cat1_widths[cat1]\n\ndf = pd.DataFrame(rects)\n\n# X-axis breaks at column centres\nx_breaks = []\nx_pos = 0\nfor cat1 in categories_1:\n    x_breaks.append(x_pos + cat1_widths[cat1] / 2)\n    x_pos += cat1_widths[cat1]\n\n# Interactive tooltips — letsplot-distinctive feature for HTML output\ntooltips = (\n    layer_tooltips()\n    .line(\"Class|@category_1\")\n    .line(\"Status|@category_2\")\n    .line(\"Count|@frequency\")\n    .line(\"Share|@pct_label\")\n)\n\n# Plot\nanyplot_theme = theme(\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_major=element_blank(),\n    panel_grid_minor=element_blank(),\n    axis_line=element_line(color=INK_SOFT),\n    axis_title=element_text(color=INK, size=20),\n    axis_text=element_text(color=INK_SOFT, size=16),\n    axis_text_x=element_text(color=INK_SOFT, size=16),\n    axis_text_y=element_text(color=INK_SOFT, size=16),\n    plot_title=element_text(color=INK, size=24, hjust=0.5, face=\"bold\"),\n    plot_subtitle=element_text(color=INK_SOFT, size=15, hjust=0.5),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=16),\n    legend_title=element_text(color=INK, size=18),\n    legend_position=\"right\",\n)\n\nplot = (\n    ggplot(df)\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", fill=\"category_2\"),\n        color=PAGE_BG,\n        size=0.5,\n        tooltips=tooltips,\n    )\n    + geom_text(aes(x=\"x_center\", y=\"y_center\", label=\"label\"), size=13, color=\"white\", fontface=\"bold\")\n    + scale_fill_manual(values=IMPRINT[:2], name=\"Survival Status\")\n    + scale_x_continuous(name=\"Passenger Class  (width ∝ count)\", breaks=x_breaks, labels=categories_1, limits=[0, 100])\n    + scale_y_continuous(name=\"Survival Rate (%)\", limits=[0, 100], breaks=[0, 25, 50, 75, 100])\n    + labs(\n        title=\"mosaic-categorical · python · letsplot · anyplot.ai\",\n        subtitle=\"First-class passengers were 2.5× more likely to survive than third-class passengers\",\n    )\n    + theme_minimal()\n    + anyplot_theme\n    + ggsize(1600, 960)\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=3)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}