{"spec_id":"bubble-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nbubble-basic: Basic Bubble Chart\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-28\n\"\"\"\n\nimport os\n\nimport matplotlib.patheffects as pe\nimport matplotlib.pyplot as plt\nimport numpy as np\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nANYPLOT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — tech company metrics: revenue vs growth with market cap as bubble size\nnp.random.seed(42)\nn_base = 35\n\nrevenue_base = np.random.uniform(5, 120, n_base)\ngrowth_base = 0.4 * (100 - revenue_base) / 100 + np.random.randn(n_base) * 0.08 + 0.05\ngrowth_base = np.clip(growth_base, -0.10, 0.55)\ncap_base = revenue_base * (1 + growth_base * 3) * np.random.uniform(0.6, 1.8, n_base)\ncap_base = np.clip(cap_base, 5, 400)\n\noutlier_revenue = np.array([18, 12, 118, 55, 85, 95])\noutlier_growth = np.array([0.48, 0.44, 0.02, 0.30, -0.05, -0.08])\noutlier_cap = np.array([280, 220, 380, 260, 150, 90])\n\nrevenue = np.concatenate([revenue_base, outlier_revenue])\ngrowth_rate = np.concatenate([growth_base, outlier_growth])\nmarket_cap = np.concatenate([cap_base, outlier_cap])\n\nsectors = np.array(\n    [\"Cloud/SaaS\"] * 12\n    + [\"E-Commerce\"] * 8\n    + [\"Semiconductors\"] * 8\n    + [\"Social Media\"] * 7\n    + [\"Cloud/SaaS\", \"Cloud/SaaS\", \"Semiconductors\", \"E-Commerce\", \"Semiconductors\", \"E-Commerce\"]\n)\nsector_names = [\"Cloud/SaaS\", \"E-Commerce\", \"Semiconductors\", \"Social Media\"]\nsector_colors = ANYPLOT_PALETTE[:4]\n\n# Scale bubble sizes by area for accurate visual perception (tuned for 3200×1800 canvas)\nsize_scaled = (market_cap / market_cap.max()) * 580 + 30\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\nfor sector, color in zip(sector_names, sector_colors, strict=True):\n    mask = sectors == sector\n    ax.scatter(\n        revenue[mask],\n        growth_rate[mask] * 100,\n        s=size_scaled[mask],\n        alpha=0.65,\n        color=color,\n        edgecolors=PAGE_BG,\n        linewidths=0.8,\n        label=sector,\n        zorder=3,\n    )\n\n# Annotate notable outliers to guide the viewer\nannotations = [\n    (n_base, \"High-Growth\\nUnicorn\", (-50, 22)),\n    (n_base + 2, \"Market\\nLeader\", (-80, 65)),\n    (n_base + 3, \"Breakout\\nPerformer\", (55, 25)),\n]\nfor idx, label, offset in annotations:\n    ax.annotate(\n        label,\n        (revenue[idx], growth_rate[idx] * 100),\n        fontsize=8,\n        fontweight=\"bold\",\n        color=INK,\n        ha=\"center\",\n        va=\"bottom\",\n        xytext=offset,\n        textcoords=\"offset points\",\n        arrowprops={\"arrowstyle\": \"->\", \"color\": INK_SOFT, \"lw\": 1.0, \"connectionstyle\": \"arc3,rad=0.2\"},\n        path_effects=[pe.withStroke(linewidth=2.5, foreground=PAGE_BG)],\n    )\n\n# Size legend — lower left to avoid data overlap\nlegend_caps = [25, 100, 300]\nlegend_handles = [\n    ax.scatter([], [], s=(v / market_cap.max()) * 580 + 30, c=INK_MUTED, alpha=0.5, edgecolors=PAGE_BG, linewidths=0.8)\n    for v in legend_caps\n]\nsize_legend = ax.legend(\n    legend_handles,\n    [f\"${v}B\" for v in legend_caps],\n    title=\"Market Cap\",\n    title_fontsize=8,\n    fontsize=8,\n    loc=\"lower left\",\n    framealpha=0.95,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n    scatterpoints=1,\n    labelspacing=1.4,\n    borderpad=0.9,\n)\nplt.setp(size_legend.get_title(), color=INK_SOFT)\nplt.setp(size_legend.get_texts(), color=INK_SOFT)\nax.add_artist(size_legend)\n\n# Sector color legend — upper right\nsector_legend = ax.legend(\n    fontsize=8,\n    loc=\"upper right\",\n    framealpha=0.95,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n    title=\"Sector\",\n    title_fontsize=8,\n    markerscale=0.7,\n    handletextpad=0.5,\n    borderpad=0.7,\n)\nplt.setp(sector_legend.get_title(), color=INK_SOFT)\nplt.setp(sector_legend.get_texts(), color=INK_SOFT)\n\n# Style\ntitle = \"bubble-basic · python · matplotlib · anyplot.ai\"\nax.set_xlabel(\"Annual Revenue ($B)\", fontsize=10, color=INK, labelpad=8)\nax.set_ylabel(\"Revenue Growth Rate (%)\", fontsize=10, color=INK, labelpad=8)\nax.set_title(title, fontsize=12, fontweight=\"medium\", color=INK, pad=12)\nax.tick_params(axis=\"both\", labelsize=8, labelcolor=INK_SOFT, length=0)\n\nfor spine in ax.spines.values():\n    spine.set_visible(False)\n\nax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK, zorder=0)\nax.xaxis.grid(False)\nax.axhline(y=0, color=INK_SOFT, linewidth=0.8, zorder=1, alpha=0.5)\n\nax.set_xlim(-5, 140)\nax.set_ylim(-15, 60)\n\nfig.subplots_adjust(left=0.10, right=0.97, top=0.92, bottom=0.12)\n\n# Save\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}