{"spec_id":"bubble-map-geographic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// bubble-map-geographic: Bubble Map with Sized Geographic Markers\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-01\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) — major US cities by population -------\nconst CITIES = [\n  { label: \"New York\",      lon: -74.006,   lat: 40.7128, pop: 8_800_000, region: \"Northeast\" },\n  { label: \"Los Angeles\",   lon: -118.2437, lat: 34.0522, pop: 3_900_000, region: \"West\" },\n  { label: \"Chicago\",       lon: -87.6298,  lat: 41.8781, pop: 2_700_000, region: \"Midwest\" },\n  { label: \"Houston\",       lon: -95.3698,  lat: 29.7604, pop: 2_300_000, region: \"South\" },\n  { label: \"Phoenix\",       lon: -112.074,  lat: 33.4484, pop: 1_650_000, region: \"West\" },\n  { label: \"Philadelphia\",  lon: -75.1652,  lat: 39.9526, pop: 1_580_000, region: \"Northeast\" },\n  { label: \"San Antonio\",   lon: -98.4936,  lat: 29.4241, pop: 1_470_000, region: \"South\" },\n  { label: \"San Diego\",     lon: -117.1611, lat: 32.7157, pop: 1_380_000, region: \"West\" },\n  { label: \"Dallas\",        lon: -96.797,   lat: 32.7767, pop: 1_300_000, region: \"South\" },\n  { label: \"Austin\",        lon: -97.7431,  lat: 30.2672, pop: 970_000,   region: \"South\" },\n  { label: \"Jacksonville\",  lon: -81.6557,  lat: 30.3322, pop: 950_000,   region: \"South\" },\n  { label: \"San Francisco\", lon: -122.4194, lat: 37.7749, pop: 870_000,   region: \"West\" },\n  { label: \"Columbus\",      lon: -82.9988,  lat: 39.9612, pop: 900_000,   region: \"Midwest\" },\n  { label: \"Indianapolis\",  lon: -86.1581,  lat: 39.7684, pop: 880_000,   region: \"Midwest\" },\n  { label: \"Seattle\",       lon: -122.3321, lat: 47.6062, pop: 740_000,   region: \"West\" },\n  { label: \"Denver\",        lon: -104.9903, lat: 39.7392, pop: 715_000,   region: \"West\" },\n  { label: \"Washington DC\", lon: -77.0369,  lat: 38.9072, pop: 690_000,   region: \"Northeast\" },\n  { label: \"Boston\",        lon: -71.0589,  lat: 42.3601, pop: 655_000,   region: \"Northeast\" },\n  { label: \"Nashville\",     lon: -86.7816,  lat: 36.1627, pop: 690_000,   region: \"South\" },\n  { label: \"Detroit\",       lon: -83.0458,  lat: 42.3314, pop: 630_000,   region: \"Midwest\" },\n  { label: \"Portland\",      lon: -122.6765, lat: 45.5152, pop: 650_000,   region: \"West\" },\n  { label: \"Memphis\",       lon: -90.049,   lat: 35.1495, pop: 630_000,   region: \"South\" },\n  { label: \"Louisville\",    lon: -85.7585,  lat: 38.2527, pop: 630_000,   region: \"South\" },\n  { label: \"Minneapolis\",   lon: -93.265,   lat: 44.9778, pop: 430_000,   region: \"Midwest\" },\n];\n\nconst REGIONS = [\"Northeast\", \"South\", \"Midwest\", \"West\"];\n\n// Simplified continental-US outline for basemap context (rough, not to scale)\nconst US_OUTLINE = [\n  [-124.7, 48.4], [-123.0, 45.5], [-124.2, 40.8], [-120.5, 34.5],\n  [-117.2, 32.5], [-114.7, 32.5], [-111.0, 31.3], [-108.2, 31.3],\n  [-106.5, 31.8], [-99.5, 26.4], [-97.4, 25.9], [-97.1, 28.0],\n  [-93.8, 29.7], [-89.4, 29.2], [-85.0, 29.7], [-82.7, 24.6],\n  [-80.1, 25.8], [-80.5, 28.5], [-81.5, 30.7], [-79.9, 33.9],\n  [-77.9, 34.7], [-76.0, 36.9], [-75.5, 39.4], [-74.0, 40.6],\n  [-71.0, 41.6], [-70.2, 43.7], [-67.0, 44.8], [-71.5, 45.0],\n  [-79.2, 43.3], [-83.5, 41.7], [-84.5, 46.5], [-89.6, 48.0],\n  [-95.2, 49.0], [-104.0, 49.0], [-116.0, 49.0], [-123.2, 49.0],\n];\n\n// --- Bubble area sizing (area, not radius, proportional to population) -----\nconst MIN_R = 8;\nconst MAX_R = 46;\nconst MAX_POP = Math.max(...CITIES.map((c) => c.pop));\nconst K = MAX_R / Math.sqrt(MAX_POP);\nconst radiusForPop = (pop) => Math.max(MIN_R, K * Math.sqrt(pop));\n\nfunction hexToRgba(hex, alpha) {\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}, ${alpha})`;\n}\n\nconst regionColor = (region) => t.palette[REGIONS.indexOf(region)];\n\nconst datasets = REGIONS.map((region) => ({\n  label: region,\n  data: CITIES.filter((c) => c.region === region).map((c) => ({\n    x: c.lon,\n    y: c.lat,\n    r: radiusForPop(c.pop),\n    label: c.label,\n    pop: c.pop,\n  })),\n  backgroundColor: hexToRgba(regionColor(region), 0.6),\n  borderColor: regionColor(region),\n  borderWidth: 1.5,\n  hoverBorderWidth: 2,\n}));\n\n// --- Basemap plugin: simplified US landmass tint behind the bubbles --------\nconst basemapPlugin = {\n  id: \"basemap\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, scales: { x, y } } = chart;\n    ctx.save();\n    ctx.beginPath();\n    US_OUTLINE.forEach(([lon, lat], i) => {\n      const px = x.getPixelForValue(lon);\n      const py = y.getPixelForValue(lat);\n      if (i === 0) ctx.moveTo(px, py);\n      else ctx.lineTo(px, py);\n    });\n    ctx.closePath();\n    ctx.fillStyle = hexToRgba(t.ink, 0.06);\n    ctx.fill();\n    ctx.strokeStyle = hexToRgba(t.ink, 0.35);\n    ctx.lineWidth = 2;\n    ctx.stroke();\n    ctx.restore();\n  },\n};\n\n// --- Size-legend plugin: reference circles showing the value-to-area scale -\nconst sizeLegendPlugin = {\n  id: \"sizeLegend\",\n  afterDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const refPops = [500_000, 3_000_000, 8_000_000];\n    const refLabels = [\"500K\", \"3M\", \"8M\"];\n    const baseline = chartArea.bottom - 34;\n    let cx = chartArea.left + 60;\n\n    ctx.save();\n    ctx.font = \"600 15px sans-serif\";\n    ctx.fillStyle = t.inkSoft;\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"alphabetic\";\n    ctx.fillText(\"Population\", chartArea.left + 20, baseline - 2 * MAX_R - 16);\n\n    refPops.forEach((pop, i) => {\n      const r = radiusForPop(pop);\n      const cy = baseline - r;\n      ctx.beginPath();\n      ctx.arc(cx, cy, r, 0, Math.PI * 2);\n      ctx.fillStyle = hexToRgba(t.ink, 0.08);\n      ctx.fill();\n      ctx.strokeStyle = t.inkSoft;\n      ctx.lineWidth = 1.5;\n      ctx.stroke();\n\n      ctx.fillStyle = t.ink;\n      ctx.font = \"13px sans-serif\";\n      ctx.textAlign = \"center\";\n      ctx.fillText(refLabels[i], cx, baseline + 18);\n\n      cx += 2 * MAX_R + 26;\n    });\n    ctx.restore();\n  },\n};\n\n// --- Mount -------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bubble\",\n  data: { datasets },\n  plugins: [basemapPlugin, sizeLegendPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 10, right: 20, bottom: 10, left: 10 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"bubble-map-geographic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: {\n        position: \"top\",\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          usePointStyle: true,\n          pointStyle: \"circle\",\n        },\n      },\n      tooltip: {\n        callbacks: {\n          label: (ctx) => {\n            const p = ctx.raw;\n            return `${p.label}: ${p.pop.toLocaleString()} (${p.x.toFixed(1)}°, ${p.y.toFixed(1)}°)`;\n          },\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: -128,\n        max: -65,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          stepSize: 10,\n          callback: (v) => `${Math.abs(v)}°W`,\n        },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Longitude\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        type: \"linear\",\n        min: 23,\n        max: 51,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          stepSize: 10,\n          callback: (v) => `${v}°N`,\n        },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Latitude\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n});\n"}