{"spec_id":"cartogram-area-distortion","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// cartogram-area-distortion: Cartogram with Area Distortion by Data Value\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 80/100 | Created: 2026-06-08\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\nfunction hexToRgba(hex, a) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r},${g},${b},${a})`;\n}\n\n// Interpolate along imprint_seq: #009E73 (green) → #4467A3 (blue)\nfunction seqColor(v) {\n  const c1 = t.seq[0];\n  const c2 = t.seq[1];\n  const r1 = parseInt(c1.slice(1, 3), 16), g1 = parseInt(c1.slice(3, 5), 16), b1 = parseInt(c1.slice(5, 7), 16);\n  const r2 = parseInt(c2.slice(1, 3), 16), g2 = parseInt(c2.slice(3, 5), 16), b2 = parseInt(c2.slice(5, 7), 16);\n  return `rgba(${Math.round(r1 + (r2 - r1) * v)},${Math.round(g1 + (g2 - g1) * v)},${Math.round(b1 + (b2 - b1) * v)},0.88)`;\n}\n\n// US contiguous 48 states: [abbr, longitude, latitude, population_millions_2023]\nconst stateRaw = [\n  [\"AL\", -86.9, 32.8, 5.0],  [\"AZ\", -111.9, 34.3, 7.3],  [\"AR\", -92.4, 34.8, 3.0],\n  [\"CA\", -119.4, 37.3, 39.2], [\"CO\", -105.3, 39.0, 5.8],  [\"CT\", -72.7, 41.6, 3.6],\n  [\"DE\", -75.5, 38.9, 1.0],  [\"FL\", -81.6, 28.1, 22.6],  [\"GA\", -83.4, 32.8, 10.9],\n  [\"ID\", -114.7, 44.4, 1.9], [\"IL\", -89.2, 40.0, 12.6],  [\"IN\", -86.1, 40.3, 6.8],\n  [\"IA\", -93.1, 42.0, 3.2],  [\"KS\", -98.4, 38.5, 2.9],   [\"KY\", -84.9, 37.5, 4.5],\n  [\"LA\", -91.8, 31.2, 4.6],  [\"ME\", -69.4, 45.2, 1.4],   [\"MD\", -77.0, 39.0, 6.2],\n  [\"MA\", -71.5, 42.3, 7.0],  [\"MI\", -84.5, 44.2, 10.0],  [\"MN\", -93.3, 45.7, 5.7],\n  [\"MS\", -89.7, 32.7, 3.0],  [\"MO\", -91.8, 38.5, 6.2],   [\"MT\", -110.5, 46.9, 1.1],\n  [\"NE\", -99.9, 41.5, 2.0],  [\"NV\", -116.4, 38.5, 3.2],  [\"NH\", -71.6, 43.7, 1.4],\n  [\"NJ\", -74.4, 40.1, 9.3],  [\"NM\", -106.1, 34.5, 2.1],  [\"NY\", -75.4, 42.8, 19.6],\n  [\"NC\", -79.8, 35.6, 10.7], [\"ND\", -100.5, 47.5, 0.8],  [\"OH\", -82.8, 40.4, 11.8],\n  [\"OK\", -97.5, 35.6, 4.0],  [\"OR\", -120.6, 44.0, 4.2],  [\"PA\", -77.2, 40.9, 13.0],\n  [\"RI\", -71.5, 41.7, 1.1],  [\"SC\", -80.9, 33.8, 5.3],   [\"SD\", -100.3, 44.5, 0.9],\n  [\"TN\", -86.3, 35.8, 7.1],  [\"TX\", -99.3, 31.5, 30.0],  [\"UT\", -111.1, 39.3, 3.4],\n  [\"VT\", -72.7, 44.0, 0.6],  [\"VA\", -78.2, 37.5, 8.7],   [\"WA\", -120.5, 47.4, 7.8],\n  [\"WV\", -80.6, 38.6, 1.8],  [\"WI\", -89.6, 44.3, 5.9],   [\"WY\", -107.5, 43.0, 0.6],\n];\n\nconst maxPop = Math.max(...stateRaw.map(d => d[3]));\nconst minPop = Math.min(...stateRaw.map(d => d[3]));\nconst R_MAX = 45;\n\nconst points = stateRaw.map(([abbr, lon, lat, pop]) => ({\n  x: lon,\n  y: lat,\n  r: Math.max(8, R_MAX * Math.sqrt(pop / maxPop)),  // floor at 8 so tiny states remain identifiable\n  label: abbr,\n  pop,\n  norm: (pop - minPop) / (maxPop - minPop),\n}));\n\n// Ascending order: small states render first, large states on top — prevents large bubbles occluding small neighbours\nconst sortedPoints = [...points].sort((a, b) => a.pop - b.pop);\n\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// Draw state abbreviations centred inside each bubble\nconst labelPlugin = {\n  id: \"stateLabels\",\n  afterDatasetsDraw(chart) {\n    const ctx = chart.ctx;\n    const meta = chart.getDatasetMeta(0);\n    const ds = chart.data.datasets[0];\n    ctx.save();\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    meta.data.forEach((pt, i) => {\n      const { r, label } = ds.data[i];\n      if (r < 12) return;\n      const fs = Math.max(10, Math.min(Math.round(r * 0.58), 18));\n      ctx.font = `bold ${fs}px sans-serif`;\n      ctx.shadowColor = \"rgba(0,0,0,0.55)\";\n      ctx.shadowBlur = 3;\n      ctx.fillStyle = \"#FFFFFF\";\n      ctx.fillText(label, pt.x, pt.y);\n    });\n    ctx.restore();\n  },\n};\n\n// Gradient colour legend + bubble-size reference (top-right corner)\nconst gradientLegendPlugin = {\n  id: \"gradientLegend\",\n  afterDraw(chart) {\n    const ctx = chart.ctx;\n    const { right, top } = chart.chartArea;\n    ctx.save();\n    ctx.shadowColor = \"transparent\";\n    ctx.shadowBlur = 0;\n\n    const lw = 158;\n    const lx = right - lw - 14;\n    const ly = top + 14;\n\n    // Box background (tall enough for gradient + size reference)\n    ctx.fillStyle = hexToRgba(t.elevatedBg, 0.92);\n    ctx.strokeStyle = hexToRgba(t.inkSoft, 0.35);\n    ctx.lineWidth = 1;\n    ctx.beginPath();\n    ctx.roundRect(lx - 10, ly - 6, lw + 20, 122, 7);\n    ctx.fill();\n    ctx.stroke();\n\n    // Title\n    ctx.fillStyle = t.ink;\n    ctx.font = \"bold 14px sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.fillText(\"Population (2023)\", lx, ly + 10);\n\n    // Gradient bar\n    const barH = 13;\n    const barY = ly + 24;\n    const grad = ctx.createLinearGradient(lx, 0, lx + lw, 0);\n    grad.addColorStop(0, t.seq[0]);\n    grad.addColorStop(1, t.seq[1]);\n    ctx.fillStyle = grad;\n    ctx.beginPath();\n    ctx.roundRect(lx, barY, lw, barH, 3);\n    ctx.fill();\n\n    // Min / max labels\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = \"13px sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.fillText(`${minPop.toFixed(1)}M`, lx, barY + barH + 15);\n    ctx.textAlign = \"right\";\n    ctx.fillText(`${maxPop.toFixed(1)}M`, lx + lw, barY + barH + 15);\n\n    // Size reference header\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = \"11px sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.fillText(\"Bubble area ∝ pop:\", lx, barY + barH + 34);\n\n    // Reference circles at three population benchmarks, coloured by the same gradient\n    const sizeRefs = [\n      { pop: 1,      label: \"1M\",                 norm: (1 - minPop) / (maxPop - minPop) },\n      { pop: 10,     label: \"10M\",                norm: (10 - minPop) / (maxPop - minPop) },\n      { pop: maxPop, label: `${maxPop.toFixed(0)}M`, norm: 1 },\n    ];\n    const refScale = 14; // max reference circle radius (CSS px) corresponds to maxPop\n    const sizeY = barY + barH + 54;\n\n    let refX = lx;\n    sizeRefs.forEach(({ pop, label, norm }) => {\n      const r = Math.max(4, refScale * Math.sqrt(pop / maxPop));\n      const cx = refX + 8 + r;\n      ctx.fillStyle = seqColor(norm);\n      ctx.beginPath();\n      ctx.arc(cx, sizeY, r, 0, Math.PI * 2);\n      ctx.fill();\n      ctx.strokeStyle = hexToRgba(t.inkSoft, 0.3);\n      ctx.lineWidth = 0.5;\n      ctx.stroke();\n      ctx.fillStyle = t.inkSoft;\n      ctx.font = \"10px sans-serif\";\n      ctx.textAlign = \"center\";\n      ctx.fillText(label, cx, sizeY + r + 9);\n      refX = cx + r;\n    });\n\n    ctx.restore();\n  },\n};\n\nnew Chart(canvas, {\n  type: \"bubble\",\n  plugins: [labelPlugin, gradientLegendPlugin],\n  data: {\n    datasets: [{\n      label: \"Population\",\n      data: sortedPoints,\n      backgroundColor: sortedPoints.map(p => seqColor(p.norm)),\n      borderColor: sortedPoints.map(p => hexToRgba(t.inkSoft, 0.25)),\n      borderWidth: 0.5,\n    }],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"cartogram-area-distortion · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 32, weight: \"bold\" },\n        padding: { top: 14, bottom: 4 },\n      },\n      subtitle: {\n        display: true,\n        text: \"US States — Dorling Cartogram: bubble area and color ∝ population (2023)\",\n        color: t.inkSoft,\n        font: { size: 20 },\n        padding: { bottom: 12 },\n      },\n      legend: { display: false },\n      tooltip: {\n        backgroundColor: hexToRgba(t.elevatedBg, 0.95),\n        titleColor: t.ink,\n        bodyColor: t.inkSoft,\n        borderColor: hexToRgba(t.inkSoft, 0.4),\n        borderWidth: 1,\n        callbacks: {\n          title: ctx => ctx[0].raw.label,\n          label: ctx => `Population: ${ctx.raw.pop.toFixed(1)}M`,\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: -128,\n        max: -64,\n        ticks: { display: false },\n        grid: { display: false },\n        border: { display: false },\n      },\n      y: {\n        type: \"linear\",\n        min: 24,\n        max: 50,\n        ticks: { display: false },\n        grid: { display: false },\n        border: { display: false },\n      },\n    },\n  },\n});\n"}