{"spec_id":"bubble-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nbubble-basic: Basic Bubble Chart\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 85/100 | Created: 2026-05-28\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove the script's own directory from sys.path so `import pygal` resolves to the\n# installed package rather than this file (which shares the package name).\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _here]\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Data — City comparison: population density vs avg commute time, bubble = green space (%)\nnp.random.seed(42)\nn_cities = 50\n\n# Uniform spread across density range to avoid lower-left clustering\npopulation_density = np.concatenate(\n    [np.random.uniform(1500, 5000, 15), np.random.uniform(5000, 9000, 20), np.random.uniform(9000, 14000, 15)]\n)\nnp.random.shuffle(population_density)\n\ncommute_time = 15 + population_density / 750 + np.random.normal(0, 2.8, n_cities)\ngreen_space_pct = np.clip(52 - population_density / 380 + np.random.normal(0, 9, n_cities), 5, 55)\n\n# Area-scaled bubble sizing via 8 tiers (pygal has no per-point size API)\ngs_min, gs_max = green_space_pct.min(), green_space_pct.max()\nbubble_norm = (green_space_pct - gs_min) / (gs_max - gs_min)\n\nn_tiers = 8\ntier_bins = np.clip(np.digitize(bubble_norm, np.linspace(0, 1, n_tiers + 1)[1:-1]), 0, n_tiers - 1)\n\n# sqrt scaling for perceptual area accuracy\ntier_sizes = [int(12 + 62 * ((t + 0.5) / n_tiers) ** 0.5) for t in range(n_tiers)]\n\n# anyplot imprint_seq: #009E73 (brand green) → #4467A3 (blue), 8 equidistant stops\ntier_colors = tuple(\n    \"#{:02X}{:02X}{:02X}\".format(\n        round(0x00 + (0x44 - 0x00) * i / (n_tiers - 1)),\n        round(0x9E + (0x67 - 0x9E) * i / (n_tiers - 1)),\n        round(0x73 + (0xA3 - 0x73) * i / (n_tiers - 1)),\n    )\n    for i in range(n_tiers)\n)\n\n# ANYPLOT_AMBER marks the focal city (9th color in the series cycle)\nANYPLOT_AMBER = \"#DDCC77\"\nstyle_colors = tier_colors + (ANYPLOT_AMBER,)\n\n# Tier labels serve as the size legend (green-space percentage ranges)\nbin_edges_pct = np.linspace(gs_min, gs_max, n_tiers + 1)\ntier_labels = [f\"{bin_edges_pct[t]:.0f}–{bin_edges_pct[t + 1]:.0f}% green\" for t in range(n_tiers)]\n\n# Focal city: highest commute time — visual anchor for data storytelling\nfocal_idx = int(np.argmax(commute_time))\nfocal_tier = int(tier_bins[focal_idx])\n\n# Group cities by green-space tier, excluding focal city\ntier_data = {t: [] for t in range(n_tiers)}\nfor i in range(n_cities):\n    if i == focal_idx:\n        continue\n    t = int(tier_bins[i])\n    tier_data[t].append(\n        {\n            \"value\": (round(float(population_density[i]), 1), round(float(commute_time[i]), 1)),\n            \"label\": (\n                f\"Density: {population_density[i]:,.0f}/km²  |  \"\n                f\"Commute: {commute_time[i]:.1f} min  |  \"\n                f\"Green space: {green_space_pct[i]:.0f}%\"\n            ),\n        }\n    )\n\nfocal_point_data = [\n    {\n        \"value\": (round(float(population_density[focal_idx]), 1), round(float(commute_time[focal_idx]), 1)),\n        \"label\": (\n            f\"★ Peak Congestion  |  \"\n            f\"Density: {population_density[focal_idx]:,.0f}/km²  |  \"\n            f\"Commute: {commute_time[focal_idx]:.1f} min  |  \"\n            f\"Green space: {green_space_pct[focal_idx]:.0f}%\"\n        ),\n    }\n]\n\n# Style — theme-adaptive, anyplot sizing\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=style_colors,\n    opacity=0.70,\n    opacity_hover=0.95,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    tooltip_font_size=36,\n    title_font_family=\"Helvetica Neue, Helvetica, Arial, sans-serif\",\n    label_font_family=\"Helvetica Neue, Helvetica, Arial, sans-serif\",\n    major_label_font_family=\"Helvetica Neue, Helvetica, Arial, sans-serif\",\n    legend_font_family=\"Helvetica Neue, Helvetica, Arial, sans-serif\",\n)\n\n# Plot\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"bubble-basic · python · pygal · anyplot.ai\",\n    x_title=\"Population Density (people/km²)\",\n    y_title=\"Avg Commute Time (min)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=4,\n    legend_box_size=36,\n    stroke=False,\n    dots_size=20,\n    show_x_guides=True,\n    show_y_guides=True,\n    x_value_formatter=lambda x: f\"{x:,.0f}\",\n    value_formatter=lambda x: f\"{x:.1f}\",\n    margin_top=80,\n    margin_bottom=200,\n    margin_left=100,\n    margin_right=80,\n    tooltip_border_radius=8,\n    tooltip_fancy_mode=True,\n    print_values=False,\n    truncate_legend=30,\n    spacing=30,\n)\n\nfor t in range(n_tiers):\n    chart.add(tier_labels[t], tier_data[t] if tier_data[t] else [], dots_size=tier_sizes[t])\n\n# Focal city rendered last in ANYPLOT_AMBER — stands out as a visual anchor\nchart.add(\"★ Peak Congestion\", focal_point_data, dots_size=tier_sizes[focal_tier] + 8)\n\n# Save\nchart.render_to_file(f\"plot-{THEME}.html\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}