{"spec_id":"bubble-packed","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nbubble-packed: Basic Packed Bubble Chart\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive chrome tokens\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — canonical order; first series always #009E73\nGROUP_NAMES = [\"Technology\", \"Revenue\", \"Operations\", \"Corporate\"]\nIMPRINT_4 = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\nGROUP_COLORS = dict(zip(GROUP_NAMES, IMPRINT_4, strict=True))\n\n# Data — department budgets with functional groupings\ndepartments = [\n    (\"Engineering\", 4500000, \"Technology\"),\n    (\"R&D\", 3800000, \"Technology\"),\n    (\"IT\", 2100000, \"Technology\"),\n    (\"Data Science\", 1650000, \"Technology\"),\n    (\"QA\", 880000, \"Technology\"),\n    (\"Sales\", 3200000, \"Revenue\"),\n    (\"Marketing\", 2800000, \"Revenue\"),\n    (\"Operations\", 1800000, \"Operations\"),\n    (\"Finance\", 1200000, \"Operations\"),\n    (\"Support\", 1100000, \"Operations\"),\n    (\"Admin\", 450000, \"Operations\"),\n    (\"HR\", 950000, \"Corporate\"),\n    (\"Legal\", 650000, \"Corporate\"),\n    (\"Product\", 1500000, \"Corporate\"),\n    (\"Design\", 720000, \"Corporate\"),\n]\n\nlabels = [d[0] for d in departments]\nvalues = np.array([d[1] for d in departments])\ngroups = [d[2] for d in departments]\nn = len(labels)\n\n# Scale radii by area (sqrt) for accurate visual perception\nradii = np.sqrt(values / values.max()) * 110\n\n# Circle packing via force simulation\nnp.random.seed(42)\nangles = np.linspace(0, 2 * np.pi, n, endpoint=False)\nx_pos = np.cos(angles) * 150 + np.random.randn(n) * 30\ny_pos = np.sin(angles) * 150 + np.random.randn(n) * 30\n\nfor _ in range(600):\n    for i in range(n):\n        fx, fy = -x_pos[i] * 0.01, -y_pos[i] * 0.01\n        for j in range(n):\n            if i != j:\n                dx = x_pos[i] - x_pos[j]\n                dy = y_pos[i] - y_pos[j]\n                dist = np.sqrt(dx**2 + dy**2) + 0.1\n                min_dist = radii[i] + radii[j] + 4\n                if dist < min_dist:\n                    force = (min_dist - dist) * 0.3\n                    fx += (dx / dist) * force\n                    fy += (dy / dist) * force\n        x_pos[i] += fx\n        y_pos[i] += fy\n\n# Unweighted mean centering for symmetric empty-space distribution\nx_pos -= np.mean(x_pos)\ny_pos -= np.mean(y_pos)\n\n# Format values for display\nformatted = [f\"${v / 1e6:.1f}M\" if v >= 1e6 else f\"${v / 1e3:.0f}K\" for v in values]\nshares = [f\"{v / values.sum() * 100:.1f}\" for v in values]\ntotal = f\"${values.sum() / 1e6:.1f}M\"\n\n# Tight axis ranges with padding\npad = 15\nx_lo = (x_pos - radii).min() - pad\nx_hi = (x_pos + radii).max() + pad\ny_lo = (y_pos - radii).min() - pad\ny_hi = (y_pos + radii).max() + pad\n\n# Canvas: width=800, height=450, scale=4 → 3200×1800 output (landscape hard target)\nfig_w, fig_h = 800, 450\nm_l, m_r, m_t, m_b = 80, 40, 80, 80\nplot_w, plot_h = fig_w - m_l - m_r, fig_h - m_t - m_b\n\n# Convert data-coordinate radii to plotly pixel diameters (scaleanchor constrains min axis)\npx_per_unit = min(plot_w / (x_hi - x_lo), plot_h / (y_hi - y_lo))\nmarker_diameters = 2 * radii * px_per_unit\n\n# Luminance-based text contrast for annotations inside bubbles\ntext_colors = []\nfor g in groups:\n    c = GROUP_COLORS[g]\n    lum = 0.299 * int(c[1:3], 16) + 0.587 * int(c[3:5], 16) + 0.114 * int(c[5:7], 16)\n    # Near-white / near-black constants contrast against bubble fills in both themes\n    text_colors.append(\"#F0EFE8\" if lum < 160 else \"#1A1A17\")\n\n# Build figure — one trace per group for idiomatic Plotly legend\nfig = go.Figure()\n\nfor group_name in GROUP_NAMES:\n    color = GROUP_COLORS[group_name]\n    idx = np.array([i for i in range(n) if groups[i] == group_name])\n    fig.add_trace(\n        go.Scatter(\n            x=x_pos[idx],\n            y=y_pos[idx],\n            mode=\"markers\",\n            name=group_name,\n            marker={\n                \"size\": list(marker_diameters[idx]),\n                \"sizemode\": \"diameter\",\n                \"color\": color,\n                \"opacity\": 0.9,\n                \"line\": {\"color\": PAGE_BG, \"width\": 2},\n            },\n            text=[labels[i] for i in idx],\n            customdata=[[formatted[i], shares[i]] for i in idx],\n            hovertemplate=\"<b>%{text}</b> (%{fullData.name})<br>Budget: %{customdata[0]}<br>Share: %{customdata[1]}%<extra></extra>\",\n        )\n    )\n\n# Text labels inside bubbles — proportional to marker diameter\nfor i in range(n):\n    d = marker_diameters[i]\n    font_size = max(9, min(12, int(d * 0.15)))\n    label_text = (\n        f\"<b>{labels[i]}</b><br>{formatted[i]}\"\n        if d > 60 and len(labels[i]) <= 9\n        else f\"<b>{labels[i]}</b>\"\n        if d > 30\n        else \"\"\n    )\n    fig.add_annotation(\n        x=x_pos[i],\n        y=y_pos[i],\n        text=label_text,\n        showarrow=False,\n        font={\"size\": font_size, \"color\": text_colors[i], \"family\": \"Arial\"},\n    )\n\n# Title font size scaled for length: round(16 × 67 / len(title))\ntitle_text = \"Department Budget Allocation · bubble-packed · python · plotly · anyplot.ai\"\ntitle_fontsize = round(16 * 67 / len(title_text))\n\nfig.update_layout(\n    autosize=False,\n    title={\"text\": title_text, \"font\": {\"size\": title_fontsize, \"color\": INK}, \"x\": 0.5, \"xanchor\": \"center\"},\n    xaxis={\"showgrid\": False, \"zeroline\": False, \"showticklabels\": False, \"title\": \"\", \"range\": [x_lo, x_hi]},\n    yaxis={\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"showticklabels\": False,\n        \"title\": \"\",\n        \"scaleanchor\": \"x\",\n        \"scaleratio\": 1,\n        \"range\": [y_lo, y_hi],\n    },\n    template=\"plotly_white\",\n    legend={\n        \"font\": {\"size\": 10, \"family\": \"Arial\", \"color\": INK_SOFT},\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"orientation\": \"h\",\n        \"yanchor\": \"top\",\n        \"y\": -0.05,\n        \"xanchor\": \"center\",\n        \"x\": 0.5,\n        \"itemsizing\": \"constant\",\n    },\n    margin={\"l\": m_l, \"r\": m_r, \"t\": m_t, \"b\": m_b},\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n)\n\n# Total budget note (bottom-right, within bottom margin)\nfig.add_annotation(\n    text=f\"Total: {total}\",\n    xref=\"paper\",\n    yref=\"paper\",\n    x=0.98,\n    y=-0.04,\n    xanchor=\"right\",\n    showarrow=False,\n    font={\"size\": 10, \"color\": INK_MUTED, \"family\": \"Arial\"},\n)\n\n# Storytelling callouts — guide viewer to key budget insight\neng_idx = labels.index(\"Engineering\")\nrd_idx = labels.index(\"R&D\")\ntech_total = sum(v for _, v, g in departments if g == \"Technology\")\ntech_share = tech_total / values.sum() * 100\n\nfig.add_annotation(\n    x=x_pos[eng_idx],\n    y=y_pos[eng_idx],\n    text=f\"<b>Largest dept</b><br>${values[eng_idx] / 1e6:.1f}M — {values[eng_idx] / values.sum() * 100:.1f}% of total\",\n    showarrow=True,\n    arrowhead=2,\n    arrowwidth=1.5,\n    arrowcolor=INK_SOFT,\n    axref=\"pixel\",\n    ayref=\"pixel\",\n    ax=0,\n    ay=-80,\n    font={\"size\": 9, \"color\": INK, \"family\": \"Arial\"},\n    bgcolor=ELEVATED_BG,\n    bordercolor=INK_SOFT,\n    borderwidth=1,\n    borderpad=4,\n    align=\"center\",\n)\nfig.add_annotation(\n    x=x_pos[rd_idx],\n    y=y_pos[rd_idx],\n    text=f\"<b>Tech group</b>: {tech_share:.0f}% of budget<br>leads all four divisions\",\n    showarrow=True,\n    arrowhead=2,\n    arrowwidth=1.5,\n    arrowcolor=INK_SOFT,\n    axref=\"pixel\",\n    ayref=\"pixel\",\n    ax=70,\n    ay=-60,\n    font={\"size\": 9, \"color\": INK, \"family\": \"Arial\"},\n    bgcolor=ELEVATED_BG,\n    bordercolor=INK_SOFT,\n    borderwidth=1,\n    borderpad=4,\n    align=\"center\",\n)\n\n# Save — landscape 3200×1800 (width=800, height=450, scale=4)\nfig.write_image(f\"plot-{THEME}.png\", width=fig_w, height=fig_h, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\", full_html=True)\n"}