{"spec_id":"cartogram-area-distortion","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ncartogram-area-distortion: Cartogram with Area Distortion by Data Value\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 84/100 | Updated: 2026-06-08\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_cartesian,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_path,\n    geom_point,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_fill_gradient,\n    scale_size,\n    theme,\n    theme_minimal,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\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\n# Imprint sequential colormap: brand green (#009E73) → blue (#4467A3)\nCMAP_LOW = \"#009E73\"\nCMAP_HIGH = \"#4467A3\"\n\n# Data: 20 European countries — population (millions) and GDP per capita (thousands USD)\n# NL/BE positions spread further apart to reduce label crowding in Central Europe\ncountries_data = {\n    \"country\": [\n        \"Germany\",\n        \"France\",\n        \"United Kingdom\",\n        \"Italy\",\n        \"Spain\",\n        \"Poland\",\n        \"Netherlands\",\n        \"Belgium\",\n        \"Sweden\",\n        \"Austria\",\n        \"Switzerland\",\n        \"Norway\",\n        \"Denmark\",\n        \"Finland\",\n        \"Ireland\",\n        \"Portugal\",\n        \"Czech Republic\",\n        \"Greece\",\n        \"Hungary\",\n        \"Romania\",\n    ],\n    \"population\": [\n        83.2,\n        67.8,\n        67.0,\n        59.0,\n        47.4,\n        37.7,\n        17.5,\n        11.6,\n        10.4,\n        9.1,\n        8.8,\n        5.4,\n        5.9,\n        5.5,\n        5.1,\n        10.3,\n        10.8,\n        10.4,\n        9.7,\n        19.0,\n    ],\n    \"gdp_per_capita\": [\n        48.7,\n        42.3,\n        46.1,\n        34.5,\n        30.1,\n        17.8,\n        57.0,\n        51.2,\n        55.7,\n        53.3,\n        92.4,\n        89.2,\n        67.8,\n        53.2,\n        100.2,\n        24.5,\n        27.0,\n        20.2,\n        18.8,\n        15.1,\n    ],\n    # Adjusted centroids: NL moved east, BE moved west/south for visual separation\n    \"lon\": [\n        10.4,\n        2.2,\n        -1.2,\n        12.6,\n        -3.7,\n        19.1,\n        6.5,\n        2.0,\n        15.0,\n        14.6,\n        8.5,\n        8.5,\n        9.5,\n        25.7,\n        -8.2,\n        -8.2,\n        16.5,\n        23.7,\n        18.5,\n        25.0,\n    ],\n    \"lat\": [\n        51.2,\n        46.2,\n        52.5,\n        41.9,\n        40.5,\n        51.9,\n        53.0,\n        49.5,\n        60.1,\n        47.0,\n        45.5,\n        60.5,\n        56.3,\n        61.9,\n        53.4,\n        39.4,\n        49.5,\n        39.1,\n        47.5,\n        45.9,\n    ],\n    \"abbr\": [\n        \"DE\",\n        \"FR\",\n        \"UK\",\n        \"IT\",\n        \"ES\",\n        \"PL\",\n        \"NL\",\n        \"BE\",\n        \"SE\",\n        \"AT\",\n        \"CH\",\n        \"NO\",\n        \"DK\",\n        \"FI\",\n        \"IE\",\n        \"PT\",\n        \"CZ\",\n        \"GR\",\n        \"HU\",\n        \"RO\",\n    ],\n}\ndf = pd.DataFrame(countries_data)\ndf[\"highlight\"] = (df[\"population\"] < 15) & (df[\"gdp_per_capita\"] > 50)\n\n# Stars shifted +1.8° north so they sit above country abbreviation labels\ndf_stars = df[df[\"highlight\"]].copy()\ndf_stars[\"lat\"] = df_stars[\"lat\"] + 1.8\n\n# Simplified European coastline polygon for outer geographic boundary\neurope_outline = pd.DataFrame(\n    {\n        \"lon\": [-12, -10, -5, 0, 5, 10, 15, 20, 25, 30, 32, 30, 28, 25, 28, 32, 30, 25, 20, 15, 10, 5, 0, -5, -10, -12],\n        \"lat\": [43, 36, 36, 38, 37, 36, 36, 35, 36, 38, 42, 45, 45, 50, 55, 60, 65, 70, 68, 62, 58, 52, 50, 48, 44, 43],\n        \"group\": [\"outline\"] * 26,\n    }\n)\n\n# Simplified individual country border outlines — closed polygons (last pt = first pt)\n# These show each country's approximate geographic footprint alongside its bubble\n_borders = {\n    \"DE\": [(5.9, 47.3), (13.5, 47.5), (15.0, 51.0), (14.3, 54.0), (8.5, 55.0), (6.1, 53.6), (5.9, 47.3)],\n    \"FR\": [(-4.5, 43.3), (3.2, 42.5), (7.6, 43.8), (7.6, 47.6), (2.5, 51.0), (-1.5, 47.5), (-4.5, 43.3)],\n    \"GB\": [(-5.7, 50.0), (1.8, 51.4), (0.5, 54.0), (-0.1, 58.7), (-3.5, 58.5), (-5.5, 55.0), (-5.7, 50.0)],\n    \"IT\": [(7.0, 44.0), (13.7, 44.0), (16.5, 40.0), (15.5, 37.5), (13.0, 37.5), (8.0, 38.5), (7.0, 44.0)],\n    \"ES\": [(-8.9, 43.7), (3.2, 43.4), (3.0, 38.0), (-1.0, 36.0), (-8.9, 36.0), (-8.9, 43.7)],\n    \"PL\": [(14.1, 54.4), (24.1, 54.4), (24.1, 49.0), (18.2, 49.0), (14.1, 49.5), (14.1, 54.4)],\n    \"NL\": [(3.4, 51.3), (7.2, 53.1), (7.2, 53.5), (4.7, 53.5), (3.4, 51.6), (3.4, 51.3)],\n    \"BE\": [(2.5, 49.5), (6.4, 49.5), (6.4, 50.8), (4.0, 51.5), (2.5, 50.8), (2.5, 49.5)],\n    \"SE\": [(11.1, 55.4), (16.0, 56.0), (18.5, 59.0), (22.5, 65.0), (17.0, 67.0), (11.9, 59.7), (11.1, 55.4)],\n    \"AT\": [(9.5, 46.4), (17.2, 46.4), (17.2, 49.0), (9.5, 49.0), (9.5, 46.4)],\n    \"CH\": [(5.9, 45.8), (10.5, 45.8), (10.5, 47.8), (5.9, 47.8), (5.9, 45.8)],\n    \"NO\": [(4.6, 58.0), (10.0, 57.9), (14.0, 64.0), (20.0, 67.0), (14.0, 67.0), (8.0, 63.0), (4.6, 58.0)],\n    \"DK\": [(8.0, 54.6), (15.2, 55.4), (12.5, 57.7), (8.0, 57.5), (8.0, 54.6)],\n    \"FI\": [(20.0, 59.8), (29.0, 61.0), (31.5, 65.5), (28.0, 67.0), (22.0, 67.0), (20.0, 65.0), (20.0, 59.8)],\n    \"IE\": [(-10.5, 51.4), (-6.0, 51.4), (-6.5, 54.5), (-10.5, 54.5), (-10.5, 51.4)],\n    \"PT\": [(-9.5, 36.9), (-6.8, 36.9), (-6.8, 42.1), (-9.5, 42.1), (-9.5, 36.9)],\n    \"CZ\": [(12.1, 48.6), (18.9, 48.6), (18.9, 51.0), (12.1, 51.0), (12.1, 48.6)],\n    \"GR\": [(19.4, 35.0), (28.3, 35.0), (28.3, 42.0), (22.0, 42.0), (19.4, 38.0), (19.4, 35.0)],\n    \"HU\": [(16.1, 45.8), (22.9, 45.8), (22.9, 48.6), (16.1, 48.6), (16.1, 45.8)],\n    \"RO\": [(20.3, 43.6), (29.7, 43.6), (29.7, 48.3), (22.0, 48.3), (20.3, 45.5), (20.3, 43.6)],\n}\nborder_rows = []\nfor abbr, pts in _borders.items():\n    for lon_v, lat_v in pts:\n        border_rows.append({\"group\": abbr, \"lon\": lon_v, \"lat\": lat_v})\ncountry_borders = pd.DataFrame(border_rows)\n\n# Title with fontsize scaled for total character count (default 16px, floor 11px)\ntitle = \"European Population Cartogram · cartogram-area-distortion · python · letsplot · anyplot.ai\"\nn = len(title)\ntitle_size = max(11, round(16 * 67 / n))\n\nplot = (\n    ggplot()\n    # Faint European coastline for outer geographic context\n    + geom_polygon(\n        aes(x=\"lon\", y=\"lat\", group=\"group\"), data=europe_outline, fill=PAGE_BG, color=INK_MUTED, size=0.5, alpha=0.4\n    )\n    # Individual country border outlines — makes area distortion legible vs actual footprints\n    + geom_path(aes(x=\"lon\", y=\"lat\", group=\"group\"), data=country_borders, color=INK_MUTED, size=0.35, alpha=0.5)\n    # Non-highlighted countries: bubble area ∝ population, fill color = GDP per capita\n    + geom_point(\n        aes(x=\"lon\", y=\"lat\", size=\"population\", fill=\"gdp_per_capita\"),\n        data=df[~df[\"highlight\"]],\n        shape=21,\n        color=INK_SOFT,\n        stroke=0.5,\n        alpha=0.82,\n        tooltips=layer_tooltips()\n        .title(\"@country\")\n        .line(\"Population|@population M\")\n        .line(\"GDP/capita|$@gdp_per_capita K\"),\n    )\n    # Highlighted small-but-wealthy nations — bold border for storytelling emphasis\n    + geom_point(\n        aes(x=\"lon\", y=\"lat\", size=\"population\", fill=\"gdp_per_capita\"),\n        data=df[df[\"highlight\"]],\n        shape=21,\n        color=INK,\n        stroke=1.6,\n        alpha=0.95,\n        tooltips=layer_tooltips()\n        .title(\"@country\")\n        .line(\"Population|@population M\")\n        .line(\"GDP/capita|$@gdp_per_capita K\"),\n    )\n    # Stars nudged +1.8° north so they sit above abbreviation labels, not on top of them\n    + geom_point(aes(x=\"lon\", y=\"lat\"), data=df_stars, shape=8, size=3.0, color=INK)\n    + scale_size(range=[8, 26], name=\"Population (M)\", breaks=[5, 20, 40, 80])\n    + scale_fill_gradient(low=CMAP_LOW, high=CMAP_HIGH, name=\"GDP/capita (USD K)\")\n    # Three-tier label hierarchy: large bold, medium, small — all inside bubbles\n    + geom_text(\n        aes(x=\"lon\", y=\"lat\", label=\"abbr\"), data=df[df[\"population\"] > 30], size=12, color=INK, fontface=\"bold\"\n    )\n    + geom_text(\n        aes(x=\"lon\", y=\"lat\", label=\"abbr\"),\n        data=df[(df[\"population\"] > 10) & (df[\"population\"] <= 30)],\n        size=9,\n        color=INK,\n    )\n    + geom_text(aes(x=\"lon\", y=\"lat\", label=\"abbr\"), data=df[df[\"population\"] <= 10], size=8, color=INK_SOFT)\n    # Annotation near Atlantic/Nordic highlighted cluster (IE/NO/DK area)\n    + geom_text(\n        aes(x=\"x\", y=\"y\"),\n        data=pd.DataFrame({\"x\": [-9.0], \"y\": [61.5]}),\n        label=\"Small nations,\\nhighest wealth\",\n        size=8,\n        color=INK_MUTED,\n        fontface=\"italic\",\n        hjust=0,\n    )\n    + labs(title=title, subtitle=\"Bubble size = population  |  Color = GDP per capita  |  ★ = small but wealthy\")\n    + coord_cartesian(xlim=[-16, 33], ylim=[34, 67])\n    + ggsize(800, 450)\n    + theme_minimal()\n    + theme(\n        plot_title=element_text(size=title_size, face=\"bold\", color=INK),\n        plot_subtitle=element_text(size=10, color=INK_MUTED),\n        legend_title=element_text(size=10, color=INK),\n        legend_text=element_text(size=9, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        panel_grid=element_blank(),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n    )\n)\n\n# Save PNG (3200×1800 px via scale=4) and interactive HTML\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}