{"spec_id":"cartogram-area-distortion","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ncartogram-area-distortion: Cartogram with Area Distortion by Data Value\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-06-08\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove '' and this file's directory from sys.path so 'import altair' resolves\n# to the installed package, not this file (which shares the library's name)\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if p not in (\"\", _here)]\n\nimport altair as alt\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens (Imprint palette — see prompts/default-style-guide.md)\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\"\nREF_FILL = \"#EEEBE3\" if THEME == \"light\" else \"#252521\"\nREF_STROKE = \"#CCCAC0\" if THEME == \"light\" else \"#3A3A36\"\n\n# Imprint palette — positions 1-4 for four US Census regions\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nregion_order = [\"Northeast\", \"Midwest\", \"South\", \"West\"]\nregion_colors = IMPRINT_PALETTE[:4]\n\n# Data - US states (2023 population estimates, millions) and geographic centroids\nstates = pd.DataFrame(\n    [\n        {\"state\": \"AL\", \"name\": \"Alabama\", \"pop\": 5.1, \"lat\": 32.8, \"lon\": -86.8, \"region\": \"South\"},\n        {\"state\": \"AK\", \"name\": \"Alaska\", \"pop\": 0.7, \"lat\": 64.2, \"lon\": -153.5, \"region\": \"West\"},\n        {\"state\": \"AZ\", \"name\": \"Arizona\", \"pop\": 7.4, \"lat\": 34.3, \"lon\": -111.7, \"region\": \"West\"},\n        {\"state\": \"AR\", \"name\": \"Arkansas\", \"pop\": 3.0, \"lat\": 34.9, \"lon\": -92.4, \"region\": \"South\"},\n        {\"state\": \"CA\", \"name\": \"California\", \"pop\": 38.9, \"lat\": 37.2, \"lon\": -119.5, \"region\": \"West\"},\n        {\"state\": \"CO\", \"name\": \"Colorado\", \"pop\": 5.9, \"lat\": 39.0, \"lon\": -105.5, \"region\": \"West\"},\n        {\"state\": \"CT\", \"name\": \"Connecticut\", \"pop\": 3.6, \"lat\": 41.6, \"lon\": -72.7, \"region\": \"Northeast\"},\n        {\"state\": \"DE\", \"name\": \"Delaware\", \"pop\": 1.0, \"lat\": 38.2, \"lon\": -74.8, \"region\": \"South\"},\n        {\"state\": \"FL\", \"name\": \"Florida\", \"pop\": 22.6, \"lat\": 28.6, \"lon\": -82.5, \"region\": \"South\"},\n        {\"state\": \"GA\", \"name\": \"Georgia\", \"pop\": 11.0, \"lat\": 33.0, \"lon\": -83.5, \"region\": \"South\"},\n        {\"state\": \"HI\", \"name\": \"Hawaii\", \"pop\": 1.4, \"lat\": 20.5, \"lon\": -157.5, \"region\": \"West\"},\n        {\"state\": \"ID\", \"name\": \"Idaho\", \"pop\": 2.0, \"lat\": 44.4, \"lon\": -114.6, \"region\": \"West\"},\n        {\"state\": \"IL\", \"name\": \"Illinois\", \"pop\": 12.5, \"lat\": 40.0, \"lon\": -89.2, \"region\": \"Midwest\"},\n        {\"state\": \"IN\", \"name\": \"Indiana\", \"pop\": 6.9, \"lat\": 39.9, \"lon\": -86.3, \"region\": \"Midwest\"},\n        {\"state\": \"IA\", \"name\": \"Iowa\", \"pop\": 3.2, \"lat\": 42.0, \"lon\": -93.5, \"region\": \"Midwest\"},\n        {\"state\": \"KS\", \"name\": \"Kansas\", \"pop\": 2.9, \"lat\": 38.5, \"lon\": -98.4, \"region\": \"Midwest\"},\n        {\"state\": \"KY\", \"name\": \"Kentucky\", \"pop\": 4.5, \"lat\": 37.8, \"lon\": -85.3, \"region\": \"South\"},\n        {\"state\": \"LA\", \"name\": \"Louisiana\", \"pop\": 4.6, \"lat\": 31.0, \"lon\": -91.8, \"region\": \"South\"},\n        {\"state\": \"ME\", \"name\": \"Maine\", \"pop\": 1.4, \"lat\": 45.4, \"lon\": -69.2, \"region\": \"Northeast\"},\n        {\"state\": \"MD\", \"name\": \"Maryland\", \"pop\": 6.2, \"lat\": 38.5, \"lon\": -76.0, \"region\": \"South\"},\n        {\"state\": \"MA\", \"name\": \"Massachusetts\", \"pop\": 7.0, \"lat\": 42.6, \"lon\": -71.2, \"region\": \"Northeast\"},\n        {\"state\": \"MI\", \"name\": \"Michigan\", \"pop\": 10.0, \"lat\": 43.4, \"lon\": -84.7, \"region\": \"Midwest\"},\n        {\"state\": \"MN\", \"name\": \"Minnesota\", \"pop\": 5.7, \"lat\": 46.3, \"lon\": -94.3, \"region\": \"Midwest\"},\n        {\"state\": \"MS\", \"name\": \"Mississippi\", \"pop\": 2.9, \"lat\": 32.7, \"lon\": -89.7, \"region\": \"South\"},\n        {\"state\": \"MO\", \"name\": \"Missouri\", \"pop\": 6.2, \"lat\": 38.4, \"lon\": -92.5, \"region\": \"Midwest\"},\n        {\"state\": \"MT\", \"name\": \"Montana\", \"pop\": 1.1, \"lat\": 47.0, \"lon\": -109.6, \"region\": \"West\"},\n        {\"state\": \"NE\", \"name\": \"Nebraska\", \"pop\": 2.0, \"lat\": 41.5, \"lon\": -99.8, \"region\": \"Midwest\"},\n        {\"state\": \"NV\", \"name\": \"Nevada\", \"pop\": 3.2, \"lat\": 39.3, \"lon\": -116.6, \"region\": \"West\"},\n        {\"state\": \"NH\", \"name\": \"New Hampshire\", \"pop\": 1.4, \"lat\": 44.2, \"lon\": -71.6, \"region\": \"Northeast\"},\n        {\"state\": \"NJ\", \"name\": \"New Jersey\", \"pop\": 9.3, \"lat\": 40.3, \"lon\": -73.8, \"region\": \"Northeast\"},\n        {\"state\": \"NM\", \"name\": \"New Mexico\", \"pop\": 2.1, \"lat\": 34.5, \"lon\": -106.0, \"region\": \"West\"},\n        {\"state\": \"NY\", \"name\": \"New York\", \"pop\": 19.6, \"lat\": 43.2, \"lon\": -75.5, \"region\": \"Northeast\"},\n        {\"state\": \"NC\", \"name\": \"N. Carolina\", \"pop\": 10.7, \"lat\": 35.6, \"lon\": -79.4, \"region\": \"South\"},\n        {\"state\": \"ND\", \"name\": \"N. Dakota\", \"pop\": 0.8, \"lat\": 47.4, \"lon\": -100.4, \"region\": \"Midwest\"},\n        {\"state\": \"OH\", \"name\": \"Ohio\", \"pop\": 11.8, \"lat\": 40.4, \"lon\": -82.8, \"region\": \"Midwest\"},\n        {\"state\": \"OK\", \"name\": \"Oklahoma\", \"pop\": 4.0, \"lat\": 35.6, \"lon\": -97.4, \"region\": \"South\"},\n        {\"state\": \"OR\", \"name\": \"Oregon\", \"pop\": 4.2, \"lat\": 44.0, \"lon\": -120.5, \"region\": \"West\"},\n        {\"state\": \"PA\", \"name\": \"Pennsylvania\", \"pop\": 13.0, \"lat\": 41.2, \"lon\": -77.8, \"region\": \"Northeast\"},\n        {\"state\": \"RI\", \"name\": \"Rhode Island\", \"pop\": 1.1, \"lat\": 41.4, \"lon\": -70.4, \"region\": \"Northeast\"},\n        {\"state\": \"SC\", \"name\": \"S. Carolina\", \"pop\": 5.4, \"lat\": 34.0, \"lon\": -81.0, \"region\": \"South\"},\n        {\"state\": \"SD\", \"name\": \"S. Dakota\", \"pop\": 0.9, \"lat\": 44.4, \"lon\": -100.2, \"region\": \"Midwest\"},\n        {\"state\": \"TN\", \"name\": \"Tennessee\", \"pop\": 7.1, \"lat\": 35.8, \"lon\": -86.3, \"region\": \"South\"},\n        {\"state\": \"TX\", \"name\": \"Texas\", \"pop\": 30.5, \"lat\": 31.5, \"lon\": -99.4, \"region\": \"South\"},\n        {\"state\": \"UT\", \"name\": \"Utah\", \"pop\": 3.4, \"lat\": 39.3, \"lon\": -111.7, \"region\": \"West\"},\n        {\"state\": \"VT\", \"name\": \"Vermont\", \"pop\": 0.6, \"lat\": 44.1, \"lon\": -72.6, \"region\": \"Northeast\"},\n        {\"state\": \"VA\", \"name\": \"Virginia\", \"pop\": 8.6, \"lat\": 37.5, \"lon\": -78.9, \"region\": \"South\"},\n        {\"state\": \"WA\", \"name\": \"Washington\", \"pop\": 7.8, \"lat\": 47.4, \"lon\": -120.5, \"region\": \"West\"},\n        {\"state\": \"WV\", \"name\": \"W. Virginia\", \"pop\": 1.8, \"lat\": 38.6, \"lon\": -80.6, \"region\": \"South\"},\n        {\"state\": \"WI\", \"name\": \"Wisconsin\", \"pop\": 5.9, \"lat\": 44.6, \"lon\": -89.8, \"region\": \"Midwest\"},\n        {\"state\": \"WY\", \"name\": \"Wyoming\", \"pop\": 0.6, \"lat\": 43.0, \"lon\": -107.5, \"region\": \"West\"},\n    ]\n)\n\nstates = states.sort_values(\"pop\", ascending=False).reset_index(drop=True)\nstates[\"rank\"] = states.index + 1\nstates[\"pop_label\"] = states[\"pop\"].apply(lambda x: f\"{x:.1f}M\")\ntop5 = states.head(5)\nlabeled_states = states[states[\"pop\"] >= 4.0].copy()\n\n# Reference map - faint state outlines for geographic context\nus_topo_url = \"https://cdn.jsdelivr.net/npm/vega-datasets@2/data/us-10m.json\"\nus_states_topo = alt.topo_feature(us_topo_url, \"states\")\n\nbackground = (\n    alt.Chart(us_states_topo).mark_geoshape(fill=REF_FILL, stroke=REF_STROKE, strokeWidth=0.4).project(type=\"albersUsa\")\n)\n\n# Dorling cartogram — single layer with alt.condition to highlight top-5 states\ntop5_names = [\"California\", \"Texas\", \"Florida\", \"New York\", \"Pennsylvania\"]\nis_top5 = alt.FieldOneOfPredicate(field=\"name\", oneOf=top5_names)\n\ncircles = (\n    alt.Chart(states)\n    .mark_circle()\n    .encode(\n        longitude=\"lon:Q\",\n        latitude=\"lat:Q\",\n        size=alt.Size(\n            \"pop:Q\",\n            scale=alt.Scale(domain=[0.5, 40], range=[40, 1800]),\n            legend=alt.Legend(\n                title=\"Population (millions)\",\n                titleFontSize=10,\n                labelFontSize=10,\n                orient=\"bottom-right\",\n                offset=15,\n                values=[1, 5, 10, 20, 35],\n            ),\n        ),\n        color=alt.Color(\n            \"region:N\",\n            scale=alt.Scale(domain=region_order, range=region_colors),\n            legend=alt.Legend(\n                title=\"Region\",\n                titleFontSize=10,\n                labelFontSize=10,\n                symbolSize=200,\n                symbolStrokeWidth=0,\n                orient=\"bottom-left\",\n                offset=15,\n            ),\n        ),\n        opacity=alt.condition(is_top5, alt.value(0.90), alt.value(0.82)),\n        stroke=alt.condition(is_top5, alt.value(INK), alt.value(PAGE_BG)),\n        strokeWidth=alt.condition(is_top5, alt.value(2.0), alt.value(1.2)),\n        tooltip=[\n            alt.Tooltip(\"name:N\", title=\"State\"),\n            alt.Tooltip(\"pop:Q\", title=\"Population (M)\", format=\".1f\"),\n            alt.Tooltip(\"region:N\", title=\"Region\"),\n        ],\n    )\n    .project(type=\"albersUsa\")\n)\n\n# State abbreviation labels for states >= 4M population\nlabels = (\n    alt.Chart(labeled_states)\n    .mark_text(fontSize=11, fontWeight=\"bold\", color=\"#FFFFFF\")\n    .encode(longitude=\"lon:Q\", latitude=\"lat:Q\", text=\"state:N\")\n    .project(type=\"albersUsa\")\n)\n\n# Population values below labels for top 5 states\npop_labels = (\n    alt.Chart(top5)\n    .mark_text(fontSize=10, color=\"#FFFFFF\", dy=16, fontStyle=\"italic\")\n    .encode(longitude=\"lon:Q\", latitude=\"lat:Q\", text=\"pop_label:N\")\n    .project(type=\"albersUsa\")\n)\n\n# Annotation — key insight placed in lower map area\nannotation_data = pd.DataFrame([{\"text\": \"Top 5 states hold 37% of US population\", \"lat\": 25.5, \"lon\": -110.0}])\nannotation = (\n    alt.Chart(annotation_data)\n    .mark_text(fontSize=9, fontStyle=\"italic\", color=INK_MUTED, align=\"left\")\n    .encode(longitude=\"lon:Q\", latitude=\"lat:Q\", text=\"text:N\")\n    .project(type=\"albersUsa\")\n)\n\n# Title with scaled font size for the longer mandated title string\ntitle_str = \"US States by Population · cartogram-area-distortion · python · altair · anyplot.ai\"\nn = len(title_str)\nratio = 67 / n if n > 67 else 1.0\ntitle_fontsize = max(11, round(16 * ratio))\n\n# Combine all layers\nchart = (\n    (background + circles + labels + pop_labels + annotation)\n    .properties(\n        width=620,\n        height=320,\n        title=alt.Title(\n            text=title_str,\n            subtitle=\"Dorling cartogram: circle area ∝ state population — bold outlines mark the 5 most populous states\",\n            fontSize=title_fontsize,\n            subtitleFontSize=10,\n            subtitleColor=INK_SOFT,\n            anchor=\"middle\",\n        ),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure(background=PAGE_BG)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n    .configure_title(color=INK)\n)\n\n# Save PNG\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad to exact 3200×1800 target (vl-convert inner-view padding leaves canvas short)\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        \"Shrink chart width/height values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save interactive HTML\nchart.save(f\"plot-{THEME}.html\")\n"}