{"spec_id":"bubble-packed","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nbubble-packed: Basic Packed Bubble Chart\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-30\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\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\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — company market cap by sector (billions USD)\nsectors = {\n    \"Technology\": [(\"Apple\", 180), (\"Microsoft\", 160), (\"Google\", 120), (\"NVIDIA\", 95), (\"Meta\", 75)],\n    \"Finance\": [(\"JPMorgan\", 85), (\"Visa\", 70), (\"Mastercard\", 55), (\"Goldman Sachs\", 45)],\n    \"Healthcare\": [(\"UnitedHealth\", 90), (\"J&J\", 65), (\"Merck\", 50), (\"Pfizer\", 40)],\n    \"Retail\": [(\"Amazon\", 140), (\"Walmart\", 60), (\"Costco\", 45), (\"Target\", 30)],\n}\n\nrecords = []\nfor sector, companies in sectors.items():\n    for name, value in companies:\n        records.append({\"name\": name, \"value\": value, \"sector\": sector, \"radius\": np.sqrt(value) * 4})\n\ndf = pd.DataFrame(records).sort_values(\"radius\", ascending=False).reset_index(drop=True)\n\n# Circle packing — greedy closest-to-center placement without overlap\nplaced_x, placed_y, placed_r = [], [], []\n\nfor _, row in df.iterrows():\n    r = row[\"radius\"]\n\n    if not placed_x:\n        placed_x.append(0.0)\n        placed_y.append(0.0)\n        placed_r.append(r)\n        continue\n\n    best_pos, best_dist = None, float(\"inf\")\n    px_arr = np.array(placed_x)\n    py_arr = np.array(placed_y)\n    pr_arr = np.array(placed_r)\n\n    for i in range(len(placed_x)):\n        for angle in np.linspace(0, 2 * np.pi, 72, endpoint=False):\n            gap = placed_r[i] + r + 2\n            tx = placed_x[i] + gap * np.cos(angle)\n            ty = placed_y[i] + gap * np.sin(angle)\n\n            dists = np.sqrt((px_arr - tx) ** 2 + (py_arr - ty) ** 2)\n            if np.all(dists >= pr_arr + r + 1):\n                cdist = np.sqrt(tx**2 + ty**2)\n                if cdist < best_dist:\n                    best_dist = cdist\n                    best_pos = (tx, ty)\n\n    bx, by = best_pos if best_pos else (0.0, 0.0)\n    placed_x.append(bx)\n    placed_y.append(by)\n    placed_r.append(r)\n\ndf[\"x\"] = placed_x\ndf[\"y\"] = placed_y\n\n# Recenter into positive coordinate space with padding\npad = 20\ndf[\"x\"] = df[\"x\"] - (df[\"x\"] - df[\"radius\"]).min() + pad\ndf[\"y\"] = df[\"y\"] - (df[\"y\"] - df[\"radius\"]).min() + pad\nplot_w = (df[\"x\"] + df[\"radius\"]).max() + pad\nplot_h = (df[\"y\"] + df[\"radius\"]).max() + pad\nmax_dim = max(plot_w, plot_h)\n\n# Sector color mapping — Imprint palette positions 1–4\nsector_order = list(sectors.keys())\nsector_colors = dict(zip(sector_order, IMPRINT_PALETTE[:4], strict=True))\n\n# Apply seaborn theme with adaptive chrome tokens\nsns.set_theme(\n    style=\"white\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"text.color\": INK,\n        \"axes.labelcolor\": INK,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Square canvas — packed bubble charts are symmetric, no preferred horizontal axis\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400)\nfig.patch.set_facecolor(PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\nax.set_xlim(0, max_dim)\nax.set_ylim(0, max_dim)\nax.set_aspect(\"equal\")\n\n# Convert data-unit radii to scatter marker sizes (points²) for pixel-accurate circles\nfig.canvas.draw()\npx_per_unit = ax.transData.transform((1, 0))[0] - ax.transData.transform((0, 0))[0]\npts_per_unit = px_per_unit * 72 / fig.dpi\ndf[\"marker_size\"] = (df[\"radius\"] * 2 * pts_per_unit) ** 2\n\n# Categorical ordering for consistent palette mapping\ndf[\"sector\"] = pd.Categorical(df[\"sector\"], categories=sector_order, ordered=True)\n\n# Draw bubbles with seaborn scatterplot\nsns.scatterplot(\n    data=df,\n    x=\"x\",\n    y=\"y\",\n    hue=\"sector\",\n    size=\"marker_size\",\n    sizes=(df[\"marker_size\"].min(), df[\"marker_size\"].max()),\n    hue_order=sector_order,\n    palette=sector_colors,\n    alpha=0.90,\n    edgecolor=INK,\n    linewidth=0.5,\n    legend=True,\n    ax=ax,\n)\n\n# Accent the largest bubble per sector with a thicker edge ring to highlight the dominant story\nsector_top_idx = df.groupby(\"sector\")[\"value\"].idxmax()\naccent = df.loc[sector_top_idx]\nax.scatter(accent[\"x\"], accent[\"y\"], s=accent[\"marker_size\"], facecolor=\"none\", edgecolor=INK, linewidth=2.5, zorder=3)\n\n# Labels inside bubbles — font sizes scaled for 2400×2400 canvas (dpi=400)\nfor _, row in df.iterrows():\n    r = row[\"radius\"]\n    name = row[\"name\"]\n    value = row[\"value\"]\n\n    if r > 38:\n        fs_name, max_chars, show_val = 10, 12, True\n    elif r > 28:\n        fs_name, max_chars, show_val = 8, 12, True\n    elif r > 22:\n        fs_name, max_chars, show_val = 6, 10, False\n    else:\n        fs_name, max_chars, show_val = 5, 8, False\n\n    display_name = name if len(name) <= max_chars else name[: max_chars - 1] + \".\"\n\n    if show_val:\n        y_off = r * 0.13\n        ax.text(\n            row[\"x\"],\n            row[\"y\"] + y_off,\n            display_name,\n            ha=\"center\",\n            va=\"center\",\n            fontsize=fs_name,\n            fontweight=\"bold\",\n            color=\"white\",\n        )\n        ax.text(\n            row[\"x\"],\n            row[\"y\"] - y_off * 2,\n            f\"${value}B\",\n            ha=\"center\",\n            va=\"center\",\n            fontsize=fs_name - 2,\n            color=\"white\",\n            alpha=0.85,\n        )\n    else:\n        ax.text(\n            row[\"x\"],\n            row[\"y\"],\n            display_name,\n            ha=\"center\",\n            va=\"center\",\n            fontsize=fs_name,\n            fontweight=\"bold\",\n            color=\"white\",\n        )\n\nax.axis(\"off\")\n\n# Title\nax.set_title(\n    \"Market Capitalization by Sector\\nbubble-packed · python · seaborn · anyplot.ai\",\n    fontsize=12,\n    fontweight=\"medium\",\n    pad=14,\n    color=INK,\n    linespacing=1.4,\n)\n\n# Filter seaborn-managed legend to hue (sector) entries, then reposition with sns.move_legend\nleg_auto = ax.get_legend()\nall_handles = leg_auto.legend_handles\nall_labels = [t.get_text() for t in leg_auto.get_texts()]\nsector_handles = [h for h, lbl in zip(all_handles, all_labels, strict=False) if lbl in sector_order]\nsector_labels = [lbl for lbl in all_labels if lbl in sector_order]\nleg_auto.remove()\nax.legend(sector_handles, sector_labels)\nsns.move_legend(\n    ax,\n    \"lower center\",\n    bbox_to_anchor=(0.5, -0.06),\n    ncol=4,\n    title=\"Sector\",\n    title_fontsize=9,\n    fontsize=8,\n    framealpha=0.95,\n)\nleg = ax.get_legend()\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nleg.get_title().set_color(INK)\nfor text in leg.get_texts():\n    text.set_color(INK_SOFT)\n\n# Reserve vertical margins for title (top) and legend (bottom)\nfig.subplots_adjust(top=0.88, bottom=0.10)\n\n# bbox_inches must stay default (None) — see prompts/library/seaborn.md \"Canvas\"\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}