{"spec_id":"mosaic-categorical","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nmosaic-categorical: Mosaic Plot for Categorical Association Analysis\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\nimport sys\n\n\n# Work around filename shadowing the altair library\nsys.path.pop(0)\n\nimport altair as alt\nimport pandas as pd\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# Okabe-Ito positions 1 and 2\nCOLOR_SURVIVED = \"#009E73\"\nCOLOR_NOT_SURVIVED = \"#C475FD\"\n\n# Data: Titanic survival by passenger class (contingency table)\ndata = pd.DataFrame(\n    {\n        \"Class\": [\"1st\", \"1st\", \"2nd\", \"2nd\", \"3rd\", \"3rd\", \"Crew\", \"Crew\"],\n        \"Survival\": [\n            \"Survived\",\n            \"Did Not Survive\",\n            \"Survived\",\n            \"Did Not Survive\",\n            \"Survived\",\n            \"Did Not Survive\",\n            \"Survived\",\n            \"Did Not Survive\",\n        ],\n        \"Count\": [203, 122, 118, 167, 178, 528, 212, 673],\n    }\n)\n\n# Calculate mosaic layout: widths from marginal proportions, heights from conditional proportions\nclass_totals = data.groupby(\"Class\")[\"Count\"].sum().reset_index()\nclass_totals.columns = [\"Class\", \"ClassTotal\"]\ntotal = class_totals[\"ClassTotal\"].sum()\nclass_totals[\"Width\"] = class_totals[\"ClassTotal\"] / total\n\nclass_order = [\"1st\", \"2nd\", \"3rd\", \"Crew\"]\nclass_totals[\"ClassOrder\"] = class_totals[\"Class\"].map({c: i for i, c in enumerate(class_order)})\nclass_totals = class_totals.sort_values(\"ClassOrder\")\nclass_totals[\"x_start\"] = class_totals[\"Width\"].cumsum() - class_totals[\"Width\"]\nclass_totals[\"x_end\"] = class_totals[\"Width\"].cumsum()\nclass_totals[\"x_mid\"] = (class_totals[\"x_start\"] + class_totals[\"x_end\"]) / 2\n\ndata = data.merge(class_totals[[\"Class\", \"Width\", \"x_start\", \"x_end\", \"x_mid\", \"ClassTotal\"]], on=\"Class\")\ndata[\"Height\"] = data[\"Count\"] / data[\"ClassTotal\"]\n\nsurvival_order = {\"Survived\": 0, \"Did Not Survive\": 1}\ndata[\"SurvivalOrder\"] = data[\"Survival\"].map(survival_order)\ndata = data.sort_values([\"Class\", \"SurvivalOrder\"])\n\ny_positions = []\nfor cls in class_order:\n    cls_data = data[data[\"Class\"] == cls].sort_values(\"SurvivalOrder\")\n    cumsum = 0.0\n    for idx in cls_data.index:\n        y_positions.append({\"index\": idx, \"y_start\": cumsum, \"y_end\": cumsum + data.loc[idx, \"Height\"]})\n        cumsum += data.loc[idx, \"Height\"]\n\ny_df = pd.DataFrame(y_positions).set_index(\"index\")\ndata[\"y_start\"] = data.index.map(y_df[\"y_start\"])\ndata[\"y_end\"] = data.index.map(y_df[\"y_end\"])\ndata[\"y_mid\"] = (data[\"y_start\"] + data[\"y_end\"]) / 2\ndata[\"Percentage\"] = (data[\"Count\"] / total * 100).round(1)\n\n# Plot: mosaic rectangles with axis titles for both categorical dimensions\nmosaic = (\n    alt.Chart(data)\n    .mark_rect(stroke=PAGE_BG, strokeWidth=3)\n    .encode(\n        x=alt.X(\"x_start:Q\", axis=alt.Axis(title=\"Passenger Class\", labels=False, ticks=False, domain=False)),\n        x2=alt.X2(\"x_end:Q\"),\n        y=alt.Y(\"y_start:Q\", axis=alt.Axis(title=\"Proportion\", labels=False, ticks=False, domain=False)),\n        y2=alt.Y2(\"y_end:Q\"),\n        color=alt.Color(\n            \"Survival:N\",\n            scale=alt.Scale(domain=[\"Survived\", \"Did Not Survive\"], range=[COLOR_SURVIVED, COLOR_NOT_SURVIVED]),\n            legend=alt.Legend(\n                title=\"Survival Status\", titleFontSize=20, labelFontSize=18, orient=\"right\", symbolSize=400\n            ),\n        ),\n        tooltip=[\"Class:N\", \"Survival:N\", \"Count:Q\", \"Percentage:Q\"],\n    )\n)\n\n# Count labels — white on green (survived), theme ink on vermillion (did not survive)\nlabels = (\n    alt.Chart(data)\n    .mark_text(fontSize=22, fontWeight=\"bold\", align=\"center\", baseline=\"middle\")\n    .encode(\n        x=alt.X(\"x_mid:Q\"),\n        y=alt.Y(\"y_mid:Q\"),\n        text=alt.Text(\"Count:Q\"),\n        color=alt.condition(alt.datum.Survival == \"Survived\", alt.value(\"white\"), alt.value(INK)),\n    )\n)\n\n# Class name labels at top of each column\nclass_labels_df = class_totals[[\"Class\", \"x_mid\"]].copy()\nclass_labels = (\n    alt.Chart(class_labels_df)\n    .mark_text(fontSize=20, fontWeight=\"bold\", baseline=\"top\", dy=15, color=INK)\n    .encode(x=alt.X(\"x_mid:Q\"), y=alt.value(1.0), text=\"Class:N\")\n)\n\n# Combine layers with theme-adaptive chrome\nchart = (\n    alt.layer(mosaic, labels, class_labels)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\"mosaic-categorical · python · altair · anyplot.ai\", fontSize=28, anchor=\"middle\"),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        titleFontSize=22,\n    )\n    .configure_title(color=INK)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save outputs\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}