{"spec_id":"mosaic-categorical","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nmosaic-categorical: Mosaic Plot for Categorical Association Analysis\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\n\nimport pandas as pd\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\"\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)\n\n# Pivot to create contingency table\ncontingency = df.pivot(index=\"Survival\", columns=\"Class\", values=\"Count\")\ncontingency = contingency[[\"First\", \"Second\", \"Third\"]]\n\n# \"Did Not Survive\" at bottom, \"Survived\" at top\nsurvival_order = [\"Did Not Survive\", \"Survived\"]\ncontingency = contingency.reindex(survival_order)\n\n# Calculate proportions for mosaic layout\nclass_totals = contingency.sum(axis=0)\ntotal = class_totals.sum()\nclass_widths = class_totals / total\n\nsurvival_categories = contingency.index.tolist()\nclass_categories = contingency.columns.tolist()\n\n# Okabe-Ito palette: Survived = brand green (first series), Did Not Survive = vermillion\ncolors = {\"Survived\": \"#009E73\", \"Did Not Survive\": \"#C475FD\"}\n\n# Gap between rectangles\ngap = 0.015\n\n# Build shapes, annotations, and hover trace data\nshapes = []\nannotations = []\nhover_xs, hover_ys, hover_texts, hover_colors = [], [], [], []\n\nx_start = 0\nfor class_name in class_categories:\n    width = class_widths[class_name] - gap\n    class_total = contingency[class_name].sum()\n\n    y_start = 0\n    for survival in survival_categories:\n        count = contingency.loc[survival, class_name]\n        height = count / class_total\n        pct = count / class_total * 100\n\n        # Rectangle shape\n        shapes.append(\n            {\n                \"type\": \"rect\",\n                \"x0\": x_start,\n                \"y0\": y_start,\n                \"x1\": x_start + width,\n                \"y1\": y_start + height,\n                \"fillcolor\": colors[survival],\n                \"line\": {\"color\": PAGE_BG, \"width\": 2},\n                \"layer\": \"below\",\n            }\n        )\n\n        # Count annotation inside rectangle\n        if height > 0.08:\n            annotations.append(\n                {\n                    \"x\": x_start + width / 2,\n                    \"y\": y_start + height / 2,\n                    \"text\": f\"<b>{count}</b>\",\n                    \"showarrow\": False,\n                    \"font\": {\"size\": 20, \"color\": \"white\"},\n                    \"xanchor\": \"center\",\n                    \"yanchor\": \"middle\",\n                }\n            )\n\n        # Collect hover point data (per cell)\n        hover_xs.append(x_start + width / 2)\n        hover_ys.append(y_start + height / 2)\n        hover_texts.append(\n            f\"<b>{class_name} Class — {survival}</b><br>\"\n            f\"Count: {count}<br>\"\n            f\"Within-class proportion: {pct:.1f}%<br>\"\n            f\"Share of all passengers: {count / total * 100:.1f}%\"\n        )\n        hover_colors.append(colors[survival])\n\n        y_start += height\n\n    # Class label below column\n    annotations.append(\n        {\n            \"x\": x_start + width / 2,\n            \"y\": -0.08,\n            \"text\": f\"<b>{class_name}</b>\",\n            \"showarrow\": False,\n            \"font\": {\"size\": 22, \"color\": INK},\n            \"xanchor\": \"center\",\n            \"yanchor\": \"top\",\n        }\n    )\n\n    x_start += width + gap\n\n# Create figure\nfig = go.Figure()\n\n# Legend traces (invisible markers shown only in legend)\nfor survival_name in [\"Survived\", \"Did Not Survive\"]:\n    fig.add_trace(\n        go.Scatter(\n            x=[None],\n            y=[None],\n            mode=\"markers\",\n            marker={\"size\": 20, \"color\": colors[survival_name]},\n            name=survival_name,\n            showlegend=True,\n        )\n    )\n\n# Per-cell invisible scatter for interactive hover tooltips\nfig.add_trace(\n    go.Scatter(\n        x=hover_xs,\n        y=hover_ys,\n        mode=\"markers\",\n        marker={\"size\": 1, \"color\": hover_colors, \"opacity\": 0},\n        hovertemplate=\"%{text}<extra></extra>\",\n        text=hover_texts,\n        showlegend=False,\n    )\n)\n\nfig.update_layout(\n    title={\n        \"text\": \"mosaic-categorical · python · plotly · anyplot.ai\",\n        \"font\": {\"size\": 28, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    shapes=shapes,\n    annotations=annotations,\n    xaxis={\n        \"title\": {\"text\": \"Passenger Class\", \"font\": {\"size\": 22, \"color\": INK}},\n        \"tickfont\": {\"size\": 18, \"color\": INK_SOFT},\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"showticklabels\": False,\n        \"range\": [-0.15, 1.02],\n        \"linecolor\": INK_SOFT,\n    },\n    yaxis={\n        \"title\": {\"text\": \"Proportion\", \"font\": {\"size\": 22, \"color\": INK}},\n        \"tickfont\": {\"size\": 18, \"color\": INK_SOFT},\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"range\": [-0.12, 1.02],\n        \"linecolor\": INK_SOFT,\n    },\n    legend={\n        \"font\": {\"size\": 18, \"color\": INK_SOFT},\n        \"x\": 1.01,\n        \"y\": 0.75,\n        \"xanchor\": \"left\",\n        \"yanchor\": \"middle\",\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    margin={\"l\": 120, \"r\": 160, \"t\": 100, \"b\": 100},\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=1600, height=900, scale=3)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}