{"spec_id":"cartogram-area-distortion","library":"muix","language":"javascript","code":"// anyplot.ai\n// cartogram-area-distortion: Cartogram with Area Distortion by Data Value\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-08-20\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: 2023 US Census population estimates (millions), plotted at each\n// state's approximate geographic centroid (longitude/latitude). The community\n// @mui/x-charts surface has no polygon/map primitive to distort real state\n// boundaries, so this follows the Dorling-cartogram convention instead: a\n// circle's area (not the true land shape) carries the data value, while\n// positions preserve each state's relative geography and neighbors. ---------\nconst REGIONS = [\"West\", \"Midwest\", \"South\", \"Northeast\"];\n\nconst states = [\n  { name: \"California\", abbr: \"CA\", lon: -119.4, lat: 36.8, population: 38.9, region: \"West\" },\n  { name: \"Texas\", abbr: \"TX\", lon: -99.3, lat: 31.0, population: 30.5, region: \"South\" },\n  { name: \"Florida\", abbr: \"FL\", lon: -81.6, lat: 27.8, population: 22.6, region: \"South\" },\n  { name: \"New York\", abbr: \"NY\", lon: -75.5, lat: 43.0, population: 19.6, region: \"Northeast\" },\n  { name: \"Pennsylvania\", abbr: \"PA\", lon: -77.8, lat: 40.9, population: 13.0, region: \"Northeast\" },\n  { name: \"Illinois\", abbr: \"IL\", lon: -89.2, lat: 40.0, population: 12.5, region: \"Midwest\" },\n  { name: \"Ohio\", abbr: \"OH\", lon: -82.8, lat: 40.4, population: 11.8, region: \"Midwest\" },\n  { name: \"Georgia\", abbr: \"GA\", lon: -83.5, lat: 32.6, population: 11.0, region: \"South\" },\n  { name: \"North Carolina\", abbr: \"NC\", lon: -79.4, lat: 35.5, population: 10.8, region: \"South\" },\n  { name: \"Michigan\", abbr: \"MI\", lon: -85.0, lat: 44.3, population: 10.0, region: \"Midwest\" },\n  { name: \"Virginia\", abbr: \"VA\", lon: -78.2, lat: 37.5, population: 8.7, region: \"South\" },\n  { name: \"Washington\", abbr: \"WA\", lon: -120.4, lat: 47.0, population: 7.8, region: \"West\" },\n  { name: \"Arizona\", abbr: \"AZ\", lon: -111.4, lat: 34.0, population: 7.4, region: \"West\" },\n  { name: \"Tennessee\", abbr: \"TN\", lon: -86.3, lat: 35.8, population: 7.1, region: \"South\" },\n  { name: \"Massachusetts\", abbr: \"MA\", lon: -71.8, lat: 42.3, population: 7.0, region: \"Northeast\" },\n  { name: \"Indiana\", abbr: \"IN\", lon: -86.1, lat: 39.9, population: 6.8, region: \"Midwest\" },\n  { name: \"Missouri\", abbr: \"MO\", lon: -92.5, lat: 38.5, population: 6.2, region: \"Midwest\" },\n  { name: \"Wisconsin\", abbr: \"WI\", lon: -89.6, lat: 44.6, population: 5.9, region: \"Midwest\" },\n  { name: \"Colorado\", abbr: \"CO\", lon: -105.5, lat: 39.0, population: 5.9, region: \"West\" },\n  { name: \"Minnesota\", abbr: \"MN\", lon: -94.3, lat: 46.0, population: 5.7, region: \"Midwest\" },\n];\n\n// --- Radius scale: circle area (πr²), not radius, is proportional to\n// population — the defining rule of an area-distortion cartogram. -----------\nconst MIN_RADIUS = 13;\nconst MAX_RADIUS = 60;\nconst populations = states.map((s) => s.population);\nconst minPop = Math.min(...populations);\nconst maxPop = Math.max(...populations);\nconst sqrtMin = Math.sqrt(minPop);\nconst sqrtMax = Math.sqrt(maxPop);\nconst radiusFor = (population) =>\n  MIN_RADIUS + ((MAX_RADIUS - MIN_RADIUS) * (Math.sqrt(population) - sqrtMin)) / (sqrtMax - sqrtMin);\n\n// Secondary encoding: US Census region, from the Imprint categorical palette.\nconst regionColor = (region) => t.palette[REGIONS.indexOf(region)];\n\n// --- Geographic domain, padded so even the largest circles stay in frame ---\nconst lons = states.map((s) => s.lon);\nconst lats = states.map((s) => s.lat);\nconst lonRange = Math.max(...lons) - Math.min(...lons);\nconst latRange = Math.max(...lats) - Math.min(...lats);\nconst xDomain = [Math.min(...lons) - lonRange * 0.16, Math.max(...lons) + lonRange * 0.16];\nconst yDomain = [Math.min(...lats) - latRange * 0.3, Math.max(...lats) + latRange * 0.3];\n\n// One series per state (MUI X community scatter sizes markers per-series, not\n// per-point) — largest population first, so smaller circles draw on top and\n// stay visible instead of being covered by their bigger neighbors.\nconst series = [...states]\n  .sort((a, b) => b.population - a.population)\n  .map((s) => ({\n    id: s.abbr,\n    label: `${s.name} (${s.region})`,\n    data: [{ x: s.lon, y: s.lat, id: s.abbr }],\n    markerSize: radiusFor(s.population),\n    color: regionColor(s.region),\n    valueFormatter: () => `${s.name}: ${s.population.toFixed(1)}M residents`,\n  }));\n\nconst title = \"cartogram-area-distortion · javascript · muix · anyplot.ai\";\nconst titleFontSize = Math.max(16, Math.round(22 * Math.min(1, 67 / title.length)));\n\n// State abbreviations, positioned in data space via the chart's own scales.\nfunction StateLabels() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  return (\n    <g>\n      {states.map((s) => {\n        const r = radiusFor(s.population);\n        const fontSize = Math.max(10, Math.min(15, r * 0.42));\n        return (\n          <text\n            key={s.abbr}\n            x={xScale(s.lon)}\n            y={yScale(s.lat)}\n            textAnchor=\"middle\"\n            dominantBaseline=\"central\"\n            fontSize={fontSize}\n            fontWeight={600}\n            fill={t.pageBg}\n          >\n            {s.abbr}\n          </text>\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -----------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const sizeLegendCy = height - 96;\n  const colorLegendY = height - 20;\n  const minR = radiusFor(minPop);\n  const maxR = radiusFor(maxPop);\n  const maxCx = 240 + minR + 40 + maxR;\n\n  return (\n    <ScatterChart\n      width={width}\n      height={height}\n      series={series}\n      margin={{ top: 108, right: 40, bottom: 170, left: 40 }}\n      xAxis={[\n        {\n          scaleType: \"linear\",\n          min: xDomain[0],\n          max: xDomain[1],\n          disableLine: true,\n          disableTicks: true,\n          valueFormatter: () => \"\",\n        },\n      ]}\n      yAxis={[\n        {\n          scaleType: \"linear\",\n          min: yDomain[0],\n          max: yDomain[1],\n          disableLine: true,\n          disableTicks: true,\n          valueFormatter: () => \"\",\n        },\n      ]}\n      legend={{ hidden: true }}\n      skipAnimation\n    >\n      <StateLabels />\n      <text x={width / 2} y={50} textAnchor=\"middle\" fontSize={titleFontSize} fontWeight={600} fill={t.ink}>\n        {title}\n      </text>\n      <text x={width / 2} y={78} textAnchor=\"middle\" fontSize={14} fill={t.inkSoft}>\n        Circle area ∝ 2023 population estimate · color = US Census region\n      </text>\n\n      {/* Size legend: two reference circles anchor the area-to-population scale */}\n      <text x={60} y={sizeLegendCy - MAX_RADIUS - 14} fontSize={13} fill={t.inkSoft}>\n        Population (millions)\n      </text>\n      <circle cx={240} cy={sizeLegendCy} r={minR} fill=\"none\" stroke={t.inkSoft} strokeWidth={1.5} />\n      <text x={240 + minR + 10} y={sizeLegendCy + 5} fontSize={13} fill={t.inkSoft}>\n        {Math.round(minPop)}M\n      </text>\n      <circle cx={maxCx} cy={sizeLegendCy} r={maxR} fill=\"none\" stroke={t.inkSoft} strokeWidth={1.5} />\n      <text x={maxCx + maxR + 10} y={sizeLegendCy + 5} fontSize={13} fill={t.inkSoft}>\n        {Math.round(maxPop)}M\n      </text>\n\n      {/* Color legend: one swatch per US Census region */}\n      {REGIONS.map((region, i) => (\n        <g key={region}>\n          <circle cx={60 + i * 260} cy={colorLegendY} r={8} fill={t.palette[i]} />\n          <text x={60 + i * 260 + 16} y={colorLegendY + 5} fontSize={13} fill={t.inkSoft}>\n            {region}\n          </text>\n        </g>\n      ))}\n    </ScatterChart>\n  );\n}\n"}