{"spec_id":"cartogram-area-distortion","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\ncartogram-area-distortion: Cartogram with Area Distortion by Data Value\nLibrary: plotly 6.8.0 | Python 3.13.13\nQuality: 82/100 | Updated: 2026-06-08\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\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 colorscale for continuous density data\nimprint_seq = [[0.0, \"#009E73\"], [1.0, \"#4467A3\"]]\n\n# Data - US states sized by population (2023 Census estimates, millions)\nstates = [\n    \"CA\",\n    \"TX\",\n    \"FL\",\n    \"NY\",\n    \"PA\",\n    \"IL\",\n    \"OH\",\n    \"GA\",\n    \"NC\",\n    \"MI\",\n    \"NJ\",\n    \"VA\",\n    \"WA\",\n    \"AZ\",\n    \"MA\",\n    \"TN\",\n    \"IN\",\n    \"MO\",\n    \"MD\",\n    \"WI\",\n    \"CO\",\n    \"MN\",\n    \"SC\",\n    \"AL\",\n    \"LA\",\n    \"KY\",\n    \"OR\",\n    \"OK\",\n    \"CT\",\n    \"UT\",\n    \"IA\",\n    \"NV\",\n    \"AR\",\n    \"MS\",\n    \"KS\",\n    \"NM\",\n    \"NE\",\n    \"ID\",\n    \"WV\",\n    \"HI\",\n    \"NH\",\n    \"ME\",\n    \"MT\",\n    \"RI\",\n    \"DE\",\n    \"SD\",\n    \"ND\",\n    \"AK\",\n    \"VT\",\n    \"WY\",\n]\n\npopulation = np.array(\n    [\n        39.03,\n        30.03,\n        22.24,\n        19.57,\n        12.97,\n        12.55,\n        11.78,\n        10.91,\n        10.70,\n        10.04,\n        9.29,\n        8.64,\n        7.81,\n        7.36,\n        7.00,\n        7.05,\n        6.83,\n        6.18,\n        6.18,\n        5.89,\n        5.84,\n        5.71,\n        5.28,\n        5.07,\n        4.59,\n        4.53,\n        4.24,\n        4.00,\n        3.62,\n        3.42,\n        3.20,\n        3.18,\n        3.05,\n        2.94,\n        2.94,\n        2.11,\n        1.97,\n        1.94,\n        1.77,\n        1.44,\n        1.40,\n        1.39,\n        1.12,\n        1.10,\n        1.02,\n        0.91,\n        0.78,\n        0.74,\n        0.65,\n        0.58,\n    ]\n)\n\narea_sq_miles = np.array(\n    [\n        163696,\n        268596,\n        65758,\n        54555,\n        46054,\n        57914,\n        44826,\n        59425,\n        53819,\n        96714,\n        8723,\n        42775,\n        71298,\n        113990,\n        10554,\n        42144,\n        36420,\n        69707,\n        12406,\n        65496,\n        104094,\n        86936,\n        32020,\n        52420,\n        52378,\n        40408,\n        98379,\n        69899,\n        5543,\n        84897,\n        56273,\n        110572,\n        53179,\n        48432,\n        82278,\n        121590,\n        77348,\n        83569,\n        24230,\n        10932,\n        9349,\n        35380,\n        147040,\n        1545,\n        2489,\n        77116,\n        70698,\n        665384,\n        9616,\n        97813,\n    ]\n)\n\nlats = np.array(\n    [\n        36.78,\n        31.97,\n        27.66,\n        42.93,\n        41.20,\n        40.63,\n        40.42,\n        32.68,\n        35.63,\n        44.31,\n        40.06,\n        37.43,\n        47.75,\n        34.05,\n        42.41,\n        35.52,\n        40.27,\n        38.57,\n        39.05,\n        43.78,\n        39.55,\n        46.73,\n        33.84,\n        32.32,\n        31.17,\n        37.84,\n        43.80,\n        35.47,\n        41.60,\n        39.32,\n        41.88,\n        38.80,\n        35.20,\n        32.35,\n        39.01,\n        34.52,\n        41.49,\n        44.07,\n        38.60,\n        19.90,\n        43.19,\n        45.25,\n        46.88,\n        41.58,\n        38.91,\n        43.97,\n        47.55,\n        63.59,\n        44.56,\n        43.08,\n    ]\n)\n\nlons = np.array(\n    [\n        -119.42,\n        -99.90,\n        -81.52,\n        -75.58,\n        -77.19,\n        -89.40,\n        -82.91,\n        -83.54,\n        -79.81,\n        -84.71,\n        -74.41,\n        -78.66,\n        -120.74,\n        -111.09,\n        -71.38,\n        -86.15,\n        -86.13,\n        -91.83,\n        -76.64,\n        -89.62,\n        -105.78,\n        -94.69,\n        -81.16,\n        -86.90,\n        -91.87,\n        -84.27,\n        -120.55,\n        -97.09,\n        -72.76,\n        -111.09,\n        -93.10,\n        -116.42,\n        -92.37,\n        -89.40,\n        -98.48,\n        -105.87,\n        -99.90,\n        -114.74,\n        -80.62,\n        -155.58,\n        -71.57,\n        -69.45,\n        -110.36,\n        -71.48,\n        -75.53,\n        -99.44,\n        -101.00,\n        -154.49,\n        -72.58,\n        -107.29,\n    ]\n)\n\ndensity = population * 1e6 / area_sq_miles\n\n# NE state offsets fan states into distinct positions to avoid bubble overlap\nne_offsets = {\n    \"NJ\": (-2.0, 3.0),  # → (38.1, -71.4) over Atlantic SE of NJ\n    \"CT\": (0.5, 5.0),  # → (42.1, -67.8) east of Maine coast\n    \"MA\": (1.5, 3.0),  # → (43.9, -68.4) NE of natural position\n    \"RI\": (-1.5, 4.0),  # → (40.1, -67.5) well east of CT\n    \"NH\": (2.5, 1.5),  # → (45.7, -70.1) north-east\n    \"VT\": (3.0, -0.5),  # → (47.6, -73.1) far north\n    \"DE\": (-3.0, 3.0),  # → (35.9, -72.5) far south over Atlantic\n    \"MD\": (-3.0, 0.5),  # → (36.1, -76.1) far south\n    \"ME\": (2.5, 2.0),  # → (47.8, -67.5) far north-east\n}\nfor i, s in enumerate(states):\n    if s in ne_offsets:\n        dlat, dlon = ne_offsets[s]\n        lats[i] += dlat\n        lons[i] += dlon\n\n# Scale bubble sizes: area proportional to population\nmax_marker_size = 70\nraw_sizes = np.sqrt(population / population.max()) * max_marker_size\nsizes = np.clip(raw_sizes, 12, max_marker_size)\n\n# Log scale for density color mapping\nlog_density = np.log10(density)\n\n# Labels for states >= 2M population\nlabel_texts = [s if p >= 2.0 else \"\" for s, p in zip(states, population, strict=False)]\n\n# Title — mandatory format; scale fontsize to prevent overflow at this length\ntitle_str = \"U.S. States Population Cartogram · cartogram-area-distortion · python · plotly · anyplot.ai\"\ntitle_fontsize = max(11, round(16 * 67 / len(title_str)))\n\n# Theme-adaptive geo background colors\nland_color = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nlake_color = \"#EDE9DF\" if THEME == \"light\" else \"#2A2A26\"\nboundary_color = \"rgba(74,74,68,0.4)\" if THEME == \"light\" else \"rgba(184,183,176,0.3)\"\n\nfig = go.Figure()\n\n# Reference layer: faint state boundary outlines for geographic context\nfig.add_trace(\n    go.Choropleth(\n        locationmode=\"USA-states\",\n        locations=states,\n        z=[0] * len(states),\n        colorscale=[[0, \"rgba(0,0,0,0)\"], [1, \"rgba(0,0,0,0)\"]],\n        showscale=False,\n        marker={\"line\": {\"color\": boundary_color, \"width\": 0.6}},\n        hoverinfo=\"skip\",\n    )\n)\n\n# Bubble cartogram: size ∝ population, color ∝ density (Imprint sequential)\nfig.add_trace(\n    go.Scattergeo(\n        locationmode=\"USA-states\",\n        lon=lons,\n        lat=lats,\n        text=[\n            f\"<b>{s}</b><br>Population: {p:.1f}M<br>Density: {d:,.0f} per sq mi<br>Area: {a:,} sq mi\"\n            for s, p, d, a in zip(states, population, density, area_sq_miles, strict=False)\n        ],\n        hoverinfo=\"text\",\n        marker={\n            \"size\": sizes,\n            \"color\": log_density,\n            \"colorscale\": imprint_seq,\n            \"cmin\": np.log10(5),\n            \"cmax\": np.log10(6000),\n            \"colorbar\": {\n                \"title\": {\n                    \"text\": \"Population Density<br>(per sq mi)\",\n                    \"font\": {\"size\": 14, \"family\": \"Arial\", \"color\": INK},\n                },\n                \"tickfont\": {\"size\": 11, \"color\": INK_SOFT},\n                \"tickvals\": np.log10([10, 50, 100, 500, 1000, 5000]).tolist(),\n                \"ticktext\": [\"10\", \"50\", \"100\", \"500\", \"1k\", \"5k\"],\n                \"len\": 0.55,\n                \"thickness\": 20,\n                \"x\": 0.94,\n                \"outlinewidth\": 0,\n                \"bgcolor\": ELEVATED_BG,\n                \"tickcolor\": INK_SOFT,\n            },\n            \"line\": {\"width\": 1.5, \"color\": PAGE_BG},\n            \"opacity\": 0.90,\n            \"sizemode\": \"diameter\",\n        },\n    )\n)\n\n# State abbreviation labels — light color contrasts well over green/blue bubbles\nfig.add_trace(\n    go.Scattergeo(\n        locationmode=\"USA-states\",\n        lon=lons,\n        lat=lats,\n        text=label_texts,\n        mode=\"text\",\n        textfont={\n            \"size\": [max(9, min(14, int(s / 5))) if t else 1 for s, t in zip(sizes, label_texts, strict=False)],\n            \"color\": \"#F0EFE8\",\n            \"family\": \"Arial Black\",\n        },\n        hoverinfo=\"skip\",\n        showlegend=False,\n    )\n)\n\nfig.update_layout(\n    title={\n        \"text\": title_str,\n        \"font\": {\"size\": title_fontsize, \"family\": \"Arial\", \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": 0.97,\n    },\n    geo={\n        \"scope\": \"usa\",\n        \"showframe\": False,\n        \"showcoastlines\": True,\n        \"coastlinecolor\": boundary_color,\n        \"coastlinewidth\": 0.5,\n        \"showland\": True,\n        \"landcolor\": land_color,\n        \"showlakes\": True,\n        \"lakecolor\": lake_color,\n        \"bgcolor\": PAGE_BG,\n        \"projection_type\": \"albers usa\",\n    },\n    autosize=False,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    margin={\"l\": 20, \"r\": 100, \"t\": 70, \"b\": 60},\n    showlegend=False,\n    annotations=[\n        {\n            \"text\": \"Dorling bubble cartogram  ·  <b>Area</b> ∝ population  ·  <b>Color</b> ∝ density\",\n            \"xref\": \"paper\",\n            \"yref\": \"paper\",\n            \"x\": 0.5,\n            \"y\": -0.04,\n            \"showarrow\": False,\n            \"font\": {\"size\": 12, \"color\": INK_MUTED, \"family\": \"Arial\"},\n        },\n        {\n            \"text\": \"California (39M) has 6× more people than<br>median state, yet New Jersey is 4× denser\",\n            \"xref\": \"paper\",\n            \"yref\": \"paper\",\n            \"x\": 0.02,\n            \"y\": 0.08,\n            \"showarrow\": False,\n            \"font\": {\"size\": 12, \"color\": INK_MUTED, \"family\": \"Arial\"},\n            \"align\": \"left\",\n            \"bgcolor\": ELEVATED_BG,\n            \"borderpad\": 6,\n        },\n    ],\n)\n\n# Save — landscape 3200×1800 (width=800, height=450, scale=4)\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}