{"spec_id":"cartogram-area-distortion","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// cartogram-area-distortion: Cartogram with Area Distortion by Data Value\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-08-20\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// US states on an approximate tile grid (col = west→east, row = north→south).\n// Every state occupies a fixed-size, contiguous reference tile (edges touch\n// its grid neighbors); the circle drawn inside each tile has area ∝ population,\n// so its size relative to the tile is the area distortion. Fill encodes a\n// GDP-per-capita tier. population in millions (2023 est.), gdpPerCapita in\n// thousand USD.\nconst states = [\n  { name: \"WA\", col: 0, row: 0, population: 7.9, gdpPerCapita: 87 },\n  { name: \"OR\", col: 0, row: 1, population: 4.2, gdpPerCapita: 71 },\n  { name: \"CA\", col: 0, row: 2, population: 39.0, gdpPerCapita: 91 },\n  { name: \"NV\", col: 1, row: 2, population: 3.2, gdpPerCapita: 66 },\n  { name: \"MT\", col: 2, row: 0, population: 1.1, gdpPerCapita: 62 },\n  { name: \"UT\", col: 2, row: 2, population: 3.4, gdpPerCapita: 70 },\n  { name: \"AZ\", col: 1, row: 3, population: 7.4, gdpPerCapita: 55 },\n  { name: \"CO\", col: 3, row: 2, population: 5.9, gdpPerCapita: 75 },\n  { name: \"ND\", col: 4, row: 0, population: 0.8, gdpPerCapita: 78 },\n  { name: \"SD\", col: 4, row: 1, population: 0.9, gdpPerCapita: 68 },\n  { name: \"NE\", col: 4, row: 2, population: 2.0, gdpPerCapita: 72 },\n  { name: \"KS\", col: 4, row: 3, population: 2.9, gdpPerCapita: 65 },\n  { name: \"TX\", col: 4, row: 4, population: 30.5, gdpPerCapita: 74 },\n  { name: \"MN\", col: 5, row: 0, population: 5.7, gdpPerCapita: 77 },\n  { name: \"IA\", col: 5, row: 1, population: 3.2, gdpPerCapita: 65 },\n  { name: \"MO\", col: 5, row: 2, population: 6.2, gdpPerCapita: 60 },\n  { name: \"OK\", col: 5, row: 3, population: 4.0, gdpPerCapita: 58 },\n  { name: \"WI\", col: 6, row: 0, population: 5.9, gdpPerCapita: 65 },\n  { name: \"IL\", col: 6, row: 1, population: 12.6, gdpPerCapita: 79 },\n  { name: \"AR\", col: 6, row: 3, population: 3.0, gdpPerCapita: 50 },\n  { name: \"LA\", col: 6, row: 4, population: 4.6, gdpPerCapita: 58 },\n  { name: \"MI\", col: 7, row: 0, population: 10.0, gdpPerCapita: 60 },\n  { name: \"IN\", col: 7, row: 1, population: 6.8, gdpPerCapita: 63 },\n  { name: \"TN\", col: 7, row: 2, population: 7.1, gdpPerCapita: 60 },\n];\n\nconst maxPopulation = Math.max(...states.map((s) => s.population));\nconst MIN_RADIUS = 14;\nconst MAX_RADIUS = 60;\n\n// Area ∝ value → radius ∝ sqrt(value), the defining cartogram encoding.\nfunction radiusFor(population) {\n  return MIN_RADIUS + (MAX_RADIUS - MIN_RADIUS) * Math.sqrt(population / maxPopulation);\n}\n\nfunction toPoint(s) {\n  return {\n    name: s.name,\n    x: s.col,\n    y: s.row,\n    marker: { radius: radiusFor(s.population) },\n    custom: { population: s.population, gdpPerCapita: s.gdpPerCapita },\n  };\n}\n\n// The core bundle has no colorAxis module, so the GDP-per-capita tiers are\n// rendered as three ordinary series — each gets its own imprint_seq-derived\n// color and a native legend entry, without needing a continuous color axis.\nfunction lerpHex(hexA, hexB, frac) {\n  const a = [1, 3, 5].map((i) => parseInt(hexA.slice(i, i + 2), 16));\n  const b = [1, 3, 5].map((i) => parseInt(hexB.slice(i, i + 2), 16));\n  const mixed = a.map((c, i) => Math.round(c + (b[i] - c) * frac));\n  return \"#\" + mixed.map((c) => c.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nconst tiers = [\n  { key: \"low\", name: \"GDP per capita < $60k\", test: (g) => g < 60, color: t.seq[0] },\n  { key: \"mid\", name: \"GDP per capita $60–74k\", test: (g) => g >= 60 && g < 75, color: lerpHex(t.seq[0], t.seq[1], 0.5) },\n  { key: \"high\", name: \"GDP per capita ≥ $75k\", test: (g) => g >= 75, color: t.seq[1] },\n];\n\nconst series = tiers.map((tier) => ({\n  name: tier.name,\n  color: tier.color,\n  data: states.filter((s) => tier.test(s.gdpPerCapita)).map(toPoint),\n}));\n\n// Reference tile: same fixed footprint for every state, so its edges line up\n// contiguously (touching neighbors) with the grid unit — the \"undistorted\"\n// baseline the population-scaled circle is measured against. Legend entry\n// only; the tiles themselves are drawn via the SVG renderer (below).\nseries.push({\n  name: \"Reference tile (equal-area baseline, contiguous grid)\",\n  color: t.inkSoft,\n  marker: { symbol: \"square\", radius: 7, fillColor: \"transparent\", lineColor: t.inkSoft, lineWidth: 1.5 },\n  enableMouseTracking: false,\n  showInLegend: true,\n  data: [],\n});\n\n// Draws one fixed-size square per state, aligned to the grid so adjacent\n// tiles' edges touch — a contiguous baseline grid the distorted circles sit\n// against, making the area distortion legible (spec requires a reference for\n// comparison). Re-run on every render/resize since pixel positions depend on\n// the current axis-to-pixel mapping.\nfunction drawReferenceTiles(chart) {\n  if (chart._tileGroup) chart._tileGroup.destroy();\n  const group = chart.renderer.g(\"reference-tiles\").attr({ zIndex: 1 }).add();\n  chart._tileGroup = group;\n\n  const xa = chart.xAxis[0];\n  const ya = chart.yAxis[0];\n  const unitW = Math.abs(xa.toPixels(1) - xa.toPixels(0));\n  const unitH = Math.abs(ya.toPixels(1) - ya.toPixels(0));\n  const tile = Math.min(unitW, unitH) * 0.94;\n  const half = tile / 2;\n\n  states.forEach((s) => {\n    const cx = xa.toPixels(s.col);\n    const cy = ya.toPixels(s.row);\n    chart.renderer\n      .rect(cx - half, cy - half, tile, tile, 2)\n      .attr({ fill: \"transparent\", stroke: t.inkSoft, \"stroke-width\": 1.25, zIndex: 1 })\n      .add(group);\n  });\n}\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: { render: function () { drawReferenceTiles(this); } },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"cartogram-area-distortion · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"US states resized by population against a contiguous equal-area reference grid\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  caption: {\n    text: \"Circle area ∝ population (millions) · fill ∝ GDP-per-capita tier · faint square = undistorted reference tile, same size for every state · position preserves rough state geography\",\n    style: { color: t.inkMuted || t.inkSoft, fontSize: \"13px\" },\n  },\n  xAxis: {\n    min: -1,\n    max: 8,\n    visible: false,\n  },\n  yAxis: {\n    min: -1,\n    max: 5,\n    reversed: true,\n    title: { text: null },\n    visible: false,\n  },\n  legend: {\n    enabled: true,\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  plotOptions: {\n    series: { animation: false },\n    scatter: {\n      marker: { symbol: \"circle\" },\n      dataLabels: {\n        enabled: true,\n        format: \"{point.name}\",\n        style: {\n          color: t.ink,\n          fontSize: \"13px\",\n          fontWeight: \"600\",\n          textOutline: \"none\",\n        },\n      },\n    },\n  },\n  tooltip: {\n    pointFormat:\n      \"Population: {point.custom.population}M<br/>GDP per capita: ${point.custom.gdpPerCapita}k\",\n  },\n  series,\n});\n"}