{"spec_id":"dot-matrix-proportional","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ndot-matrix-proportional: Dot Matrix Chart for Proportional Counts\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 84/100 | Created: 2026-05-08\n\"\"\"\n\nimport os as _os\nimport sys\n\n\n# Prevent this file (bokeh.py) from shadowing the real bokeh package.\n# When Python runs a script, it inserts the script's directory into sys.path[0].\n# Since this file IS named bokeh.py, that directory entry must be removed first.\n_here = _os.path.dirname(_os.path.abspath(__file__))\nsys.path = [p for p in sys.path if _os.path.abspath(p) != _here]\ndel _here, _os\n\nimport os\nimport time\nfrom pathlib import Path\n\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Legend, LegendItem, Range1d\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\"\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 (position 1 = brand green, always first series)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data: Exercise Frequency Survey, 200 respondents\ncategories = [\"Daily\", \"3–5 times/week\", \"1–2 times/week\", \"Rarely / Never\"]\ncounts = [52, 74, 46, 28]\nN_COLS = 20\nN_ROWS = 10  # 20 × 10 = 200 total dots\n\n# Build per-dot color list (filled left-to-right, top-to-bottom)\ndot_colors = []\nfor i, cnt in enumerate(counts):\n    dot_colors.extend([IMPRINT[i]] * cnt)\n\n# Compute grid coordinates (row 0 displayed at top → y = N_ROWS-1)\nxs, ys, colors = [], [], []\nidx = 0\nfor row in range(N_ROWS):\n    for col in range(N_COLS):\n        xs.append(col)\n        ys.append(N_ROWS - 1 - row)\n        colors.append(dot_colors[idx])\n        idx += 1\n\n# Figure — landscape canvas\np = figure(\n    width=4800,\n    height=2700,\n    title=\"dot-matrix-proportional  ·  bokeh  ·  anyplot.ai\",\n    toolbar_location=None,\n    x_range=Range1d(-0.8, N_COLS - 0.2),\n    y_range=Range1d(-0.6, N_ROWS - 0.3),\n)\n\n# Draw one renderer per category; collect legend items manually\nlegend_items = []\ntotal = sum(counts)\nrenderers = []\nfor i, (cat, cnt) in enumerate(zip(categories, counts, strict=True)):\n    mask = [j for j, c in enumerate(colors) if c == IMPRINT[i]]\n    pct = round(cnt / total * 100)\n    src = ColumnDataSource(\n        data={\n            \"x\": [xs[j] for j in mask],\n            \"y\": [ys[j] for j in mask],\n            \"category\": [cat] * len(mask),\n            \"count\": [cnt] * len(mask),\n            \"pct\": [pct] * len(mask),\n        }\n    )\n    r = p.scatter(\"x\", \"y\", source=src, color=IMPRINT[i], size=140, line_color=PAGE_BG, line_width=2)\n    renderers.append(r)\n    legend_items.append(LegendItem(label=f\"{cat}  ({cnt} · {pct}%)\", renderers=[r]))\n\nhover = HoverTool(renderers=renderers, tooltips=[(\"Category\", \"@category\"), (\"Count\", \"@count\"), (\"Share\", \"@pct%\")])\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\np.title.text_font_size = \"28pt\"\np.title.text_color = INK\np.title.align = \"center\"\n\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = None\np.add_tools(hover)\n\n# Legend in right panel (outside plot area so no overlap with data)\nlegend = Legend(\n    items=legend_items,\n    title=f\"n = {total} respondents\",\n    title_text_font_size=\"20pt\",\n    title_text_color=INK,\n    background_fill_color=ELEVATED_BG,\n    border_line_color=INK_SOFT,\n    label_text_color=INK_SOFT,\n    label_text_font_size=\"22pt\",\n    glyph_height=55,\n    glyph_width=55,\n    spacing=18,\n    padding=25,\n)\np.add_layout(legend, \"right\")\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)\n\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"}