{"spec_id":"cartogram-area-distortion","library":"d3","language":"javascript","code":"// anyplot.ai\n// cartogram-area-distortion: Cartogram with Area Distortion by Data Value\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 84/100 | Created: 2026-06-08\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\nconst margin = { top: 130, right: 230, bottom: 60, left: 30 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// US states: name, abbreviation, centroid lon/lat, population 2023 (M), GDP 2022 (B$)\nconst states = [\n  { name: \"Alabama\",        abbr: \"AL\", lon: -86.7,  lat: 32.8, pop: 5.1,  gdp: 271  },\n  { name: \"Alaska\",         abbr: \"AK\", lon: -153.0, lat: 64.2, pop: 0.74, gdp: 61   },\n  { name: \"Arizona\",        abbr: \"AZ\", lon: -111.7, lat: 34.3, pop: 7.4,  gdp: 488  },\n  { name: \"Arkansas\",       abbr: \"AR\", lon: -92.4,  lat: 34.9, pop: 3.1,  gdp: 155  },\n  { name: \"California\",     abbr: \"CA\", lon: -119.7, lat: 37.2, pop: 39.0, gdp: 3890 },\n  { name: \"Colorado\",       abbr: \"CO\", lon: -105.5, lat: 39.0, pop: 5.9,  gdp: 498  },\n  { name: \"Connecticut\",    abbr: \"CT\", lon: -72.7,  lat: 41.6, pop: 3.6,  gdp: 331  },\n  { name: \"Delaware\",       abbr: \"DE\", lon: -75.5,  lat: 39.0, pop: 1.0,  gdp: 91   },\n  { name: \"Florida\",        abbr: \"FL\", lon: -81.5,  lat: 27.8, pop: 22.6, gdp: 1460 },\n  { name: \"Georgia\",        abbr: \"GA\", lon: -83.4,  lat: 32.7, pop: 11.0, gdp: 743  },\n  { name: \"Hawaii\",         abbr: \"HI\", lon: -157.5, lat: 20.3, pop: 1.44, gdp: 96   },\n  { name: \"Idaho\",          abbr: \"ID\", lon: -114.5, lat: 44.4, pop: 2.0,  gdp: 110  },\n  { name: \"Illinois\",       abbr: \"IL\", lon: -89.2,  lat: 40.1, pop: 12.6, gdp: 1023 },\n  { name: \"Indiana\",        abbr: \"IN\", lon: -86.3,  lat: 40.0, pop: 6.9,  gdp: 432  },\n  { name: \"Iowa\",           abbr: \"IA\", lon: -93.5,  lat: 42.1, pop: 3.2,  gdp: 229  },\n  { name: \"Kansas\",         abbr: \"KS\", lon: -98.4,  lat: 38.5, pop: 2.9,  gdp: 207  },\n  { name: \"Kentucky\",       abbr: \"KY\", lon: -84.3,  lat: 37.5, pop: 4.5,  gdp: 263  },\n  { name: \"Louisiana\",      abbr: \"LA\", lon: -91.8,  lat: 31.2, pop: 4.6,  gdp: 279  },\n  { name: \"Maine\",          abbr: \"ME\", lon: -69.2,  lat: 45.4, pop: 1.4,  gdp: 79   },\n  { name: \"Maryland\",       abbr: \"MD\", lon: -76.8,  lat: 39.1, pop: 6.2,  gdp: 511  },\n  { name: \"Massachusetts\",  abbr: \"MA\", lon: -71.8,  lat: 42.3, pop: 7.1,  gdp: 709  },\n  { name: \"Michigan\",       abbr: \"MI\", lon: -84.5,  lat: 44.3, pop: 10.0, gdp: 634  },\n  { name: \"Minnesota\",      abbr: \"MN\", lon: -94.3,  lat: 46.4, pop: 5.7,  gdp: 501  },\n  { name: \"Mississippi\",    abbr: \"MS\", lon: -89.7,  lat: 32.7, pop: 3.0,  gdp: 139  },\n  { name: \"Missouri\",       abbr: \"MO\", lon: -92.6,  lat: 38.4, pop: 6.2,  gdp: 395  },\n  { name: \"Montana\",        abbr: \"MT\", lon: -109.6, lat: 47.0, pop: 1.1,  gdp: 68   },\n  { name: \"Nebraska\",       abbr: \"NE\", lon: -99.9,  lat: 41.5, pop: 2.0,  gdp: 163  },\n  { name: \"Nevada\",         abbr: \"NV\", lon: -116.7, lat: 39.5, pop: 3.2,  gdp: 244  },\n  { name: \"New Hampshire\",  abbr: \"NH\", lon: -71.6,  lat: 43.7, pop: 1.4,  gdp: 109  },\n  { name: \"New Jersey\",     abbr: \"NJ\", lon: -74.5,  lat: 40.1, pop: 9.3,  gdp: 748  },\n  { name: \"New Mexico\",     abbr: \"NM\", lon: -106.1, lat: 34.5, pop: 2.1,  gdp: 131  },\n  { name: \"New York\",       abbr: \"NY\", lon: -75.5,  lat: 42.9, pop: 19.8, gdp: 2053 },\n  { name: \"North Carolina\", abbr: \"NC\", lon: -79.4,  lat: 35.6, pop: 10.7, gdp: 714  },\n  { name: \"North Dakota\",   abbr: \"ND\", lon: -100.5, lat: 47.5, pop: 0.78, gdp: 71   },\n  { name: \"Ohio\",           abbr: \"OH\", lon: -82.8,  lat: 40.4, pop: 11.8, gdp: 838  },\n  { name: \"Oklahoma\",       abbr: \"OK\", lon: -97.5,  lat: 35.5, pop: 4.0,  gdp: 239  },\n  { name: \"Oregon\",         abbr: \"OR\", lon: -120.5, lat: 43.9, pop: 4.3,  gdp: 347  },\n  { name: \"Pennsylvania\",   abbr: \"PA\", lon: -77.2,  lat: 40.6, pop: 13.1, gdp: 913  },\n  { name: \"Rhode Island\",   abbr: \"RI\", lon: -71.5,  lat: 41.7, pop: 1.1,  gdp: 79   },\n  { name: \"South Carolina\", abbr: \"SC\", lon: -80.9,  lat: 33.8, pop: 5.3,  gdp: 281  },\n  { name: \"South Dakota\",   abbr: \"SD\", lon: -100.3, lat: 44.4, pop: 0.91, gdp: 71   },\n  { name: \"Tennessee\",      abbr: \"TN\", lon: -86.3,  lat: 35.9, pop: 7.1,  gdp: 481  },\n  { name: \"Texas\",          abbr: \"TX\", lon: -99.3,  lat: 31.5, pop: 30.1, gdp: 2355 },\n  { name: \"Utah\",           abbr: \"UT\", lon: -111.1, lat: 39.3, pop: 3.4,  gdp: 270  },\n  { name: \"Vermont\",        abbr: \"VT\", lon: -72.7,  lat: 44.0, pop: 0.65, gdp: 40   },\n  { name: \"Virginia\",       abbr: \"VA\", lon: -79.4,  lat: 37.5, pop: 8.7,  gdp: 704  },\n  { name: \"Washington\",     abbr: \"WA\", lon: -120.5, lat: 47.4, pop: 7.9,  gdp: 785  },\n  { name: \"West Virginia\",  abbr: \"WV\", lon: -80.6,  lat: 38.6, pop: 1.8,  gdp: 89   },\n  { name: \"Wisconsin\",      abbr: \"WI\", lon: -90.0,  lat: 44.6, pop: 5.9,  gdp: 433  },\n  { name: \"Wyoming\",        abbr: \"WY\", lon: -107.6, lat: 43.0, pop: 0.58, gdp: 46   },\n  { name: \"D.C.\",           abbr: \"DC\", lon: -77.0,  lat: 38.9, pop: 0.69, gdp: 162  },\n];\n\n// GDP per capita in USD\nstates.forEach(d => { d.gdpPerCapita = (d.gdp / d.pop) * 1000; });\n\n// AlbersUSA composite projection handles AK + HI insets automatically\nconst geoFeatures = {\n  type: \"FeatureCollection\",\n  features: states.map(d => ({\n    type: \"Feature\",\n    geometry: { type: \"Point\", coordinates: [d.lon, d.lat] },\n  })),\n};\n\nconst projection = d3.geoAlbersUsa().fitExtent(\n  [[0, 12], [iw, ih]],\n  geoFeatures\n);\n\nstates.forEach(d => {\n  const p = projection([d.lon, d.lat]);\n  d.x = p ? p[0] : iw / 2;\n  d.y = p ? p[1] : ih / 2;\n});\n\n// Radius scale: area ∝ population (Dorling cartogram style)\nconst maxPop = d3.max(states, d => d.pop);\nconst maxR = 70;\nconst rScale = d3.scaleSqrt().domain([0, maxPop]).range([0, maxR]);\n\n// Save projected positions as attraction anchors\nstates.forEach(d => { d.x0 = d.x; d.y0 = d.y; });\n\n// Force simulation: pull circles toward geographic centroids, repel overlaps\nconst sim = d3.forceSimulation(states)\n  .force(\"attract-x\", d3.forceX(d => d.x0).strength(0.65))\n  .force(\"attract-y\", d3.forceY(d => d.y0).strength(0.65))\n  .force(\"collide\", d3.forceCollide(d => rScale(d.pop) + 2).strength(1.0))\n  .stop();\n\nfor (let i = 0; i < 350; i++) sim.tick();\n\n// Color: GDP per capita using Imprint sequential scale (green → blue)\nconst gdpPCValues = states.map(d => d.gdpPerCapita);\nconst minGdpPC = d3.min(gdpPCValues);\nconst maxGdpPC = d3.max(gdpPCValues);\nconst colorScale = d3.scaleSequential(\n  d3.interpolateRgbBasis(t.seq)\n).domain([minGdpPC, maxGdpPC]);\n\n// SVG\nconst svg = d3.select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\nconst g = svg.append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Circles — sorted by size descending so small circles render on top\nconst sortedStates = [...states].sort((a, b) => b.pop - a.pop);\n\ng.selectAll(\"circle.state\")\n  .data(sortedStates)\n  .join(\"circle\")\n  .attr(\"class\", \"state\")\n  .attr(\"cx\", d => d.x)\n  .attr(\"cy\", d => d.y)\n  .attr(\"r\", d => rScale(d.pop))\n  .attr(\"fill\", d => colorScale(d.gdpPerCapita))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// Abbreviation labels: threshold lowered to 8px; AK always labeled\ng.selectAll(\"text.abbr\")\n  .data(states.filter(d => rScale(d.pop) >= 8 || d.abbr === \"AK\"))\n  .join(\"text\")\n  .attr(\"class\", \"abbr\")\n  .attr(\"x\", d => d.x)\n  .attr(\"y\", d => d.y)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"central\")\n  .attr(\"fill\", t.pageBg)\n  .style(\"font-size\", d => `${Math.max(9, Math.min(16, rScale(d.pop) * 0.44))}px`)\n  .style(\"font-weight\", \"700\")\n  .style(\"pointer-events\", \"none\")\n  .text(d => d.abbr);\n\n// --- Geographic reference inset (bottom-right of inner plot) ---\n// Shows states at equal size by actual geographic position — contrast with cartogram\nconst insetW = 270, insetH = 155;\nconst insetX = iw - insetW - 4;\nconst insetY = ih - insetH - 4;\n\nconst insetProj = d3.geoAlbersUsa().fitExtent(\n  [[6, 6], [insetW - 6, insetH - 22]],\n  geoFeatures\n);\n\ng.append(\"rect\")\n  .attr(\"x\", insetX)\n  .attr(\"y\", insetY)\n  .attr(\"width\", insetW)\n  .attr(\"height\", insetH)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1)\n  .attr(\"rx\", 4)\n  .attr(\"opacity\", 0.95);\n\ng.append(\"text\")\n  .attr(\"x\", insetX + insetW / 2)\n  .attr(\"y\", insetY + insetH - 5)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-style\", \"italic\")\n  .text(\"Geographic reference (equal-size dots)\");\n\nstates.forEach(d => {\n  const p = insetProj([d.lon, d.lat]);\n  if (!p) return;\n  g.append(\"circle\")\n    .attr(\"cx\", insetX + p[0])\n    .attr(\"cy\", insetY + p[1])\n    .attr(\"r\", 4)\n    .attr(\"fill\", colorScale(d.gdpPerCapita))\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 0.8);\n});\n\n// Title (scaled for length)\nconst titleStr = \"US Population Cartogram · cartogram-area-distortion · javascript · d3 · anyplot.ai\";\nconst titlePx = Math.round(22 * Math.min(1, 67 / titleStr.length));\n\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titlePx}px`)\n  .style(\"font-weight\", \"600\")\n  .text(titleStr);\n\n// Subtitle — moved to y=96 to clear WA circle overlap; font bumped to 14px\nsvg.append(\"text\")\n  .attr(\"x\", margin.left)\n  .attr(\"y\", 96)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Circle area ∝ population (2023, millions) · Color = GDP per capita (USD, 2022)\");\n\n// --- Color legend ---\nconst legX = margin.left + iw + 28;\nconst legY = margin.top + 24;\nconst legH = 190;\nconst legW = 16;\n\nconst defs = svg.append(\"defs\");\nconst grad = defs.append(\"linearGradient\")\n  .attr(\"id\", \"cleg-grad\")\n  .attr(\"x1\", \"0%\").attr(\"y1\", \"100%\")\n  .attr(\"x2\", \"0%\").attr(\"y2\", \"0%\");\n\n[0, 0.25, 0.5, 0.75, 1].forEach(s => {\n  grad.append(\"stop\")\n    .attr(\"offset\", `${s * 100}%`)\n    .attr(\"stop-color\", colorScale(minGdpPC + s * (maxGdpPC - minGdpPC)));\n});\n\nsvg.append(\"rect\")\n  .attr(\"x\", legX)\n  .attr(\"y\", legY)\n  .attr(\"width\", legW)\n  .attr(\"height\", legH)\n  .attr(\"fill\", \"url(#cleg-grad)\")\n  .attr(\"rx\", 2);\n\nconst cLegScale = d3.scaleLinear().domain([minGdpPC, maxGdpPC]).range([legH, 0]);\nconst cAxis = d3.axisRight(cLegScale)\n  .ticks(5)\n  .tickFormat(d => `$${Math.round(d / 1000)}k`);\n\nconst cAxisG = svg.append(\"g\")\n  .attr(\"transform\", `translate(${legX + legW}, ${legY})`)\n  .call(cAxis);\n\ncAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\ncAxisG.selectAll(\"line\").attr(\"stroke\", t.inkSoft).attr(\"stroke-opacity\", 0.5);\ncAxisG.select(\".domain\").remove();\n\nsvg.append(\"text\")\n  .attr(\"x\", legX)\n  .attr(\"y\", legY - 12)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"GDP / capita\");\n\n// --- Size legend ---\nconst szLegY = legY + legH + 52;\n\nsvg.append(\"text\")\n  .attr(\"x\", legX)\n  .attr(\"y\", szLegY - 12)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Population\");\n\nconst szVals = [1, 10, 30];\nconst szCx = legX + maxR + 4;\nlet szY = szLegY;\n\nszVals.forEach(v => {\n  const r = rScale(v);\n  szY += r;\n  svg.append(\"circle\")\n    .attr(\"cx\", szCx)\n    .attr(\"cy\", szY)\n    .attr(\"r\", r)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1)\n    .attr(\"stroke-dasharray\", \"3,2\");\n  svg.append(\"line\")\n    .attr(\"x1\", szCx + r)\n    .attr(\"y1\", szY)\n    .attr(\"x2\", szCx + r + 6)\n    .attr(\"y2\", szY)\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1);\n  svg.append(\"text\")\n    .attr(\"x\", szCx + r + 10)\n    .attr(\"y\", szY + 4)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"13px\")\n    .text(`${v}M`);\n  szY += r + 6;\n});\n"}