{"spec_id":"bubble-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nbubble-basic: Basic Bubble Chart\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 87/100 | Created: 2026-05-29\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\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\"\n\nANYPLOT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nstage_colors = ANYPLOT_PALETTE[:4]\n\n# Data — tech startup metrics: funding vs revenue, sized by employees, colored by stage\nnp.random.seed(42)\nn = 49\n\nstages = np.random.choice([\"Seed\", \"Series A\", \"Series B\", \"Growth\"], size=n, p=[0.25, 0.30, 0.25, 0.20])\n\nstage_funding = {\"Seed\": (6, 4), \"Series A\": (18, 7), \"Series B\": (38, 10), \"Growth\": (60, 12)}\nfunding_m = np.array([np.random.normal(*stage_funding[s]) for s in stages])\nfunding_m = np.clip(funding_m, 1, 80)\n\nrevenue_m = funding_m * np.random.uniform(0.7, 1.5, size=n) + np.random.normal(5, 3, size=n)\nrevenue_m = np.clip(revenue_m, 2, 100)\n\nstage_emp = {\"Seed\": (25, 10), \"Series A\": (80, 35), \"Series B\": (250, 90), \"Growth\": (550, 150)}\nemployees = np.array([int(np.clip(np.random.normal(*stage_emp[s]), 15, 900)) for s in stages])\n\ndf = pd.DataFrame(\n    {\n        \"Funding ($M)\": np.round(funding_m, 1),\n        \"Revenue ($M)\": np.round(revenue_m, 1),\n        \"Employees\": employees,\n        \"Stage\": pd.Categorical(stages, categories=[\"Seed\", \"Series A\", \"Series B\", \"Growth\"], ordered=True),\n    }\n)\n\n# Add outlier: high-funded low-revenue startup to demonstrate full chart dynamics\noutlier = pd.DataFrame(\n    {\n        \"Funding ($M)\": [68.5],\n        \"Revenue ($M)\": [7.2],\n        \"Employees\": [380],\n        \"Stage\": pd.Categorical([\"Series B\"], categories=[\"Seed\", \"Series A\", \"Series B\", \"Growth\"], ordered=True),\n    }\n)\ndf = pd.concat([df, outlier], ignore_index=True)\n\n# Flag top-3 companies by revenue for storytelling annotations\ntop3_idx = df[\"Revenue ($M)\"].nlargest(3).index.tolist()\ndf[\"label\"] = \"\"\nfor i in top3_idx:\n    df.loc[i, \"label\"] = f\"{df.loc[i, 'Stage']} · ${df.loc[i, 'Revenue ($M)']}M\"\n\ntitle = \"bubble-basic · python · altair · anyplot.ai\"\n\n# Plot — bubble layer\nbubbles = (\n    alt.Chart(df)\n    .mark_circle(stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(\n        x=alt.X(\n            \"Funding ($M):Q\", scale=alt.Scale(domain=[0, 85], nice=False), axis=alt.Axis(domainWidth=0, tickSize=6)\n        ),\n        y=alt.Y(\n            \"Revenue ($M):Q\", scale=alt.Scale(domain=[0, 110], nice=False), axis=alt.Axis(domainWidth=0, tickSize=6)\n        ),\n        size=alt.Size(\n            \"Employees:Q\",\n            scale=alt.Scale(range=[50, 2000], domain=[15, 900]),\n            legend=alt.Legend(\n                title=\"Employees\",\n                titleFontSize=10,\n                labelFontSize=10,\n                values=[50, 200, 500, 900],\n                symbolFillColor=ANYPLOT_PALETTE[0],\n                symbolStrokeColor=PAGE_BG,\n                symbolOpacity=0.65,\n                direction=\"vertical\",\n            ),\n        ),\n        color=alt.Color(\n            \"Stage:N\",\n            scale=alt.Scale(domain=[\"Seed\", \"Series A\", \"Series B\", \"Growth\"], range=stage_colors),\n            legend=alt.Legend(\n                title=\"Stage\",\n                titleFontSize=10,\n                labelFontSize=10,\n                symbolType=\"circle\",\n                symbolSize=200,\n                symbolStrokeWidth=0,\n                symbolOpacity=0.65,\n            ),\n        ),\n        opacity=alt.condition(alt.datum.label != \"\", alt.value(0.9), alt.value(0.6)),\n        tooltip=[\"Stage:N\", \"Funding ($M):Q\", \"Revenue ($M):Q\", \"Employees:Q\"],\n    )\n)\n\n# Annotation layers — sort by revenue descending and alternate dy to prevent collision\n_labeled = df[df[\"label\"] != \"\"].sort_values(\"Revenue ($M)\", ascending=False).reset_index(drop=True)\n_dy_offsets = [-15, 12, -15]\n_annotation_layers = [\n    alt.Chart(_labeled.iloc[[k]])\n    .mark_text(align=\"right\", dx=-10, dy=_dy_offsets[k], fontSize=10, fontWeight=\"bold\")\n    .encode(x=\"Funding ($M):Q\", y=\"Revenue ($M):Q\", text=\"label:N\", color=alt.value(INK))\n    for k in range(len(_labeled))\n]\nannotations = alt.layer(*_annotation_layers)\n\nchart = (\n    (bubbles + annotations)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        padding={\"left\": 0, \"right\": 0, \"top\": 0, \"bottom\": 0},\n        title=alt.Title(\n            title,\n            fontSize=16,\n            fontWeight=\"bold\",\n            color=INK,\n            anchor=\"middle\",\n            subtitle=\"Tech Startup Metrics — Funding vs Revenue by Stage & Team Size\",\n            subtitleFontSize=12,\n            subtitleColor=INK_SOFT,\n            subtitlePadding=4,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0, continuousWidth=620, continuousHeight=320)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.15,\n        gridDash=[3, 3],\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=12,\n    )\n    .configure_legend(\n        fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK, orient=\"right\", padding=10\n    )\n)\n\n# Save PNG\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# PAD to exact 3200×1800 (do not crop — cropping clips title/axis labels)\nTW, TH = 3200, 1800\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\n# Save HTML\nchart.save(f\"plot-{THEME}.html\")\n"}