{"spec_id":"heatmap-geographic","library":"muix","language":"javascript","code":"// anyplot.ai\n// heatmap-geographic: Geographic Heatmap for Spatial Density\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-02\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { ContinuousColorLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst SIZE = window.ANYPLOT_SIZE;\nconst MARGIN = { top: 90, right: 250, bottom: 90, left: 130 };\nconst PLOT_WIDTH = SIZE.width - MARGIN.left - MARGIN.right;\nconst PLOT_HEIGHT = SIZE.height - MARGIN.top - MARGIN.bottom;\n\nconst TITLE =\n  \"Bay Area Retail Foot-Traffic Density · heatmap-geographic · javascript · muix · anyplot.ai\";\n// Scale the title down once it runs past the ~67-char mandated baseline\n// (see prompts/plot-generator.md \"Title fontsize must scale with title length\").\nconst TITLE_FONT_SIZE = Math.max(14, Math.round(22 * Math.min(1, 67 / TITLE.length)));\n\n// --- Deterministic PRNG (fixed-seed LCG, Box-Muller for gaussian jitter) ----\nlet seed = 42;\nconst rand = () => {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n};\nconst gaussian = () => {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n};\n\n// --- Data: retail foot-traffic check-ins across the San Francisco Bay Area --\nconst LON_MIN = -122.55;\nconst LON_MAX = -121.75;\nconst LAT_MIN = 37.25;\nconst LAT_MAX = 37.9;\n\n// Degrees-to-km conversion at this latitude, used so the KDE bandwidth below\n// reflects real city-scale distances instead of raw (anisotropic) degrees.\nconst KM_PER_DEG_LAT = 111;\nconst KM_PER_DEG_LON = 88;\n\nconst hotspots = [\n  { lon: -122.42, lat: 37.775, weight: 1.0, sigmaKm: 2.6 }, // San Francisco downtown\n  { lon: -122.27, lat: 37.805, weight: 0.7, sigmaKm: 3.2 }, // Oakland downtown\n  { lon: -122.27, lat: 37.872, weight: 0.4, sigmaKm: 2.2 }, // Berkeley\n  { lon: -121.885, lat: 37.335, weight: 0.85, sigmaKm: 3.4 }, // San Jose downtown\n  { lon: -122.16, lat: 37.445, weight: 0.55, sigmaKm: 2.4 }, // Palo Alto\n  { lon: -121.99, lat: 37.548, weight: 0.3, sigmaKm: 2.0 }, // Fremont\n];\n\nconst visits = [];\nhotspots.forEach((h) => {\n  const count = Math.round(h.weight * 140);\n  for (let i = 0; i < count; i += 1) {\n    visits.push({\n      lon: h.lon + (gaussian() * h.sigmaKm) / KM_PER_DEG_LON,\n      lat: h.lat + (gaussian() * h.sigmaKm) / KM_PER_DEG_LAT,\n    });\n  }\n});\nfor (let i = 0; i < 90; i += 1) {\n  visits.push({\n    lon: LON_MIN + rand() * (LON_MAX - LON_MIN),\n    lat: LAT_MIN + rand() * (LAT_MAX - LAT_MIN),\n  });\n}\n\n// --- Kernel density estimate, rasterized to a smooth image ------------------\n// A discrete-marker scatter approximation leaves scalloped circular edges\n// around each hotspot. Community @mui/x-charts has no native heatmap/image\n// layer, so instead the KDE is sampled onto a raster grid, colour-mapped per\n// pixel (Imprint sequential ramp, alpha fading out below DENSITY_FLOOR), and\n// drawn as one <image>: the browser's own bilinear upscaling then renders a\n// genuinely continuous density field. Distances are converted to km so the\n// kernel is isotropic in real space, and truncated at 3*bandwidth so\n// far-away points can't sum into a visible tail.\nconst BANDWIDTH_KM = 4.5;\nconst TRUNC_KM_SQ = (3 * BANDWIDTH_KM) ** 2;\nconst DENSITY_FLOOR = 0.12; // below this the pixel is fully transparent\nconst RASTER_W = 400;\nconst RASTER_H = Math.round(RASTER_W * (PLOT_HEIGHT / PLOT_WIDTH));\n\nconst rawDensity = new Float32Array(RASTER_W * RASTER_H);\nlet maxDensity = 0;\nfor (let py = 0; py < RASTER_H; py += 1) {\n  const lat = LAT_MAX - ((py + 0.5) / RASTER_H) * (LAT_MAX - LAT_MIN);\n  for (let px = 0; px < RASTER_W; px += 1) {\n    const lon = LON_MIN + ((px + 0.5) / RASTER_W) * (LON_MAX - LON_MIN);\n    let density = 0;\n    for (let i = 0; i < visits.length; i += 1) {\n      const dKmLon = (lon - visits[i].lon) * KM_PER_DEG_LON;\n      const dKmLat = (lat - visits[i].lat) * KM_PER_DEG_LAT;\n      const distKmSq = dKmLon * dKmLon + dKmLat * dKmLat;\n      if (distKmSq < TRUNC_KM_SQ) {\n        density += Math.exp(-distKmSq / (2 * BANDWIDTH_KM * BANDWIDTH_KM));\n      }\n    }\n    rawDensity[py * RASTER_W + px] = density;\n    maxDensity = Math.max(maxDensity, density);\n  }\n}\n\nconst hexToRgb = (hex) => [\n  parseInt(hex.slice(1, 3), 16),\n  parseInt(hex.slice(3, 5), 16),\n  parseInt(hex.slice(5, 7), 16),\n];\nconst seqLowRgb = hexToRgb(t.seq[0]);\nconst seqHighRgb = hexToRgb(t.seq[1]);\n\nconst canvas = document.createElement(\"canvas\");\ncanvas.width = RASTER_W;\ncanvas.height = RASTER_H;\nconst ctx = canvas.getContext(\"2d\");\nconst image = ctx.createImageData(RASTER_W, RASTER_H);\nfor (let i = 0; i < rawDensity.length; i += 1) {\n  const z = Math.min(1, rawDensity[i] / maxDensity);\n  const o = i * 4;\n  image.data[o] = Math.round(seqLowRgb[0] + (seqHighRgb[0] - seqLowRgb[0]) * z);\n  image.data[o + 1] = Math.round(seqLowRgb[1] + (seqHighRgb[1] - seqLowRgb[1]) * z);\n  image.data[o + 2] = Math.round(seqLowRgb[2] + (seqHighRgb[2] - seqLowRgb[2]) * z);\n  image.data[o + 3] =\n    z <= DENSITY_FLOOR ? 0 : Math.round(((z - DENSITY_FLOOR) / (1 - DENSITY_FLOOR)) * 235);\n}\nctx.putImageData(image, 0, 0);\nconst HEATMAP_URI = canvas.toDataURL(\"image/png\");\n\n// --- Geographic reference graticule (basemap substitute — no map tiles are\n// available in the community package, so a lon/lat grid gives spatial context)\nconst latLines = [37.4, 37.6, 37.8];\nconst lonLines = [-122.35, -122.15, -121.95];\n\nfunction MapTitle() {\n  return (\n    <text\n      x={SIZE.width / 2}\n      y={40}\n      textAnchor=\"middle\"\n      dominantBaseline=\"hanging\"\n      fontSize={TITLE_FONT_SIZE}\n      fontWeight={500}\n      fill={t.ink}\n    >\n      {TITLE}\n    </text>\n  );\n}\n\nexport default function Chart() {\n  return (\n    <ScatterChart\n      width={SIZE.width}\n      height={SIZE.height}\n      skipAnimation\n      disableVoronoi\n      margin={MARGIN}\n      series={[]}\n      xAxis={[\n        {\n          min: LON_MIN,\n          max: LON_MAX,\n          label: \"Longitude\",\n          tickNumber: 4,\n          valueFormatter: (v) => `${Math.abs(v).toFixed(2)}°W`,\n          tickLabelStyle: { fontSize: 14 },\n          labelStyle: { fontSize: 16 },\n        },\n      ]}\n      yAxis={[\n        {\n          min: LAT_MIN,\n          max: LAT_MAX,\n          label: \"Latitude\",\n          tickNumber: 4,\n          valueFormatter: (v) => `${v.toFixed(2)}°N`,\n          tickLabelStyle: { fontSize: 14 },\n          labelStyle: { fontSize: 16 },\n        },\n      ]}\n      zAxis={[\n        {\n          id: \"density\",\n          min: 0,\n          max: 1,\n          // min/max must also live on colorMap itself: ContinuousColorLegend\n          // reads colorMap.min/max directly (not the axis-level min/max).\n          colorMap: { type: \"continuous\", min: 0, max: 1, color: [t.seq[0], t.seq[1]] },\n        },\n      ]}\n      slots={{ noDataOverlay: () => null }}\n      slotProps={{ legend: { hidden: true } }}\n    >\n      <image\n        href={HEATMAP_URI}\n        x={MARGIN.left}\n        y={MARGIN.top}\n        width={PLOT_WIDTH}\n        height={PLOT_HEIGHT}\n        preserveAspectRatio=\"none\"\n      />\n      {latLines.map((lat) => (\n        <ChartsReferenceLine\n          key={`lat-${lat}`}\n          y={lat}\n          lineStyle={{ stroke: t.grid, strokeDasharray: \"4 4\", strokeWidth: 1 }}\n          label={`${lat.toFixed(2)}°N`}\n          labelStyle={{ fontSize: 12, fill: t.inkSoft }}\n          labelAlign=\"end\"\n        />\n      ))}\n      {lonLines.map((lon) => (\n        <ChartsReferenceLine\n          key={`lon-${lon}`}\n          x={lon}\n          lineStyle={{ stroke: t.grid, strokeDasharray: \"4 4\", strokeWidth: 1 }}\n          label={`${Math.abs(lon).toFixed(2)}°W`}\n          labelStyle={{ fontSize: 12, fill: t.inkSoft }}\n        />\n      ))}\n      <ContinuousColorLegend\n        axisDirection=\"z\"\n        axisId=\"density\"\n        position={{ horizontal: \"right\", vertical: \"middle\" }}\n        direction=\"column\"\n        length=\"55%\"\n        thickness={14}\n        minLabel=\"Low\"\n        maxLabel=\"High\"\n        labelStyle={{ fontSize: 14, fill: t.inkSoft }}\n      />\n      <MapTitle />\n    </ScatterChart>\n  );\n}\n"}