{"spec_id":"bubble-packed","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nbubble-packed: Basic Packed Bubble Chart\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport circlify\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\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 — 8 hues, canonical order (hybrid-v3 sort)\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\nnp.random.seed(42)\n\n# Data — department budget allocation by division ($K)\nlabels = [\n    \"Engineering\",\n    \"R&D\",\n    \"Data Science\",\n    \"QA\",\n    \"Marketing\",\n    \"Sales\",\n    \"Support\",\n    \"Finance\",\n    \"HR\",\n    \"Legal\",\n    \"Operations\",\n    \"IT\",\n    \"Security\",\n    \"Design\",\n    \"Product\",\n]\nvalues = [850, 750, 460, 195, 420, 680, 210, 290, 180, 150, 320, 380, 170, 240, 550]\ngroups = [\"Technology\"] * 4 + [\"Revenue\"] * 3 + [\"Corporate\"] * 3 + [\"Operations\"] * 3 + [\"Product\"] * 2\nn = len(labels)\n\n# Circle packing layout via circlify (returns circles in ascending value order)\ncircles = circlify.circlify(values, show_enclosure=False)\nidx_asc = np.argsort(values)\nscale = 300\n\nx_coords = np.zeros(n)\ny_coords = np.zeros(n)\nradii = np.zeros(n)\nfor ci, oi in zip(circles, idx_asc, strict=True):\n    x_coords[oi] = ci.x * scale\n    y_coords[oi] = ci.y * scale\n    radii[oi] = ci.r * scale\n\ngroup_order = [\"Technology\", \"Revenue\", \"Operations\", \"Corporate\", \"Product\"]\ngroup_colors = IMPRINT_PALETTE[:5]\n\ndf = pd.DataFrame(\n    {\n        \"label\": labels,\n        \"value\": values,\n        \"group\": groups,\n        \"x\": x_coords,\n        \"y\": y_coords,\n        \"radius\": radii,\n        \"budget\": [f\"${v}K\" for v in values],\n    }\n)\n\n# Interactive legend selection — click a division to highlight it\nselection = alt.selection_point(fields=[\"group\"], bind=\"legend\")\n\nr_min, r_max = radii.min(), radii.max()\nradius_threshold = r_min + (r_max - r_min) * 0.25\n\ncircles_layer = (\n    alt.Chart(df)\n    .mark_circle(stroke=PAGE_BG, strokeWidth=2.5)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None, scale=alt.Scale(padding=r_max * 0.6)),\n        y=alt.Y(\"y:Q\", axis=None, scale=alt.Scale(padding=r_max * 0.6)),\n        size=alt.Size(\"radius:Q\", scale=alt.Scale(range=[r_min**2 * 1.6, r_max**2 * 1.6]), legend=None),\n        color=alt.Color(\n            \"group:N\",\n            scale=alt.Scale(domain=group_order, range=group_colors),\n            legend=alt.Legend(\n                title=\"Division\",\n                titleFontSize=11,\n                titleFontWeight=\"bold\",\n                labelFontSize=10,\n                symbolSize=200,\n                orient=\"right\",\n            ),\n        ),\n        opacity=alt.condition(selection, alt.value(0.9), alt.value(0.15)),\n        tooltip=[\n            alt.Tooltip(\"label:N\", title=\"Department\"),\n            alt.Tooltip(\"budget:N\", title=\"Budget\"),\n            alt.Tooltip(\"group:N\", title=\"Division\"),\n        ],\n    )\n    .add_params(selection)\n)\n\n# Labels for large bubbles: two-line department name + budget\ndf_large = df[df[\"radius\"] >= radius_threshold].copy()\ndf_large[\"display_text\"] = df_large[\"label\"] + \"\\n\" + df_large[\"budget\"]\n\nlarge_labels = (\n    alt.Chart(df_large)\n    .mark_text(fontWeight=\"bold\", fontSize=16, lineBreak=\"\\n\")\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        text=\"display_text:N\",\n        color=alt.value(\"#FFFFFF\"),\n        opacity=alt.condition(selection, alt.value(1.0), alt.value(0.1)),\n    )\n)\n\n# Labels for smaller bubbles: department name only\ndf_small = df[df[\"radius\"] < radius_threshold].copy()\n\nsmall_labels = (\n    alt.Chart(df_small)\n    .mark_text(fontWeight=\"bold\", fontSize=12)\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        text=\"label:N\",\n        color=alt.value(\"#FFFFFF\"),\n        opacity=alt.condition(selection, alt.value(1.0), alt.value(0.1)),\n    )\n)\n\n# Title with length-scaled font size (67-char baseline → 16px default)\ntitle_str = \"Department Budget Allocation · bubble-packed · python · altair · anyplot.ai\"\nn_chars = len(title_str)\ntitle_fs = max(11, round(16 * 67 / n_chars)) if n_chars > 67 else 16\nsubtitle_fs = max(8, round(title_fs * 0.85))\n\nchart = (\n    alt.layer(circles_layer, large_labels, small_labels)\n    .properties(\n        width=450,\n        height=460,\n        background=PAGE_BG,\n        padding={\"left\": 10, \"right\": 10, \"top\": 10, \"bottom\": 10},\n        title=alt.Title(\n            title_str,\n            subtitle=\"Technology division leads at 39% of total budget — Engineering alone at $850K\",\n            fontSize=title_fs,\n            subtitleFontSize=subtitle_fs,\n            subtitleColor=INK_SOFT,\n            fontWeight=\"bold\",\n            anchor=\"middle\",\n            color=INK,\n        ),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad to exact 2400×2400 square canvas (Step 0 contract)\nTW, TH = 2400, 2400\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}×{_h}, exceeds target {TW}×{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\nchart.save(f\"plot-{THEME}.html\")\n"}