{"spec_id":"mosaic-categorical","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nmosaic-categorical: Mosaic Plot for Categorical Association Analysis\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Label\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Okabe-Ito palette — first series always #009E73\nSURVIVAL_COLORS = {\"Survived\": \"#009E73\", \"Did Not Survive\": \"#C475FD\"}\n\n# Data - Titanic survival data (class vs survival)\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# Calculate proportions\ntotal = df[\"Count\"].sum()\nclass_totals = df.groupby(\"Class\")[\"Count\"].sum()\nclass_order = [\"First\", \"Second\", \"Third\"]\nsurvival_order = [\"Survived\", \"Did Not Survive\"]\n\n# Build mosaic rectangles\nrectangles = []\ngap = 0.015\nplot_width = 0.90\nx_start = 0.05\nmosaic_bottom = 0.12\nmosaic_height = 0.75\n\nfor class_name in class_order:\n    class_data = df[df[\"Class\"] == class_name]\n    class_total = class_totals[class_name]\n    class_width = (class_total / total) * plot_width - gap\n\n    y_start = mosaic_bottom\n    for survival in survival_order:\n        cell_data = class_data[class_data[\"Survival\"] == survival]\n        if len(cell_data) > 0:\n            count = cell_data[\"Count\"].values[0]\n            cell_height = (count / class_total) * mosaic_height - gap / 2\n            pct = count / class_total * 100\n\n            rectangles.append(\n                {\n                    \"left\": x_start,\n                    \"right\": x_start + class_width,\n                    \"bottom\": y_start,\n                    \"top\": y_start + cell_height,\n                    \"class\": class_name,\n                    \"survival\": survival,\n                    \"count\": count,\n                    \"pct\": f\"{pct:.1f}%\",\n                }\n            )\n            y_start += (count / class_total) * mosaic_height\n\n    x_start += (class_total / total) * plot_width\n\n# Assign Okabe-Ito colors\ncolors = [SURVIVAL_COLORS[r[\"survival\"]] for r in rectangles]\n\n# ColumnDataSource\nsource = ColumnDataSource(\n    data={\n        \"left\": [r[\"left\"] for r in rectangles],\n        \"right\": [r[\"right\"] for r in rectangles],\n        \"bottom\": [r[\"bottom\"] for r in rectangles],\n        \"top\": [r[\"top\"] for r in rectangles],\n        \"color\": colors,\n        \"class\": [r[\"class\"] for r in rectangles],\n        \"survival\": [r[\"survival\"] for r in rectangles],\n        \"count\": [r[\"count\"] for r in rectangles],\n        \"pct\": [r[\"pct\"] for r in rectangles],\n    }\n)\n\n# Figure\np = figure(\n    width=4800,\n    height=2700,\n    title=\"mosaic-categorical · python · bokeh · anyplot.ai\",\n    x_range=(0, 1),\n    y_range=(0, 1),\n    tools=\"\",\n    toolbar_location=None,\n)\n\n# Mosaic rectangles\nquads = p.quad(\n    left=\"left\",\n    right=\"right\",\n    bottom=\"bottom\",\n    top=\"top\",\n    source=source,\n    color=\"color\",\n    line_color=PAGE_BG,\n    line_width=4,\n    alpha=0.9,\n    name=\"mosaic\",\n)\n\n# HoverTool for interactivity\nhover = HoverTool(\n    renderers=[quads],\n    tooltips=[(\"Class\", \"@class\"), (\"Status\", \"@survival\"), (\"Count\", \"@count\"), (\"Proportion\", \"@pct\")],\n)\np.add_tools(hover)\n\n# Count labels inside each rectangle (light text on colored backgrounds)\nfor rect in rectangles:\n    cx = (rect[\"left\"] + rect[\"right\"]) / 2\n    cy = (rect[\"bottom\"] + rect[\"top\"]) / 2\n    label = Label(\n        x=cx,\n        y=cy,\n        text=str(rect[\"count\"]),\n        text_align=\"center\",\n        text_baseline=\"middle\",\n        text_font_size=\"36pt\",\n        text_font_style=\"bold\",\n        text_color=\"#FFFDF6\",\n    )\n    p.add_layout(label)\n\n# Class labels at bottom of each column\nx_pos = 0.05\nfor class_name in class_order:\n    class_total = class_totals[class_name]\n    class_width = (class_total / total) * plot_width - gap\n    label = Label(\n        x=x_pos + class_width / 2,\n        y=0.07,\n        text=class_name,\n        text_align=\"center\",\n        text_baseline=\"middle\",\n        text_font_size=\"32pt\",\n        text_font_style=\"bold\",\n        text_color=INK,\n    )\n    p.add_layout(label)\n    x_pos += (class_total / total) * plot_width\n\n# X-axis description\nx_axis_label = Label(\n    x=0.5,\n    y=0.02,\n    text=\"Passenger Class  (column width ∝ share of total passengers)\",\n    text_align=\"center\",\n    text_baseline=\"bottom\",\n    text_font_size=\"24pt\",\n    text_color=INK_SOFT,\n)\np.add_layout(x_axis_label)\n\n# Centered legend — two items horizontally balanced around plot center (~0.49)\nlegend_y = 0.92\nlegend_items = [\n    {\"color\": \"#009E73\", \"text\": \"Survived\", \"x\": 0.38},\n    {\"color\": \"#C475FD\", \"text\": \"Did Not Survive\", \"x\": 0.58},\n]\nfor item in legend_items:\n    p.quad(\n        left=[item[\"x\"] - 0.015],\n        right=[item[\"x\"] + 0.015],\n        bottom=[legend_y - 0.018],\n        top=[legend_y + 0.018],\n        color=item[\"color\"],\n        line_color=PAGE_BG,\n        line_width=2,\n    )\n    label = Label(\n        x=item[\"x\"] + 0.025,\n        y=legend_y,\n        text=item[\"text\"],\n        text_align=\"left\",\n        text_baseline=\"middle\",\n        text_font_size=\"26pt\",\n        text_color=INK_SOFT,\n    )\n    p.add_layout(label)\n\n# Subtitle\nsubtitle = Label(\n    x=0.5,\n    y=0.97,\n    text=\"Titanic Survival by Passenger Class\",\n    text_align=\"center\",\n    text_baseline=\"top\",\n    text_font_size=\"32pt\",\n    text_font_style=\"italic\",\n    text_color=INK_SOFT,\n)\np.add_layout(subtitle)\n\n# Style\np.title.text_font_size = \"40pt\"\np.title.text_font_style = \"bold\"\np.title.align = \"center\"\np.title.text_color = INK\n\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.visible = False\np.ygrid.visible = False\n\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome via Selenium\nW, H = 4800, 2700\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}