{"spec_id":"treemap-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ntreemap-basic: Basic Treemap\nLibrary: bokeh 3.9.2 | Python 3.13.14\nQuality: 81/100 | Updated: 2026-08-04\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, LabelSet, Legend, LegendItem\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\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\n\n# Data - budget allocation by department and project\ndata = [\n    {\"category\": \"Engineering\", \"subcategory\": \"Backend\", \"value\": 220},\n    {\"category\": \"Engineering\", \"subcategory\": \"Frontend\", \"value\": 180},\n    {\"category\": \"Sales\", \"subcategory\": \"Enterprise\", \"value\": 200},\n    {\"category\": \"Marketing\", \"subcategory\": \"Digital\", \"value\": 150},\n    {\"category\": \"Sales\", \"subcategory\": \"SMB\", \"value\": 120},\n    {\"category\": \"Engineering\", \"subcategory\": \"DevOps\", \"value\": 90},\n    {\"category\": \"Marketing\", \"subcategory\": \"Brand\", \"value\": 80},\n    {\"category\": \"HR\", \"subcategory\": \"Recruiting\", \"value\": 70},\n    {\"category\": \"Marketing\", \"subcategory\": \"Events\", \"value\": 60},\n    {\"category\": \"Finance\", \"subcategory\": \"Accounting\", \"value\": 60},\n    {\"category\": \"HR\", \"subcategory\": \"Training\", \"value\": 50},\n    {\"category\": \"Finance\", \"subcategory\": \"Planning\", \"value\": 40},\n]\n\n# Create dataframe\ndf = pd.DataFrame(data)\n\n# Group rows by category (largest total budget first), then by value\n# descending within each category. Keeping same-category rows contiguous\n# makes the squarify layout cluster them spatially, so category membership\n# reads as a spatial grouping and not just a color coincidence.\ncategory_totals = df.groupby(\"category\")[\"value\"].sum().sort_values(ascending=False)\ncategory_rank = {cat: i for i, cat in enumerate(category_totals.index)}\ndf[\"_cat_rank\"] = df[\"category\"].map(category_rank)\ndf = df.sort_values([\"_cat_rank\", \"value\"], ascending=[True, False]).drop(columns=\"_cat_rank\").reset_index(drop=True)\n\n# Map categories to colors using the Imprint palette\nunique_categories = df[\"category\"].unique()\ncategory_color_map = {cat: IMPRINT[i % len(IMPRINT)] for i, cat in enumerate(unique_categories)}\n\n# Extract values and labels\nvalues = df[\"value\"].values\nlabels = df[\"subcategory\"].values\ncategories = df[\"category\"].values\n\n# Normalize sizes to fit in 100x100 area\ntotal_value = sum(values)\nnormalized = [v * 10000 / total_value for v in values]\n\n\n# Squarify algorithm for treemap layout\ndef squarify(sizes, x=0, y=0, w=100, h=100):\n    \"\"\"Layout rectangles using squarify algorithm.\"\"\"\n    rects = []\n    if not sizes:\n        return rects\n\n    remaining = list(enumerate(sizes))\n\n    while remaining:\n        if w >= h:\n            # Horizontal layout\n            row = []\n            row_area = 0\n            best_ratio = float(\"inf\")\n\n            for _i, (idx, size) in enumerate(remaining):\n                test_row = row + [(idx, size)]\n                test_area = row_area + size\n                col_width = test_area / h if h > 0 else 0\n\n                ratios = []\n                for _, s in test_row:\n                    rect_h = s / col_width if col_width > 0 else 0\n                    ratio = max(col_width / rect_h, rect_h / col_width) if rect_h > 0 else float(\"inf\")\n                    ratios.append(ratio)\n                test_ratio = max(ratios) if ratios else float(\"inf\")\n\n                if test_ratio <= best_ratio:\n                    row = test_row\n                    row_area = test_area\n                    best_ratio = test_ratio\n                else:\n                    break\n\n            col_width = row_area / h if h > 0 else 0\n            rect_y = y\n            for idx, size in row:\n                rect_h = size / col_width if col_width > 0 else 0\n                rects.append({\"idx\": idx, \"x\": x, \"y\": rect_y, \"dx\": col_width, \"dy\": rect_h})\n                rect_y += rect_h\n\n            x += col_width\n            w -= col_width\n            remaining = remaining[len(row) :]\n        else:\n            # Vertical layout\n            row = []\n            row_area = 0\n            best_ratio = float(\"inf\")\n\n            for _i, (idx, size) in enumerate(remaining):\n                test_row = row + [(idx, size)]\n                test_area = row_area + size\n                row_height = test_area / w if w > 0 else 0\n\n                ratios = []\n                for _, s in test_row:\n                    rect_w = s / row_height if row_height > 0 else 0\n                    ratio = max(rect_w / row_height, row_height / rect_w) if rect_w > 0 else float(\"inf\")\n                    ratios.append(ratio)\n                test_ratio = max(ratios) if ratios else float(\"inf\")\n\n                if test_ratio <= best_ratio:\n                    row = test_row\n                    row_area = test_area\n                    best_ratio = test_ratio\n                else:\n                    break\n\n            row_height = row_area / w if w > 0 else 0\n            rect_x = x\n            for idx, size in row:\n                rect_w = size / row_height if row_height > 0 else 0\n                rects.append({\"idx\": idx, \"x\": rect_x, \"y\": y, \"dx\": rect_w, \"dy\": row_height})\n                rect_x += rect_w\n\n            y += row_height\n            h -= row_height\n            remaining = remaining[len(row) :]\n\n    return rects\n\n\nrects = squarify(normalized)\nrects = sorted(rects, key=lambda r: r[\"idx\"])\n\n\ndef category_boundaries(rects, categories, tol=1e-6):\n    \"\"\"Find shared edges between rectangles of different categories.\n\n    Returns the (x0, y0, x1, y1) segments to draw as bold divider lines —\n    the spec calls for subtle borders that \"show hierarchy boundaries\", so\n    only edges between two different categories get the heavier treatment.\n    \"\"\"\n    segments = []\n    for i, a in enumerate(rects):\n        ax0, ay0, ax1, ay1 = a[\"x\"], a[\"y\"], a[\"x\"] + a[\"dx\"], a[\"y\"] + a[\"dy\"]\n        for b in rects[i + 1 :]:\n            if categories[a[\"idx\"]] == categories[b[\"idx\"]]:\n                continue\n            bx0, by0, bx1, by1 = b[\"x\"], b[\"y\"], b[\"x\"] + b[\"dx\"], b[\"y\"] + b[\"dy\"]\n\n            if abs(ax1 - bx0) < tol or abs(bx1 - ax0) < tol:\n                shared_x = ax1 if abs(ax1 - bx0) < tol else ax0\n                lo, hi = max(ay0, by0), min(ay1, by1)\n                if hi - lo > tol:\n                    segments.append((shared_x, lo, shared_x, hi))\n\n            if abs(ay1 - by0) < tol or abs(by1 - ay0) < tol:\n                shared_y = ay1 if abs(ay1 - by0) < tol else ay0\n                lo, hi = max(ax0, bx0), min(ax1, bx1)\n                if hi - lo > tol:\n                    segments.append((lo, shared_y, hi, shared_y))\n\n    return segments\n\n\nboundary_segments = category_boundaries(rects, categories)\n\n# Extract rectangle data for plotting\nx_centers = []\ny_centers = []\nwidths = []\nheights = []\ncolors = []\ndisplay_labels = []\nhover_category = []\nhover_subcategory = []\nhover_value = []\nhover_share = []\n\nfor r in rects:\n    idx = r[\"idx\"]\n    rx, ry = r[\"x\"], r[\"y\"]\n    rw, rh = r[\"dx\"], r[\"dy\"]\n\n    x_centers.append(rx + rw / 2)\n    y_centers.append(ry + rh / 2)\n    widths.append(rw)\n    heights.append(rh)\n    colors.append(category_color_map[categories[idx]])\n    hover_category.append(categories[idx])\n    hover_subcategory.append(labels[idx])\n    hover_value.append(int(values[idx]))\n    hover_share.append(round(100 * values[idx] / total_value, 1))\n\n    if rw > 10 and rh > 8:\n        display_labels.append(f\"{labels[idx]}\\n${int(values[idx])}K\")\n    elif rw > 6 or rh > 6:\n        display_labels.append(labels[idx])\n    else:\n        display_labels.append(\"\")\n\n# Create data source\nsource = ColumnDataSource(\n    data={\n        \"x\": x_centers,\n        \"y\": y_centers,\n        \"width\": widths,\n        \"height\": heights,\n        \"color\": colors,\n        \"label\": display_labels,\n        \"category\": hover_category,\n        \"subcategory\": hover_subcategory,\n        \"value\": hover_value,\n        \"share\": hover_share,\n    }\n)\n\n# Hover tooltip — idiomatic bokeh interactivity for the HTML detail view\n# (inert in the static PNG since toolbar_location=None, but active on hover\n# in plot-{THEME}.html)\nhover = HoverTool(\n    tooltips=[\n        (\"Department\", \"@category\"),\n        (\"Project\", \"@subcategory\"),\n        (\"Budget\", \"$@value{0,0}K\"),\n        (\"Share of total\", \"@share%\"),\n    ]\n)\n\n# Create figure\np = figure(\n    width=3200,\n    height=1800,\n    title=\"treemap-basic · bokeh · anyplot.ai\",\n    x_range=(-2, 102),\n    y_range=(-2, 102),\n    tools=[hover],\n    toolbar_location=None,\n)\n\n# Style figure background\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Draw rectangles\np.rect(\n    x=\"x\",\n    y=\"y\",\n    width=\"width\",\n    height=\"height\",\n    source=source,\n    fill_color=\"color\",\n    fill_alpha=0.90,\n    line_color=PAGE_BG,\n    line_width=2,\n    hover_fill_alpha=1.0,\n    hover_line_color=INK,\n)\n\n# Bold divider lines only where two different categories meet — reinforces\n# the hierarchy spatially (not just via color), leaving within-category\n# rectangles separated by the thin uniform border above.\nif boundary_segments:\n    bx0, by0, bx1, by1 = zip(*boundary_segments, strict=True)\n    p.segment(x0=list(bx0), y0=list(by0), x1=list(bx1), y1=list(by1), line_color=INK_SOFT, line_width=5)\n\n# Add labels\nlabels_set = LabelSet(\n    x=\"x\",\n    y=\"y\",\n    text=\"label\",\n    source=source,\n    text_align=\"center\",\n    text_baseline=\"middle\",\n    text_font_size=\"26pt\",\n    text_color=INK,\n)\np.add_layout(labels_set)\n\n# Style title\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.align = \"center\"\n\n# Hide axes for cleaner look\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.visible = False\np.ygrid.visible = False\n\n# Legend — real bokeh Legend anchored in the right gutter, outside the\n# treemap's 0-100 data area, so it never overlaps a rectangle. Each item\n# references an invisible dummy renderer colored from category_color_map.\nlegend_items = []\nfor cat, color in category_color_map.items():\n    dummy = p.scatter(x=[-10], y=[-10], marker=\"square\", size=0, fill_color=color, line_color=color)\n    legend_items.append(LegendItem(label=cat, renderers=[dummy]))\n\nlegend = Legend(\n    items=legend_items,\n    location=\"center\",\n    label_text_font_size=\"30pt\",\n    label_text_color=INK_SOFT,\n    background_fill_color=ELEVATED_BG,\n    border_line_color=INK_SOFT,\n    padding=20,\n    spacing=14,\n)\np.add_layout(legend, \"right\")\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome\nW, H = 3200, 1800\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()}\")\n# Headless Chrome's --window-size sets the OUTER window, which still reserves\n# a phantom title-bar height even headless — pin the viewport exactly via CDP.\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}