{"spec_id":"cartogram-area-distortion","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\ncartogram-area-distortion: Cartogram with Area Distortion by Data Value\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-08\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.patheffects as pe\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.cm import ScalarMappable\nfrom matplotlib.collections import PatchCollection\nfrom matplotlib.colors import LinearSegmentedColormap, Normalize\n\n\n# Theme tokens\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 for GDP per capita (single-polarity continuous data)\nimprint_seq = LinearSegmentedColormap.from_list(\"imprint_seq\", [\"#009E73\", \"#4467A3\"])\n\n# Data — US states with approximate centroids, population (thousands), and GDP per capita\nnp.random.seed(42)\n\nstates = {\n    \"WA\": (-122.0, 47.5, 7615, 78000),\n    \"OR\": (-120.5, 44.0, 4218, 56000),\n    \"CA\": (-119.5, 37.0, 39538, 81000),\n    \"NV\": (-116.8, 39.0, 3104, 55000),\n    \"ID\": (-114.5, 44.5, 1839, 45000),\n    \"MT\": (-109.5, 47.0, 1084, 42000),\n    \"WY\": (-107.5, 43.0, 577, 65000),\n    \"UT\": (-111.5, 39.5, 3272, 54000),\n    \"CO\": (-105.5, 39.0, 5773, 70000),\n    \"AZ\": (-111.5, 34.5, 7279, 52000),\n    \"NM\": (-106.0, 34.5, 2117, 46000),\n    \"ND\": (-100.5, 47.5, 779, 63000),\n    \"SD\": (-100.0, 44.5, 887, 58000),\n    \"NE\": (-99.8, 41.5, 1962, 61000),\n    \"KS\": (-98.5, 38.5, 2937, 55000),\n    \"OK\": (-97.5, 35.5, 3959, 48000),\n    \"TX\": (-99.0, 31.5, 29145, 62000),\n    \"MN\": (-94.5, 46.0, 5640, 65000),\n    \"IA\": (-93.5, 42.0, 3190, 59000),\n    \"MO\": (-92.5, 38.5, 6154, 50000),\n    \"AR\": (-92.5, 35.0, 3012, 41000),\n    \"LA\": (-92.0, 31.0, 4657, 49000),\n    \"WI\": (-89.5, 44.5, 5822, 56000),\n    \"IL\": (-89.0, 40.0, 12812, 68000),\n    \"MI\": (-84.5, 44.0, 10077, 51000),\n    \"IN\": (-86.0, 40.0, 6733, 53000),\n    \"OH\": (-82.5, 40.5, 11799, 57000),\n    \"KY\": (-85.5, 37.8, 4506, 46000),\n    \"TN\": (-86.0, 35.8, 6911, 52000),\n    \"MS\": (-89.5, 32.5, 2961, 38000),\n    \"AL\": (-86.8, 32.8, 5024, 44000),\n    \"GA\": (-83.5, 33.0, 10712, 58000),\n    \"FL\": (-81.5, 28.5, 21538, 51000),\n    \"SC\": (-81.0, 34.0, 5119, 46000),\n    \"NC\": (-79.5, 35.5, 10439, 56000),\n    \"VA\": (-79.0, 37.5, 8631, 63000),\n    \"WV\": (-80.5, 38.8, 1794, 40000),\n    \"PA\": (-77.5, 41.0, 13002, 65000),\n    \"NY\": (-74.5, 43.0, 20201, 82000),\n    \"NJ\": (-74.5, 40.0, 9289, 72000),\n    \"MD\": (-76.5, 39.0, 6177, 62000),\n    \"DE\": (-75.5, 39.0, 990, 73000),\n    \"CT\": (-72.7, 41.6, 3606, 76000),\n    \"RI\": (-71.5, 41.7, 1098, 58000),\n    \"MA\": (-71.8, 42.3, 7029, 82000),\n    \"VT\": (-72.6, 44.0, 643, 52000),\n    \"NH\": (-71.5, 43.5, 1377, 60000),\n    \"ME\": (-69.0, 45.0, 1362, 46000),\n}\n\nnames = list(states.keys())\nlons = np.array([states[s][0] for s in names])\nlats = np.array([states[s][1] for s in names])\npopulations = np.array([states[s][2] for s in names])\ngdp_per_capita = np.array([states[s][3] for s in names])\n\n# Scale circle radii proportional to sqrt(population) for area proportionality\nmax_pop = populations.max()\nradii = np.sqrt(populations / max_pop) * 2.8\nmin_radius = 0.55\nradii = np.maximum(radii, min_radius)\n\n# Collision avoidance — more iterations and stronger push for dense NE corridor\nadjusted_lons = lons.copy()\nadjusted_lats = lats.copy()\nne_states = {\"NY\", \"NJ\", \"CT\", \"RI\", \"MA\", \"VT\", \"NH\", \"ME\", \"PA\", \"DE\", \"MD\"}\nfor _ in range(150):\n    for i in range(len(names)):\n        for j in range(i + 1, len(names)):\n            dx = adjusted_lons[j] - adjusted_lons[i]\n            dy = adjusted_lats[j] - adjusted_lats[i]\n            dist = np.sqrt(dx**2 + dy**2)\n            both_ne = names[i] in ne_states and names[j] in ne_states\n            spacing = 1.5 if both_ne else 1.25\n            min_dist = (radii[i] + radii[j]) * spacing\n            if dist < min_dist and dist > 0:\n                overlap = min_dist - dist\n                push = overlap / 2\n                nx, ny = dx / dist, dy / dist\n                strength = 0.55 if both_ne else 0.35\n                adjusted_lons[i] -= nx * push * strength\n                adjusted_lats[i] -= ny * push * strength\n                adjusted_lons[j] += nx * push * strength\n                adjusted_lats[j] += ny * push * strength\n\n# Plot\nfig = plt.figure(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax = fig.add_axes([0.03, 0.08, 0.73, 0.83])\nax.set_facecolor(PAGE_BG)\nnorm = Normalize(vmin=gdp_per_capita.min(), vmax=gdp_per_capita.max())\n\n# Build circles as PatchCollection for efficient rendering (distinctive matplotlib feature)\nshadow_patches = []\nmain_patches = []\nface_colors = []\nsort_idx = np.argsort(-populations)\n\nfor i in sort_idx:\n    color = imprint_seq(norm(gdp_per_capita[i]))\n    if populations[i] > 8000:\n        shadow = mpatches.Circle((adjusted_lons[i] + 0.15, adjusted_lats[i] - 0.15), radii[i])\n        shadow_patches.append(shadow)\n    circle = mpatches.Circle((adjusted_lons[i], adjusted_lats[i]), radii[i])\n    main_patches.append(circle)\n    face_colors.append(color)\n\nif shadow_patches:\n    shadow_edgecolor = \"#00000012\" if THEME == \"light\" else \"#FFFFFF08\"\n    shadow_col = PatchCollection(\n        shadow_patches, facecolors=\"none\", edgecolors=shadow_edgecolor, linewidths=4, zorder=1.8\n    )\n    ax.add_collection(shadow_col)\n\nmain_col = PatchCollection(\n    main_patches, facecolors=face_colors, edgecolors=PAGE_BG, linewidths=1.2, alpha=0.94, zorder=2\n)\nax.add_collection(main_col)\n\n# State labels — lower threshold to include all notable states including tiny WY/VT\nfor i in sort_idx:\n    if populations[i] > 560:\n        fontsize = 8 if populations[i] > 5000 else 7\n        ax.text(\n            adjusted_lons[i],\n            adjusted_lats[i],\n            names[i],\n            ha=\"center\",\n            va=\"center\",\n            fontsize=fontsize,\n            fontweight=\"bold\",\n            color=INK,\n            zorder=4,\n            path_effects=[pe.withStroke(linewidth=2.5, foreground=PAGE_BG), pe.Normal()],\n        )\n\n# Annotations with theme-adaptive styling\nca_idx = names.index(\"CA\")\nax.annotate(\n    \"California\\n39.5M people\",\n    fontsize=8,\n    xy=(adjusted_lons[ca_idx], adjusted_lats[ca_idx] - radii[ca_idx]),\n    xytext=(adjusted_lons[ca_idx] + 5, adjusted_lats[ca_idx] - 6.5),\n    arrowprops={\"arrowstyle\": \"-|>\", \"color\": INK_SOFT, \"lw\": 1.5, \"connectionstyle\": \"arc3,rad=0.15\"},\n    fontweight=\"bold\",\n    color=INK,\n    ha=\"center\",\n    zorder=5,\n    bbox={\"boxstyle\": \"round,pad=0.35\", \"facecolor\": ELEVATED_BG, \"edgecolor\": INK_SOFT, \"alpha\": 0.92},\n)\n\nny_idx = names.index(\"NY\")\nax.annotate(\n    \"New York\\n$82K GDP/cap\",\n    fontsize=8,\n    xy=(adjusted_lons[ny_idx], adjusted_lats[ny_idx] + radii[ny_idx]),\n    xytext=(adjusted_lons[ny_idx] - 12, adjusted_lats[ny_idx] + 5.5),\n    arrowprops={\"arrowstyle\": \"-|>\", \"color\": INK_SOFT, \"lw\": 1.5, \"connectionstyle\": \"arc3,rad=-0.15\"},\n    fontweight=\"bold\",\n    color=INK,\n    ha=\"center\",\n    zorder=5,\n    bbox={\"boxstyle\": \"round,pad=0.35\", \"facecolor\": ELEVATED_BG, \"edgecolor\": INK_SOFT, \"alpha\": 0.92},\n)\n\nms_idx = names.index(\"MS\")\nax.annotate(\n    \"Mississippi\\n$38K GDP/cap (lowest)\",\n    fontsize=7,\n    xy=(adjusted_lons[ms_idx], adjusted_lats[ms_idx]),\n    xytext=(adjusted_lons[ms_idx] - 11, adjusted_lats[ms_idx] + 4.5),\n    arrowprops={\"arrowstyle\": \"-|>\", \"color\": INK_MUTED, \"lw\": 1.2, \"connectionstyle\": \"arc3,rad=0.1\"},\n    fontweight=\"bold\",\n    color=INK_SOFT,\n    ha=\"center\",\n    zorder=5,\n    bbox={\"boxstyle\": \"round,pad=0.35\", \"facecolor\": ELEVATED_BG, \"edgecolor\": INK_SOFT, \"alpha\": 0.9},\n)\n\n# Colorbar with theme-adaptive styling\nsm = ScalarMappable(cmap=imprint_seq, norm=norm)\nsm.set_array([])\ncbar_ax = fig.add_axes([0.79, 0.18, 0.018, 0.52])\ncbar = fig.colorbar(sm, cax=cbar_ax)\ncbar.set_label(\"GDP per Capita (USD)\", fontsize=11, labelpad=12, color=INK)\ncbar.ax.tick_params(labelsize=9, length=0, pad=5, labelcolor=INK_SOFT)\ncbar.ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"${x / 1000:.0f}K\"))\ncbar.outline.set_linewidth(0.3)\ncbar.outline.set_edgecolor(INK_SOFT)\n\n# Size legend\nlegend_x = -67.5\nlegend_y_base = 28.0\nax.text(\n    legend_x, legend_y_base + 11.5, \"Population\", fontsize=10, fontweight=\"bold\", ha=\"center\", va=\"center\", color=INK\n)\nlegend_pops = [30000, 10000, 1000]\nlegend_labels = [\"30M\", \"10M\", \"1M\"]\nlegend_radii = [np.sqrt(p / max_pop) * 2.8 for p in legend_pops]\n\ny_pos = legend_y_base\nfor r, label in zip(legend_radii, legend_labels, strict=True):\n    circle_y = y_pos + r\n    legend_circle = mpatches.Circle(\n        (legend_x - 1.5, circle_y), r, facecolor=ELEVATED_BG, edgecolor=INK_SOFT, linewidth=0.8, zorder=2\n    )\n    ax.add_patch(legend_circle)\n    ax.text(legend_x + 1.8, circle_y, label, fontsize=9, va=\"center\", ha=\"left\", color=INK_SOFT)\n    y_pos += r * 2 + 1.0\n\n# Reference map inset (bottom-left corner for geographic comparison)\ninset_ax = fig.add_axes([0.03, 0.08, 0.14, 0.22])\nus_lon = np.array(\n    [\n        -124,\n        -117,\n        -111,\n        -109,\n        -109,\n        -114,\n        -120,\n        -124,\n        -124,\n        -122,\n        -117,\n        -104,\n        -104,\n        -100,\n        -97,\n        -94,\n        -90,\n        -89,\n        -82,\n        -81,\n        -81,\n        -80,\n        -75,\n        -70,\n        -67,\n        -67,\n        -71,\n        -74,\n        -76,\n        -80,\n        -82,\n        -85,\n        -88,\n        -90,\n        -90,\n        -94,\n        -97,\n        -97,\n        -100,\n        -104,\n        -109,\n        -111,\n        -117,\n        -120,\n        -124,\n    ]\n)\nus_lat = np.array(\n    [\n        42,\n        37,\n        32,\n        32,\n        37,\n        42,\n        46,\n        46,\n        48,\n        49,\n        49,\n        49,\n        43,\n        43,\n        37,\n        33,\n        30,\n        30,\n        30,\n        25,\n        27,\n        32,\n        35,\n        42,\n        44,\n        47,\n        45,\n        41,\n        39,\n        39,\n        35,\n        30,\n        30,\n        29,\n        30,\n        29,\n        26,\n        37,\n        41,\n        41,\n        37,\n        32,\n        33,\n        39,\n        42,\n    ]\n)\ninset_ax.fill(us_lon, us_lat, color=ELEVATED_BG, edgecolor=INK_SOFT, linewidth=0.8, alpha=0.85)\ninset_ax.set_xlim(-128, -64)\ninset_ax.set_ylim(23, 52)\ninset_ax.set_aspect(\"equal\")\ninset_ax.set_title(\"Actual Geography\", fontsize=7, fontweight=\"bold\", color=INK_MUTED, pad=3)\nfor spine in inset_ax.spines.values():\n    spine.set_edgecolor(INK_SOFT)\n    spine.set_linewidth(0.4)\ninset_ax.set_xticks([])\ninset_ax.set_yticks([])\ninset_ax.patch.set_facecolor(PAGE_BG)\n\n# Main axes styling\nax.set_xlim(-128, -63)\nax.set_ylim(24, 51)\nax.set_aspect(\"equal\")\nfor spine in ax.spines.values():\n    spine.set_visible(False)\nax.set_xticks([])\nax.set_yticks([])\n\n# Title — full format: {Descriptive} · {spec-id} · {language} · {library} · anyplot.ai\ntitle = \"US States by Population · cartogram-area-distortion · python · matplotlib · anyplot.ai\"\nn = len(title)\ntitle_fontsize = max(8, round(12 * 67 / n)) if n > 67 else 12\nfig.text(0.40, 0.965, title, fontsize=title_fontsize, fontweight=\"medium\", ha=\"center\", va=\"center\", color=INK)\n\n# Caption\nfig.text(\n    0.40,\n    0.02,\n    \"Circle area ∝ state population  |  Color encodes GDP per capita\",\n    fontsize=8,\n    ha=\"center\",\n    va=\"center\",\n    color=INK_MUTED,\n    style=\"italic\",\n)\n\n# Save\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}