{"spec_id":"bubble-map-geographic","library":"muix","language":"javascript","code":"// anyplot.ai\n// bubble-map-geographic: Bubble Map with Sized Geographic Markers\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 84/100 | Created: 2026-09-01\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: approximate 2023 metro-area population (millions) for 20 major\n// world cities, plotted at each city's longitude/latitude. The community\n// @mui/x-charts surface has no polygon/coastline basemap primitive, so the\n// geographic context comes from a longitude/latitude graticule (grid) and\n// degree-formatted axes instead of drawn country borders. -------------------\nconst REGIONS = [\"Asia-Pacific\", \"Europe\", \"Africa\", \"Americas\"];\n\nconst cities = [\n  { name: \"Tokyo\", lon: 139.7, lat: 35.7, population: 37.4, region: \"Asia-Pacific\" },\n  { name: \"Delhi\", lon: 77.2, lat: 28.6, population: 32.9, region: \"Asia-Pacific\" },\n  { name: \"Shanghai\", lon: 121.5, lat: 31.2, population: 29.2, region: \"Asia-Pacific\" },\n  { name: \"Jakarta\", lon: 106.8, lat: -6.2, population: 33.4, region: \"Asia-Pacific\" },\n  { name: \"Sydney\", lon: 151.2, lat: -33.9, population: 5.3, region: \"Asia-Pacific\" },\n  { name: \"Moscow\", lon: 37.6, lat: 55.8, population: 12.6, region: \"Europe\" },\n  { name: \"Istanbul\", lon: 29.0, lat: 41.0, population: 15.5, region: \"Europe\" },\n  { name: \"Paris\", lon: 2.35, lat: 48.85, population: 11.1, region: \"Europe\" },\n  { name: \"London\", lon: -0.1, lat: 51.5, population: 9.6, region: \"Europe\" },\n  { name: \"Madrid\", lon: -3.7, lat: 40.4, population: 6.7, region: \"Europe\" },\n  { name: \"Cairo\", lon: 31.2, lat: 30.0, population: 21.3, region: \"Africa\" },\n  { name: \"Kinshasa\", lon: 15.3, lat: -4.3, population: 15.6, region: \"Africa\" },\n  { name: \"Lagos\", lon: 3.4, lat: 6.5, population: 15.4, region: \"Africa\" },\n  { name: \"Johannesburg\", lon: 28.0, lat: -26.2, population: 6.2, region: \"Africa\" },\n  { name: \"Nairobi\", lon: 36.8, lat: -1.3, population: 4.9, region: \"Africa\" },\n  { name: \"Sao Paulo\", lon: -46.6, lat: -23.5, population: 22.6, region: \"Americas\" },\n  { name: \"Mexico City\", lon: -99.1, lat: 19.4, population: 22.3, region: \"Americas\" },\n  { name: \"New York\", lon: -74.0, lat: 40.7, population: 18.9, region: \"Americas\" },\n  { name: \"Buenos Aires\", lon: -58.4, lat: -34.6, population: 15.4, region: \"Americas\" },\n  { name: \"Bogota\", lon: -74.1, lat: 4.7, population: 11.2, region: \"Americas\" },\n];\n\n// --- Radius scale: circle AREA (not radius) carries the population value,\n// anchored at zero so proportions stay accurate; a floor keeps small cities\n// visible (spec: \"minimum bubble size to ensure small values remain visible\").\nconst MIN_RADIUS = 9;\nconst MAX_RADIUS = 46;\nconst maxPopulation = Math.max(...cities.map((c) => c.population));\nconst radiusFor = (population) =>\n  Math.max(MIN_RADIUS, MAX_RADIUS * Math.sqrt(population / maxPopulation));\n\n// Secondary encoding: world region, from the Imprint categorical palette.\n// Solid (non-alpha) fill: an alpha-hex fill composites against the page\n// background, which differs between themes (#FAF8F1 vs #1A1A17), so the same\n// alpha value renders as a visibly different hue per theme — a violation of\n// the Imprint contract that data colors stay identical across themes. Overlap\n// legibility in dense clusters instead comes from the largest-first draw\n// order below (smaller circles stay uncovered on top of larger ones).\nconst regionColor = (region) => t.palette[REGIONS.indexOf(region)];\n\nconst lonLabel = (v) => `${Math.round(Math.abs(v))}°${v < 0 ? \"W\" : \"E\"}`;\nconst latLabel = (v) => `${Math.round(Math.abs(v))}°${v < 0 ? \"S\" : \"N\"}`;\n\n// One series per city (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 = [...cities]\n  .sort((a, b) => b.population - a.population)\n  .map((c) => ({\n    id: c.name,\n    label: `${c.name} (${c.region})`,\n    data: [{ x: c.lon, y: c.lat, id: c.name }],\n    markerSize: radiusFor(c.population),\n    color: regionColor(c.region),\n    valueFormatter: () =>\n      `${c.name} — ${c.population.toFixed(1)}M residents · ${latLabel(c.lat)}, ${lonLabel(c.lon)}`,\n  }));\n\nconst title = \"bubble-map-geographic · javascript · muix · anyplot.ai\";\nconst titleFontSize = Math.max(16, Math.round(22 * Math.min(1, 67 / title.length)));\n\n// Size legend: three reference populations anchor the sqrt-area scale,\n// aligned along a shared baseline so the circles read left-to-right.\nconst legendPopulations = [5, 15, 35];\nconst legendRadii = legendPopulations.map(radiusFor);\nconst legendBaselineY = 800;\nlet legendCx = 116;\nconst legendCenters = legendRadii.map((r, i) => {\n  if (i > 0) legendCx += legendRadii[i - 1] + 46 + r;\n  return legendCx;\n});\n\n// --- Chart (default-exported component — the harness mounts it) -----------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n\n  return (\n    <ScatterChart\n      width={width}\n      height={height}\n      series={series}\n      margin={{ top: 120, right: 56, bottom: 232, left: 72 }}\n      xAxis={[\n        {\n          scaleType: \"linear\",\n          min: -180,\n          max: 180,\n          tickNumber: 7,\n          valueFormatter: lonLabel,\n          label: \"Longitude\",\n          labelStyle: { fontSize: 15 },\n          tickLabelStyle: { fontSize: 14 },\n        },\n      ]}\n      yAxis={[\n        {\n          scaleType: \"linear\",\n          min: -60,\n          max: 85,\n          tickNumber: 6,\n          valueFormatter: latLabel,\n          label: \"Latitude\",\n          labelStyle: { fontSize: 15 },\n          tickLabelStyle: { fontSize: 14 },\n        },\n      ]}\n      grid={{ horizontal: true, vertical: true }}\n      legend={{ hidden: true }}\n      skipAnimation\n    >\n      <text\n        x={width / 2}\n        y={50}\n        textAnchor=\"middle\"\n        fontSize={titleFontSize}\n        fontWeight={600}\n        fill={t.ink}\n      >\n        {title}\n      </text>\n      <text x={width / 2} y={78} textAnchor=\"middle\" fontSize={14} fill={t.inkSoft}>\n        Bubble area ∝ 2023 metro population estimate · color = world region\n      </text>\n\n      {/* Size legend: reference circles anchor the population-to-area scale */}\n      <text\n        x={legendCenters[0] - legendRadii[0]}\n        y={legendBaselineY - MAX_RADIUS - 20}\n        fontSize={13}\n        fill={t.inkSoft}\n      >\n        Population (millions)\n      </text>\n      {legendPopulations.map((pop, i) => (\n        <g key={pop}>\n          <circle\n            cx={legendCenters[i]}\n            cy={legendBaselineY - legendRadii[i]}\n            r={legendRadii[i]}\n            fill=\"none\"\n            stroke={t.inkSoft}\n            strokeWidth={1.5}\n          />\n          <text\n            x={legendCenters[i]}\n            y={legendBaselineY + 18}\n            textAnchor=\"middle\"\n            fontSize={13}\n            fill={t.inkSoft}\n          >\n            {pop}M\n          </text>\n        </g>\n      ))}\n\n      {/* Color legend: one swatch per world region */}\n      {REGIONS.map((region, i) => (\n        <g key={region}>\n          <circle cx={116 + i * 280} cy={860} r={8} fill={t.palette[i]} />\n          <text x={116 + i * 280 + 16} y={865} fontSize={13} fill={t.inkSoft}>\n            {region}\n          </text>\n        </g>\n      ))}\n    </ScatterChart>\n  );\n}\n"}