{"spec_id":"cartogram-area-distortion","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ncartogram-area-distortion: Cartogram with Area Distortion by Data Value\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 78/100 | Updated: 2026-06-08\n\"\"\"\n\nimport math\nimport os\nimport sys\n\n\n# Remove the script's own directory from sys.path so that `import pygal` finds\n# the installed package rather than this file (which shares the package name).\n_here = os.path.abspath(os.path.dirname(os.path.abspath(__file__)))\nsys.path[:] = [p for p in sys.path if p and os.path.abspath(p) != _here]\ndel _here\n\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens — theme-adaptive chrome\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# Imprint sequential: low population density (green) → high density (blue)\nDENSITY_LOW = \"#009E73\"\nDENSITY_HIGH = \"#4467A3\"\n\n\ndef _lerp_hex(c0, c1, t):\n    r0, g0, b0 = (int(c0[i : i + 2], 16) for i in (1, 3, 5))\n    r1, g1, b1 = (int(c1[i : i + 2], 16) for i in (1, 3, 5))\n    r, g, b = (int(round(a + (b - a) * t)) for a, b in ((r0, r1), (g0, g1), (b0, b1)))\n    return f\"#{r:02X}{g:02X}{b:02X}\"\n\n\nSHORT_LABELS = {\n    \"S. Korea\": \"KR\",\n    \"Colombia\": \"CO\",\n    \"Australia\": \"AU\",\n    \"Vietnam\": \"VN\",\n    \"Kenya\": \"KE\",\n    \"S. Africa\": \"ZA\",\n    \"Italy\": \"IT\",\n    \"UK\": \"GB\",\n    \"Germany\": \"DE\",\n    \"France\": \"FR\",\n    \"Japan\": \"JP\",\n    \"Philippines\": \"PH\",\n    \"Bangladesh\": \"BD\",\n}\nABBREV_THRESHOLD = 100  # million\n\n# Countries grouped by continent: (population millions, land area thousand km², 2024 est.)\n# Distortion ratio = pop_share / area_share: >1 means the region GROWS vs geographic map.\nregions = {\n    \"Asia\": {\n        \"India\": (1441, 3287),\n        \"China\": (1425, 9597),\n        \"Indonesia\": (278, 1905),\n        \"Pakistan\": (240, 882),\n        \"Bangladesh\": (173, 148),\n        \"Japan\": (124, 378),\n        \"Philippines\": (117, 300),\n        \"Vietnam\": (99, 331),\n        \"S. Korea\": (52, 100),\n    },\n    \"Africa\": {\n        \"Nigeria\": (224, 924),\n        \"Ethiopia\": (126, 1104),\n        \"Egypt\": (113, 1001),\n        \"DR Congo\": (102, 2345),\n        \"S. Africa\": (60, 1221),\n        \"Kenya\": (55, 580),\n    },\n    \"Europe\": {\"Russia\": (144, 17098), \"Germany\": (84, 357), \"UK\": (68, 244), \"France\": (68, 640), \"Italy\": (59, 301)},\n    \"Americas\": {\"USA\": (340, 9834), \"Brazil\": (216, 8516), \"Mexico\": (130, 1964), \"Colombia\": (52, 1139)},\n    \"Oceania\": {\"Australia\": (27, 7692)},\n}\n\ntotal_pop = sum(pop for cont in regions.values() for pop, _ in cont.values())\ntotal_area = sum(area for cont in regions.values() for _, area in cont.values())\n\n# Flat list preserving continent order so colours cycle meaningfully\nall_items = [(cont, name, pop, area) for cont, countries in regions.items() for name, (pop, area) in countries.items()]\n\n# Log-scaled density for perceptually balanced gradient (linear would crowd low end)\nlog_densities = [math.log1p(pop / area) for _, _, pop, area in all_items]\nmin_ld, max_ld = min(log_densities), max(log_densities)\n\ntile_colors = tuple(\n    _lerp_hex(DENSITY_LOW, DENSITY_HIGH, (math.log1p(pop / area) - min_ld) / (max_ld - min_ld))\n    for _, _, pop, area in all_items\n)\n\ntitle = \"World Population Cartogram · cartogram-area-distortion · python · pygal · anyplot.ai\"\nsubtitle = (\n    \"Tile area ~ population (2024 est.) · Tile color = population density: sparse (green) → dense (blue)\"\n    \"\\nNote: treemap approximates cartogram — geographic adjacency not preserved (pygal limitation)\"\n)\ntitle_fontsize = max(44, round(66 * 67 / len(title)))\n\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=tile_colors,\n    title_font_size=title_fontsize,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=36,\n    value_font_size=46,\n    stroke_width=2.5,\n)\n\n# Per-country series: one series per country so each tile gets its own density colour.\n# 27 series → legend disabled (27-item legend would dominate the canvas); colour\n# encoding and geographic note explained in the subtitle instead.\ntreemap = pygal.Treemap(\n    style=custom_style,\n    width=3200,\n    height=1800,\n    title=f\"{title}\\n{subtitle}\",\n    show_legend=False,\n    print_labels=True,\n    print_values=False,\n    margin=50,\n    margin_bottom=60,\n    margin_top=25,\n    margin_left=50,\n    margin_right=50,\n    truncate_label=-1,\n    spacing=6,\n    rounded_corners=4,\n)\n\nfor cont, name, pop, area in all_items:\n    pop_share = pop / total_pop\n    area_share = area / total_area\n    ratio = pop_share / area_share\n    density = pop / area\n    label = SHORT_LABELS.get(name, name) if pop < ABBREV_THRESHOLD else name\n    treemap.add(\n        f\"{name} ({cont})\",\n        [\n            {\n                \"value\": pop,\n                \"label\": label,\n                \"formatter\": lambda x, n=name, r=ratio, c=cont, d=density: (\n                    f\"{n} ({c}): {x:,.0f}M pop · ×{r:.1f} vs map · {d:.1f} pop/1000 km²\"\n                ),\n            }\n        ],\n    )\n\n# Save PNG and interactive HTML\ntreemap.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(treemap.render())\n"}