{"spec_id":"treemap-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ntreemap-basic: Basic Treemap\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-08-04\n\"\"\"\n\nimport os\nimport sys\n\nimport pandas as pd\n\n\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nif _script_dir in sys.path:\n    sys.path.remove(_script_dir)\nimport altair as alt\nfrom PIL import Image\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# Imprint palette (categorical, canonical order)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Data - market capitalization by sector and company (billions USD)\ndata = [\n    {\"category\": \"Technology\", \"subcategory\": \"Apple\", \"value\": 2800},\n    {\"category\": \"Technology\", \"subcategory\": \"Microsoft\", \"value\": 2400},\n    {\"category\": \"Technology\", \"subcategory\": \"Google\", \"value\": 1800},\n    {\"category\": \"Technology\", \"subcategory\": \"NVIDIA\", \"value\": 1200},\n    {\"category\": \"Finance\", \"subcategory\": \"JPMorgan\", \"value\": 500},\n    {\"category\": \"Finance\", \"subcategory\": \"BofA\", \"value\": 300},\n    {\"category\": \"Finance\", \"subcategory\": \"Wells Fargo\", \"value\": 200},\n    {\"category\": \"Healthcare\", \"subcategory\": \"UnitedHealth\", \"value\": 450},\n    {\"category\": \"Healthcare\", \"subcategory\": \"J&J\", \"value\": 380},\n    {\"category\": \"Healthcare\", \"subcategory\": \"Pfizer\", \"value\": 250},\n    {\"category\": \"Energy\", \"subcategory\": \"Exxon\", \"value\": 420},\n    {\"category\": \"Energy\", \"subcategory\": \"Chevron\", \"value\": 300},\n    {\"category\": \"Consumer\", \"subcategory\": \"Amazon\", \"value\": 1500},\n    {\"category\": \"Consumer\", \"subcategory\": \"Walmart\", \"value\": 400},\n    {\"category\": \"Consumer\", \"subcategory\": \"Tesla\", \"value\": 600},\n]\ndf = pd.DataFrame(data)\n\n# Canvas dimensions - Altair inner view (see prompts/library/altair.md \"Canvas\")\nwidth = 620\nheight = 320\n\n\n# --- Squarified treemap layout (Bruls, Huizing & van Wijk, 2000) -----------\n# A strip layout (categories as plain vertical bands) produces elongated,\n# hard-to-scan slivers for small subcategories. Squarifying keeps every\n# rectangle's aspect ratio close to 1:1, which is both easier to read and\n# closer to how disk-usage / finance treemap tools lay hierarchy out.\ndef squarify(sizes, x, y, w, h):\n    \"\"\"Lay `sizes` (already normalized so sum(sizes) == w * h) into the\n    x, y, w, h rectangle. Returns rects in the same order as `sizes`.\"\"\"\n    sizes = list(sizes)\n    rects = []\n    while sizes:\n        side = min(w, h)\n        row = [sizes[0]]\n        for size in sizes[1:]:\n            if _worst_ratio([*row, size], side) <= _worst_ratio(row, side):\n                row.append(size)\n            else:\n                break\n        row_sum = sum(row)\n        if w >= h:\n            row_w = row_sum / h\n            ry = y\n            for size in row:\n                rh = (size / row_sum) * h\n                rects.append((x, ry, row_w, rh))\n                ry += rh\n            x, w = x + row_w, w - row_w\n        else:\n            row_h = row_sum / w\n            rx = x\n            for size in row:\n                rw = (size / row_sum) * w\n                rects.append((rx, y, rw, row_h))\n                rx += rw\n            y, h = y + row_h, h - row_h\n        sizes = sizes[len(row) :]\n    return rects\n\n\ndef _worst_ratio(row, side):\n    row_sum = sum(row)\n    row_max, row_min = max(row), min(row)\n    return max((side**2 * row_max) / row_sum**2, row_sum**2 / (side**2 * row_min))\n\n\ndef normalize(values, area):\n    total = sum(values)\n    return [v / total * area for v in values]\n\n\nGUTTER_OUTER = 3.2  # gap between category groups, in view units\nGUTTER_INNER = 1.1  # gap between subcategory cells within a group\n\ncategory_totals = df.groupby(\"category\")[\"value\"].sum().sort_values(ascending=False)\nsorted_cats = list(category_totals.index)\ncolor_map = {cat: IMPRINT[i % len(IMPRINT)] for i, cat in enumerate(sorted_cats)}\n\ncat_sizes = normalize(list(category_totals.to_numpy()), width * height)\ncat_boxes_raw = squarify(cat_sizes, 0, 0, width, height)\n\ncategory_boxes = []\nall_rects = []\nfor cat, (cx, cy, cw, ch) in zip(sorted_cats, cat_boxes_raw, strict=True):\n    category_boxes.append({\"category\": cat, \"x\": cx, \"y\": cy, \"x2\": cx + cw, \"y2\": cy + ch})\n\n    # Inset the group so a visible gap separates it from its neighbors.\n    ix, iy = cx + GUTTER_OUTER / 2, cy + GUTTER_OUTER / 2\n    iw, ih = max(cw - GUTTER_OUTER, 1.0), max(ch - GUTTER_OUTER, 1.0)\n\n    cat_df = df[df[\"category\"] == cat].sort_values(\"value\", ascending=False)\n    cat_total = cat_df[\"value\"].sum()\n    sub_sizes = normalize(list(cat_df[\"value\"].to_numpy()), iw * ih)\n    sub_boxes = squarify(sub_sizes, ix, iy, iw, ih)\n\n    for (_, row), (sx, sy, sw, sh) in zip(cat_df.iterrows(), sub_boxes, strict=True):\n        gx, gy = min(GUTTER_INNER / 2, sw / 3), min(GUTTER_INNER / 2, sh / 3)\n        dx, dy = max(sw - 2 * gx, 0.5), max(sh - 2 * gy, 0.5)\n        all_rects.append(\n            {\n                \"category\": cat,\n                \"subcategory\": row[\"subcategory\"],\n                \"value\": row[\"value\"],\n                \"share_of_category\": row[\"value\"] / cat_total,\n                \"x\": sx + gx,\n                \"y\": sy + gy,\n                \"x2\": sx + gx + dx,\n                \"y2\": sy + gy + dy,\n                \"x_center\": sx + gx + dx / 2,\n                \"y_center\": sy + gy + dy / 2,\n                \"area\": dx * dy,\n            }\n        )\n\ncategory_df = pd.DataFrame(category_boxes)\nrects_df = pd.DataFrame(all_rects)\nrects_df[\"display_value\"] = rects_df[\"value\"].apply(lambda v: f\"${v}B\")\n\nmin_area_for_label = width * height * 0.018\n\n# Group outline - a heavier border around each category shows the nesting\n# depth (group -> item) independently of color.\ngroup_outline = (\n    alt.Chart(category_df)\n    .mark_rect(filled=False, stroke=INK_SOFT, strokeWidth=2.2, strokeOpacity=0.55)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[0, width]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[0, height]), axis=None),\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n    )\n)\n\n# Hover selection - a distinctly Altair/Vega-Lite feature (a declarative\n# param bound to encoding channels) rather than a plain static layer.\nhover = alt.selection_point(on=\"pointerover\", fields=[\"subcategory\"], empty=False)\n\ncells = (\n    alt.Chart(rects_df)\n    .mark_rect()\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[0, width]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[0, height]), axis=None),\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n        color=alt.Color(\n            \"category:N\",\n            scale=alt.Scale(domain=list(color_map.keys()), range=list(color_map.values())),\n            legend=alt.Legend(\n                title=\"Sector\",\n                titleFontSize=11,\n                labelFontSize=9,\n                labelLimit=200,\n                padding=10,\n                symbolSize=90,\n                orient=\"right\",\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n                titleColor=INK,\n                labelColor=INK_SOFT,\n            ),\n        ),\n        stroke=alt.condition(hover, alt.value(INK), alt.value(PAGE_BG)),\n        strokeWidth=alt.condition(hover, alt.value(3.0), alt.value(1.0)),\n        tooltip=[\n            alt.Tooltip(\"category:N\", title=\"Sector\"),\n            alt.Tooltip(\"subcategory:N\", title=\"Company\"),\n            alt.Tooltip(\"display_value:N\", title=\"Market Cap\"),\n        ],\n    )\n    .add_params(hover)\n)\n\n# Shading overlay - the smaller a cell is relative to its own sector, the\n# more it is tinted toward the ink token. This reads as depth/weight within\n# the hierarchy (per spec: \"nesting depth or color shading intensity\")\n# without altering the underlying categorical hue used for the legend.\nshading = (\n    alt.Chart(rects_df)\n    .mark_rect(fill=INK)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[0, width]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[0, height]), axis=None),\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n        opacity=alt.Opacity(\"share_of_category:Q\", scale=alt.Scale(domain=[0, 1], range=[0.22, 0.0]), legend=None),\n    )\n)\n\nlabels_df = rects_df[rects_df[\"area\"] >= min_area_for_label]\n\nname_labels = (\n    alt.Chart(labels_df)\n    .mark_text(fontSize=12, fontWeight=\"bold\", color=INK, dy=-9)\n    .encode(\n        x=alt.X(\"x_center:Q\", scale=alt.Scale(domain=[0, width])),\n        y=alt.Y(\"y_center:Q\", scale=alt.Scale(domain=[0, height])),\n        text=\"subcategory:N\",\n    )\n)\n\nvalue_labels = (\n    alt.Chart(labels_df)\n    .mark_text(fontSize=10, color=INK, dy=8)\n    .encode(\n        x=alt.X(\"x_center:Q\", scale=alt.Scale(domain=[0, width])),\n        y=alt.Y(\"y_center:Q\", scale=alt.Scale(domain=[0, height])),\n        text=\"display_value:N\",\n    )\n)\n\nchart = (\n    alt.layer(group_outline, cells, shading, name_labels, value_labels)\n    .properties(\n        width=width,\n        height=height,\n        background=PAGE_BG,\n        title=alt.Title(text=\"treemap-basic · python · altair · anyplot.ai\", fontSize=16, anchor=\"middle\", color=INK),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_axis(domainColor=INK_SOFT, tickColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n    .configure_title(color=INK)\n)\n\n# Save outputs - hard target 3200x1800 (see prompts/library/altair.md \"Canvas\")\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}x{_h}, exceeds target {TW}x{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n"}