{"spec_id":"treemap-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ntreemap-basic: Basic Treemap\nLibrary: letsplot 4.11.0 | Python 3.13.14\nQuality: 88/100 | Updated: 2026-08-04\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_rect,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    scale_alpha_identity,\n    scale_color_identity,\n    scale_fill_manual,\n    theme,\n    theme_void,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\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 for consistent color mapping across categories\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Data - Budget allocation with two-level hierarchy\n# Departments (main) and projects/teams (sub) for realistic budget breakdown\ndata = {\n    \"category\": [\n        \"Engineering\",\n        \"Engineering\",\n        \"Engineering\",\n        \"Marketing\",\n        \"Marketing\",\n        \"Sales\",\n        \"Sales\",\n        \"Operations\",\n        \"HR\",\n        \"Finance\",\n    ],\n    \"subcategory\": [\n        \"Backend\",\n        \"Frontend\",\n        \"DevOps\",\n        \"Digital\",\n        \"Events\",\n        \"Enterprise\",\n        \"SMB\",\n        \"Infrastructure\",\n        \"Recruiting\",\n        \"Planning\",\n    ],\n    \"value\": [15, 12, 5, 14, 8, 12, 6, 12, 7, 5],\n}\n\ndf_data = pd.DataFrame(data)\ndf_data = df_data.sort_values(\"value\", ascending=False).reset_index(drop=True)\n\n\ndef squarify(values, x, y, width, height):\n    \"\"\"Compute treemap rectangles using squarify algorithm.\n\n    Tracks a running remaining-total and remaining width/height (rather than\n    the fixed global values) so consumed area always matches the actual\n    remaining container, guaranteeing the tiling fully fills the bounding box.\n    \"\"\"\n    if len(values) == 0:\n        return []\n\n    remaining_total = sum(values)\n    if remaining_total == 0:\n        return []\n\n    rects = []\n    remaining_values = list(values)\n    remaining_x, remaining_y = x, y\n    remaining_w, remaining_h = width, height\n\n    while remaining_values:\n        if remaining_w >= remaining_h:\n            row_values = []\n            row_sum = 0\n            best_ratio = float(\"inf\")\n\n            for v in remaining_values:\n                test_values = row_values + [v]\n                test_sum = row_sum + v\n                row_width = (test_sum / remaining_total) * remaining_w if remaining_total > 0 else 0\n\n                if row_width > 0:\n                    worst_ratio = 0\n                    for rv in test_values:\n                        rect_height = (rv / test_sum) * remaining_h if test_sum > 0 else 0\n                        ratio = (\n                            max(row_width / rect_height, rect_height / row_width) if rect_height > 0 else float(\"inf\")\n                        )\n                        worst_ratio = max(worst_ratio, ratio)\n\n                    if worst_ratio <= best_ratio:\n                        best_ratio = worst_ratio\n                        row_values = test_values\n                        row_sum = test_sum\n                    else:\n                        break\n                else:\n                    row_values = test_values\n                    row_sum = test_sum\n\n            row_width = (row_sum / remaining_total) * remaining_w if remaining_total > 0 else 0\n            current_y = remaining_y\n            for rv in row_values:\n                rect_height = (rv / row_sum) * remaining_h if row_sum > 0 else 0\n                rects.append((remaining_x, current_y, row_width, rect_height))\n                current_y += rect_height\n\n            remaining_x += row_width\n            remaining_w -= row_width\n            remaining_total -= row_sum\n            remaining_values = remaining_values[len(row_values) :]\n        else:\n            col_values = []\n            col_sum = 0\n            best_ratio = float(\"inf\")\n\n            for v in remaining_values:\n                test_values = col_values + [v]\n                test_sum = col_sum + v\n                col_height = (test_sum / remaining_total) * remaining_h if remaining_total > 0 else 0\n\n                if col_height > 0:\n                    worst_ratio = 0\n                    for cv in test_values:\n                        rect_width = (cv / test_sum) * remaining_w if test_sum > 0 else 0\n                        ratio = (\n                            max(col_height / rect_width, rect_width / col_height) if rect_width > 0 else float(\"inf\")\n                        )\n                        worst_ratio = max(worst_ratio, ratio)\n\n                    if worst_ratio <= best_ratio:\n                        best_ratio = worst_ratio\n                        col_values = test_values\n                        col_sum = test_sum\n                    else:\n                        break\n                else:\n                    col_values = test_values\n                    col_sum = test_sum\n\n            col_height = (col_sum / remaining_total) * remaining_h if remaining_total > 0 else 0\n            current_x = remaining_x\n            for cv in col_values:\n                rect_width = (cv / col_sum) * remaining_w if col_sum > 0 else 0\n                rects.append((current_x, remaining_y, rect_width, col_height))\n                current_x += rect_width\n\n            remaining_y += col_height\n            remaining_h -= col_height\n            remaining_total -= col_sum\n            remaining_values = remaining_values[len(col_values) :]\n\n    return rects\n\n\n# Compute treemap layout\nrects = squarify(df_data[\"value\"].tolist(), 0, 0, 100, 100)\n\n# Build rectangle dataframe\nrect_df = pd.DataFrame(\n    {\n        \"xmin\": [r[0] for r in rects],\n        \"ymin\": [r[1] for r in rects],\n        \"xmax\": [r[0] + r[2] for r in rects],\n        \"ymax\": [r[1] + r[3] for r in rects],\n        \"category\": df_data[\"category\"].tolist(),\n        \"subcategory\": df_data[\"subcategory\"].tolist(),\n        \"value\": df_data[\"value\"].tolist(),\n    }\n)\n\n# Calculate label positions\nrect_df[\"label_x\"] = (rect_df[\"xmin\"] + rect_df[\"xmax\"]) / 2\nrect_df[\"label_y\"] = (rect_df[\"ymin\"] + rect_df[\"ymax\"]) / 2\nrect_df[\"width\"] = rect_df[\"xmax\"] - rect_df[\"xmin\"]\nrect_df[\"height\"] = rect_df[\"ymax\"] - rect_df[\"ymin\"]\n\n# Shading intensity by nesting depth: within each department, the largest\n# cost center is fully opaque and successive ones step down in alpha, giving\n# a visual cue for the subcategory hierarchy beyond color alone.\nrect_df[\"subcat_rank\"] = rect_df.groupby(\"category\")[\"value\"].rank(ascending=False, method=\"first\") - 1\nrect_df[\"shade_alpha\"] = (0.95 - 0.15 * rect_df[\"subcat_rank\"]).clip(lower=0.55)\n\n# Create adaptive labels for improved readability\ntotal_value = df_data[\"value\"].sum()\n\n\ndef make_label(row):\n    w, h = row[\"width\"], row[\"height\"]\n    pct = row[\"value\"] / total_value * 100\n    # Large rectangles: show both category and subcategory with percentage\n    if w > 20 and h > 12:\n        return f\"{row['category']}\\n{row['subcategory']}\\n{pct:.0f}%\"\n    # Medium rectangles: subcategory and percentage\n    elif w > 12 and h > 8:\n        return f\"{row['subcategory']}\\n{pct:.0f}%\"\n    # Small rectangles: just subcategory\n    elif w > 8 and h > 6:\n        return f\"{row['subcategory']}\"\n    # Very small: no label (visible in legend)\n    return \"\"\n\n\nrect_df[\"label\"] = rect_df.apply(make_label, axis=1)\n\n# Map categories to Okabe-Ito colors\nunique_categories = df_data[\"category\"].unique().tolist()\ncategory_colors = {cat: IMPRINT[i % len(IMPRINT)] for i, cat in enumerate(unique_categories)}\ncolor_values = [category_colors[cat] for cat in unique_categories]\n\n# Per-swatch label color: pick whichever of near-black/near-white ink gives\n# the higher WCAG contrast against that category's fill, so labels stay\n# legible on both light swatches (e.g. lavender) and dark ones (e.g. red).\nDARK_INK = \"#1A1A17\"\nLIGHT_INK = \"#F0EFE8\"\n\n\ndef relative_luminance(hex_color):\n    r, g, b = (int(hex_color.lstrip(\"#\")[i : i + 2], 16) / 255 for i in (0, 2, 4))\n\n    def channel(c):\n        return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4\n\n    r, g, b = channel(r), channel(g), channel(b)\n    return 0.2126 * r + 0.7152 * g + 0.0722 * b\n\n\ndef contrast_ratio(lum_a, lum_b):\n    lighter, darker = max(lum_a, lum_b), min(lum_a, lum_b)\n    return (lighter + 0.05) / (darker + 0.05)\n\n\ndef best_label_color(bg_hex):\n    bg_lum = relative_luminance(bg_hex)\n    dark_contrast = contrast_ratio(bg_lum, relative_luminance(DARK_INK))\n    light_contrast = contrast_ratio(bg_lum, relative_luminance(LIGHT_INK))\n    return DARK_INK if dark_contrast >= light_contrast else LIGHT_INK\n\n\nlabel_colors = {cat: best_label_color(color) for cat, color in category_colors.items()}\nrect_df[\"label_color\"] = rect_df[\"category\"].map(label_colors)\nTEXT_SIZE = 7\n\n# Create the plot\nplot = (\n    ggplot(rect_df)\n    + geom_rect(\n        aes(xmin=\"xmin\", ymin=\"ymin\", xmax=\"xmax\", ymax=\"ymax\", fill=\"category\", alpha=\"shade_alpha\"),\n        color=INK_SOFT,\n        size=0.7,\n    )\n    + geom_text(aes(x=\"label_x\", y=\"label_y\", label=\"label\", color=\"label_color\"), size=TEXT_SIZE, fontface=\"bold\")\n    + scale_fill_manual(values=color_values)\n    + scale_color_identity()\n    + scale_alpha_identity()\n    + labs(title=\"Budget Breakdown · treemap-basic · python · letsplot · anyplot.ai\", fill=\"Department\")\n    + theme_void()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_title=element_text(size=16, color=INK, hjust=0.5),\n        legend_title=element_text(size=12, color=INK),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_position=\"right\",\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n    )\n    + ggsize(800, 450)\n)\n\n# Save outputs with theme suffix\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}