{"spec_id":"bubble-packed","library":"makie","language":"julia","code":"# anyplot.ai\n# bubble-packed: Basic Packed Bubble Chart\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 88/100 | Created: 2026-05-29\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# Theme tokens\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst ELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\",  # 1 — brand green   → Africa\n    colorant\"#C475FD\",  # 2 — lavender       → Americas\n    colorant\"#4467A3\",  # 3 — blue           → Asia\n    colorant\"#BD8233\",  # 4 — ochre          → Europe\n]\n\n# Data — World Population 2023 (millions), Top 30 Countries by continent\nlabels = [\n    \"India\",      \"China\",    \"USA\",      \"Indonesia\", \"Pakistan\",\n    \"Nigeria\",    \"Brazil\",   \"Bangladesh\",\"Russia\",   \"Ethiopia\",\n    \"Mexico\",     \"Japan\",    \"Philippines\",\"Egypt\",   \"DR Congo\",\n    \"Vietnam\",    \"Iran\",     \"Germany\",  \"Thailand\",  \"UK\",\n    \"France\",     \"Tanzania\", \"S. Africa\",\"Kenya\",    \"Myanmar\",\n    \"Colombia\",   \"S. Korea\", \"Spain\",    \"Argentina\", \"Algeria\"\n]\n\nvalues = Float64[\n    1429, 1412,  339,  278,  231,\n     224,  215,  170,  144,  127,\n     128,  123,  115,  106,  102,\n      98,   89,   84,   72,   68,\n      68,   65,   60,   56,   54,\n      52,   52,   47,   46,   46\n]\n\ngroups = [\n    \"Asia\",    \"Asia\",    \"Americas\", \"Asia\",    \"Asia\",\n    \"Africa\",  \"Americas\",\"Asia\",    \"Europe\",  \"Africa\",\n    \"Americas\",\"Asia\",    \"Asia\",    \"Africa\",  \"Africa\",\n    \"Asia\",    \"Asia\",    \"Europe\",  \"Asia\",    \"Europe\",\n    \"Europe\",  \"Africa\",  \"Africa\",  \"Africa\",  \"Asia\",\n    \"Americas\",\"Asia\",    \"Europe\",  \"Americas\",\"Africa\"\n]\n\ngroup_names     = [\"Africa\", \"Americas\", \"Asia\", \"Europe\"]\ngroup_color_map = Dict(zip(group_names, IMPRINT_PALETTE))\ncircle_colors   = [group_color_map[g] for g in groups]\n\n# Radii: circle area proportional to population value\nmax_radius = 118.0\nradii = max_radius .* sqrt.(values ./ maximum(values))\nn = length(labels)\n\n# Initial positions: random spread scaled to expected packed area\ninit_spread = sqrt(sum(radii .^ 2)) * 0.55\npositions_x = randn(n) .* init_spread\npositions_y = randn(n) .* init_spread\n\n# Force-directed circle packing — repel overlapping pairs, attract toward center\npad = 3.0\nfor _ in 1:6000\n    for i in 1:n\n        for j in (i + 1):n\n            dx   = positions_x[i] - positions_x[j]\n            dy   = positions_y[i] - positions_y[j]\n            dist = sqrt(dx^2 + dy^2)\n            min_d = radii[i] + radii[j] + pad\n            if dist < min_d\n                if dist < 1.0e-8\n                    θr   = 2π * rand()\n                    dx, dy = cos(θr), sin(θr)\n                    dist  = 1.0\n                end\n                push_amt = (min_d - dist) / dist * 0.5\n                positions_x[i] += dx * push_amt\n                positions_y[i] += dy * push_amt\n                positions_x[j] -= dx * push_amt\n                positions_y[j] -= dy * push_amt\n            end\n        end\n        positions_x[i] *= 0.9988\n        positions_y[i] *= 0.9988\n    end\nend\n\n# Center the cluster on the canvas after simulation\ncx_mean = sum(positions_x) / n\ncy_mean = sum(positions_y) / n\npositions_x .-= cx_mean\npositions_y .-= cy_mean\n\n# Compute square axis limits that contain all circles with margin\nmargin   = 28.0\nx_min    = minimum(positions_x .- radii) - margin\nx_max    = maximum(positions_x .+ radii) + margin\ny_min    = minimum(positions_y .- radii) - margin\ny_max    = maximum(positions_y .+ radii) + margin\nplot_cx  = (x_min + x_max) / 2\nplot_cy  = (y_min + y_max) / 2\nhalf_ext = max(x_max - x_min, y_max - y_min) / 2\n\n# Title — 66 chars, within 67-char baseline → no fontsize reduction needed\ntitle_str  = \"World Population 2023 · bubble-packed · julia · makie · anyplot.ai\"\nn_title    = length(title_str)\ntitle_size = max(14, round(Int, 20 * min(1.0, 67.0 / n_title)))\n\n# Figure — square 1200×1200 × px_per_unit=2 → 2400×2400 final PNG\nfig = Figure(\n    size            = (1200, 1200),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title              = title_str,\n    titlesize          = Float32(title_size),\n    titlecolor         = INK,\n    backgroundcolor    = PAGE_BG,\n    aspect             = DataAspect(),\n    topspinevisible    = false,\n    bottomspinevisible = false,\n    leftspinevisible   = false,\n    rightspinevisible  = false,\n    xgridvisible       = false,\n    ygridvisible       = false,\n    xticksvisible      = false,\n    yticksvisible      = false,\n    xticklabelsvisible = false,\n    yticklabelsvisible = false,\n)\n\nxlims!(ax, plot_cx - half_ext, plot_cx + half_ext)\nylims!(ax, plot_cy - half_ext, plot_cy + half_ext)\n\n# Draw packed circles — 60-point polygon per circle\nθ_pts = range(0, 2π, length = 61)[1:60]\nfor i in 1:n\n    r   = radii[i]\n    pts = [Point2f(positions_x[i] + r * cos(t), positions_y[i] + r * sin(t)) for t in θ_pts]\n    poly!(ax, pts; color = (circle_colors[i], 0.88), strokewidth = 1.5, strokecolor = INK_SOFT)\nend\n\n# Country labels inside circles (white text, scaled to circle size)\nfor i in 1:n\n    r = radii[i]\n    if r >= 55\n        fs = clamp(round(Int, r / 6.0), 11, 20)\n        # Country name shifted up to make room for population annotation\n        text!(ax, positions_x[i], positions_y[i] + r * 0.18;\n            text     = labels[i],\n            align    = (:center, :center),\n            fontsize = Float32(fs),\n            color    = colorant\"#FFFFFF\",\n        )\n        # Population value annotation below the country name\n        pop_str = values[i] >= 1000 ?\n            string(round(values[i] / 1000; digits=2)) * \"B\" :\n            string(round(Int, values[i])) * \"M\"\n        text!(ax, positions_x[i], positions_y[i] - r * 0.18;\n            text     = pop_str,\n            align    = (:center, :center),\n            fontsize = Float32(clamp(round(Int, fs * 0.72), 9, 11)),\n            color    = RGBAf(1f0, 1f0, 1f0, 0.85f0),\n        )\n    elseif r >= 38\n        text!(ax, positions_x[i], positions_y[i];\n            text     = labels[i],\n            align    = (:center, :center),\n            fontsize = 10.0f0,\n            color    = colorant\"#FFFFFF\",\n        )\n    end\nend\n\n# Continent legend — horizontal, positioned below the chart\nlegend_entries = [PolyElement(color = c, strokecolor = INK_SOFT, strokewidth = 1.0)\n                  for c in IMPRINT_PALETTE]\nLegend(\n    fig[2, 1],\n    legend_entries,\n    group_names;\n    orientation  = :horizontal,\n    tellwidth    = false,\n    tellheight   = true,\n    framecolor   = INK_SOFT,\n    framevisible    = false,\n    labelcolor      = INK,\n    backgroundcolor = ELEVATED_BG,\n    labelsize       = 14.0f0,\n    padding      = (14, 14, 8, 8),\n)\n\nrowgap!(fig.layout, 6)\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}