{"spec_id":"heatmap-geographic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// heatmap-geographic: Geographic Heatmap for Spatial Density\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME;\n\n// --- Basemap chrome (not data — Imprint palette only governs data colors) --\nconst BLOCK_FILL = THEME === \"light\" ? \"#EDE9DA\" : \"#26261F\";\nconst STREET_COLOR = THEME === \"light\" ? \"rgba(107,106,99,0.4)\" : \"rgba(168,167,159,0.35)\";\n\n// --- Layout constants --------------------------------------------------------\n// The mount is a fixed 1600×900 CSS box (harness renders at deviceScaleFactor 2\n// -> 3200×1800 PNG). Margins are set explicitly so the plot-area pixel size is\n// known at codegen time, letting a district bounding box be chosen whose aspect\n// ratio matches plotWidth/plotHeight — that keeps the density grid cells square\n// instead of stretched (1 km must map to the same pixel count on both axes).\nconst MOUNT_WIDTH = 1600;\nconst MOUNT_HEIGHT = 900;\nconst MARGIN_LEFT = 80;\nconst MARGIN_RIGHT = 40;\nconst MARGIN_TOP = 110;\nconst MARGIN_BOTTOM = 170; // x-axis title/ticks + gap + hand-drawn gradient legend\nconst PLOT_WIDTH = MOUNT_WIDTH - MARGIN_LEFT - MARGIN_RIGHT;\nconst PLOT_HEIGHT = MOUNT_HEIGHT - MARGIN_TOP - MARGIN_BOTTOM;\n\n// Illustrative shopping-street district, not a real place.\nconst LAT0 = 45.42;\nconst LON0 = -75.7;\nconst KM_PER_DEG_LAT = 110.57;\nconst KM_PER_DEG_LON = 111.32 * Math.cos((LAT0 * Math.PI) / 180);\n\nconst X_RANGE_KM = 2.4; // district width — a long commercial corridor\nconst Y_RANGE_KM = (X_RANGE_KM * PLOT_HEIGHT) / PLOT_WIDTH; // locks pixel aspect\nconst X_MIN = -X_RANGE_KM / 2;\nconst X_MAX = X_RANGE_KM / 2;\nconst Y_MIN = -Y_RANGE_KM / 2;\nconst Y_MAX = Y_RANGE_KM / 2;\nconst PX_PER_KM = PLOT_WIDTH / X_RANGE_KM;\n\n// --- Deterministic PRNG (browser has no seeded RNG) --------------------------\nlet lcgState = 42;\nfunction nextRandom() {\n  lcgState = (lcgState * 1103515245 + 12345) % 2147483648;\n  return lcgState / 2147483648;\n}\nfunction jitter(spread) {\n  // Sum of 3 uniforms centred on 0 — cheap approx-normal, bounded, deterministic.\n  return ((nextRandom() + nextRandom() + nextRandom() - 1.5) / 1.5) * spread;\n}\n\n// --- Data: anonymized, opted-in foot-traffic pings near a shopping district --\n// (latitude, longitude) only — value defaults to 1, i.e. pure point density,\n// as used for retail site-selection analysis.\nconst HOTSPOTS = [\n  { name: \"Transit Station Plaza\", cx: -1.0, cy: 0.08, spread: 0.14, weight: 0.34 },\n  { name: \"Mall Food Court\", cx: -0.15, cy: -0.05, spread: 0.16, weight: 0.3 },\n  { name: \"Boutique Row\", cx: 0.55, cy: 0.1, spread: 0.22, weight: 0.22 },\n  { name: \"Farmers Market Square\", cx: 1.0, cy: -0.02, spread: 0.12, weight: 0.14 },\n];\nconst N_PINGS = 1600;\n\n// Fixed landmark reference points (away from every hotspot) so the basemap\n// reads as a real district layout, not just a density surface.\nconst LANDMARKS = [\n  { name: \"Clock Tower\", x: -0.55, y: -0.32 },\n  { name: \"Public Library\", x: 0.12, y: 0.36 },\n];\n\nconst pings = [];\nHOTSPOTS.forEach((hotspot) => {\n  const target = Math.round(N_PINGS * hotspot.weight);\n  for (let i = 0; i < target; i++) {\n    const x = hotspot.cx + jitter(hotspot.spread);\n    const y = hotspot.cy + jitter(hotspot.spread * 0.6);\n    if (x < X_MIN || x > X_MAX || y < Y_MIN || y > Y_MAX) continue;\n    pings.push({\n      x,\n      y,\n      lat: Math.round((LAT0 + y / KM_PER_DEG_LAT) * 10000) / 10000,\n      lon: Math.round((LON0 + x / KM_PER_DEG_LON) * 10000) / 10000,\n    });\n  }\n});\n\n// --- Kernel density estimation on a regular grid -----------------------------\n// A Gaussian KDE turns the discrete pings into the continuous intensity surface\n// the spec calls for (as opposed to e.g. hexagonal binning). BANDWIDTH_KM sets\n// how far each ping's influence spreads — tuned to this district's ~2.4 km\n// scale so adjacent hotspots blend smoothly without merging into one blob.\nconst BANDWIDTH_KM = 0.085;\nconst GRID_COLS = 150;\nconst CELL_KM = X_RANGE_KM / GRID_COLS;\nconst GRID_ROWS = Math.round(Y_RANGE_KM / CELL_KM);\nconst CELL_PX = PX_PER_KM * CELL_KM;\nconst TWO_BW_SQ = 2 * BANDWIDTH_KM * BANDWIDTH_KM;\nconst CUTOFF_KM = 3.2 * BANDWIDTH_KM; // skip pings beyond ~3.2 sigma — negligible contribution\n\nlet maxDensity = 0;\nconst cells = [];\nfor (let row = 0; row < GRID_ROWS; row++) {\n  const cy = Y_MIN + (row + 0.5) * CELL_KM;\n  for (let col = 0; col < GRID_COLS; col++) {\n    const cx = X_MIN + (col + 0.5) * CELL_KM;\n    let density = 0;\n    for (let p = 0; p < pings.length; p++) {\n      const dx = pings[p].x - cx;\n      if (dx > CUTOFF_KM || dx < -CUTOFF_KM) continue;\n      const dy = pings[p].y - cy;\n      if (dy > CUTOFF_KM || dy < -CUTOFF_KM) continue;\n      density += Math.exp(-(dx * dx + dy * dy) / TWO_BW_SQ);\n    }\n    if (density > maxDensity) maxDensity = density;\n    cells.push({ x: cx, y: cy, density });\n  }\n}\n\n// --- Map density -> Imprint sequential gradient + alpha ----------------------\n// The core Highcharts bundle has no heatmap/colorAxis module (see\n// prompts/library/highcharts.md), so each cell's fill is computed by hand — a\n// gamma-boosted interpolation across the two-stop imprint_seq gradient, with\n// alpha scaling so near-zero cells stay transparent and the basemap shows\n// through underneath (per spec: \"sequential colormap ... with transparency\").\nconst seqLow = [t.seq[0].slice(1, 3), t.seq[0].slice(3, 5), t.seq[0].slice(5, 7)].map((h) => parseInt(h, 16));\nconst seqHigh = [t.seq[1].slice(1, 3), t.seq[1].slice(3, 5), t.seq[1].slice(5, 7)].map((h) => parseInt(h, 16));\nconst MIN_ALPHA = 0.04;\nconst MAX_ALPHA = 0.92;\nconst RENDER_THRESHOLD = 0.03; // skip visually-negligible cells (basemap already shows through)\n\nconst heatCells = [];\ncells.forEach((cell) => {\n  const frac = maxDensity > 0 ? cell.density / maxDensity : 0;\n  if (frac < RENDER_THRESHOLD) return;\n  const boosted = Math.pow(frac, 0.6); // lifts mid-range density into visible contrast\n  const rgb = seqLow.map((c, i) => Math.round(c + (seqHigh[i] - c) * boosted));\n  const alpha = MIN_ALPHA + (MAX_ALPHA - MIN_ALPHA) * boosted;\n  heatCells.push({\n    x: cell.x,\n    y: cell.y,\n    lat: Math.round((LAT0 + cell.y / KM_PER_DEG_LAT) * 10000) / 10000,\n    lon: Math.round((LON0 + cell.x / KM_PER_DEG_LON) * 10000) / 10000,\n    frac: Math.round(frac * 1000) / 1000,\n    color: `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha.toFixed(3)})`,\n  });\n});\n\n// --- Schematic street grid — basemap context beneath the density surface ----\n// Irregular spacing plus major/minor line weights (avenues vs. side streets)\n// and one diagonal boulevard, so the basemap reads as an actual district\n// layout rather than a uniform grid. Each line is its own tiny series (not one\n// multi-segment series) so Highcharts never re-sorts the two endpoints by x,\n// which would otherwise scramble the grid.\nconst STREET_LINE_BASE = {\n  type: \"line\",\n  color: STREET_COLOR,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  showInLegend: false,\n  zIndex: 0,\n};\nconst AVENUE_STYLE = { lineWidth: 1.5, dashStyle: \"ShortDash\" }; // major streets\nconst SIDE_STREET_STYLE = { lineWidth: 1, dashStyle: \"Dash\" }; // minor streets\nconst AVENUE_X_FRACS = [0.14, 0.52, 0.88];\nconst SIDE_STREET_X_FRACS = [0.32, 0.7];\nconst AVENUE_Y_FRACS = [0.26, 0.74];\nconst SIDE_STREET_Y_FRACS = [0.12, 0.45, 0.6, 0.9];\nconst streetGridSeries = [\n  ...AVENUE_X_FRACS.map((f) => ({\n    ...STREET_LINE_BASE,\n    ...AVENUE_STYLE,\n    data: [\n      [X_MIN + f * X_RANGE_KM, Y_MAX],\n      [X_MIN + f * X_RANGE_KM, Y_MIN],\n    ],\n  })),\n  ...SIDE_STREET_X_FRACS.map((f) => ({\n    ...STREET_LINE_BASE,\n    ...SIDE_STREET_STYLE,\n    data: [\n      [X_MIN + f * X_RANGE_KM, Y_MAX],\n      [X_MIN + f * X_RANGE_KM, Y_MIN],\n    ],\n  })),\n  ...AVENUE_Y_FRACS.map((f) => ({\n    ...STREET_LINE_BASE,\n    ...AVENUE_STYLE,\n    data: [\n      [X_MIN, Y_MIN + f * Y_RANGE_KM],\n      [X_MAX, Y_MIN + f * Y_RANGE_KM],\n    ],\n  })),\n  ...SIDE_STREET_Y_FRACS.map((f) => ({\n    ...STREET_LINE_BASE,\n    ...SIDE_STREET_STYLE,\n    data: [\n      [X_MIN, Y_MIN + f * Y_RANGE_KM],\n      [X_MAX, Y_MIN + f * Y_RANGE_KM],\n    ],\n  })),\n  {\n    ...STREET_LINE_BASE,\n    ...AVENUE_STYLE,\n    data: [\n      [X_MIN, Y_MIN + 0.1 * Y_RANGE_KM],\n      [X_MIN + 0.62 * X_RANGE_KM, Y_MAX],\n    ],\n  }, // diagonal boulevard cutting across the grid\n];\n\n// --- Chart --------------------------------------------------------------------\nconst title = \"heatmap-geographic · javascript · highcharts · anyplot.ai\";\n\nfunction drawColorLegend(chart) {\n  const r = chart.renderer;\n  const x0 = chart.plotLeft;\n  const y0 = chart.plotTop + chart.plotHeight + 68;\n  const barWidth = 260;\n  const barHeight = 16;\n\n  r.text(\"Relative ping density\", x0, y0 - 10)\n    .css({ color: t.inkSoft, fontSize: \"14px\", fontWeight: \"600\" })\n    .add();\n\n  r.rect(x0, y0, barWidth, barHeight, 3)\n    .attr({\n      fill: {\n        linearGradient: { x1: 0, y1: 0, x2: 1, y2: 0 },\n        stops: [\n          [0, t.seq[0]],\n          [1, t.seq[1]],\n        ],\n      },\n      \"stroke-width\": 0,\n    })\n    .add();\n  r.text(\"Low\", x0, y0 + barHeight + 18)\n    .css({ color: t.inkSoft, fontSize: \"12px\" })\n    .add();\n  r.text(\"High\", x0 + barWidth - 24, y0 + barHeight + 18)\n    .css({ color: t.inkSoft, fontSize: \"12px\" })\n    .add();\n}\n\n// Named on the chart directly, turning the color contrast into a story instead\n// of leaving it to the tooltip alone. Placed in the margin band above the plot\n// area since the density surface can extend right up to the axis-max edge.\nfunction drawHotspotLabels(chart) {\n  const py = chart.plotTop - 10;\n  HOTSPOTS.forEach((h) => {\n    const px = chart.xAxis[0].toPixels(h.cx, false);\n    chart.renderer\n      .text(h.name, px, py)\n      .attr({ align: \"center\" })\n      .css({ color: t.ink, fontSize: \"13px\", fontWeight: \"700\" })\n      .add();\n  });\n}\n\n// Two fixed landmark markers (small filled square + label) reinforce that the\n// basemap represents a real street layout, distinct from the density blobs.\nfunction drawLandmarks(chart) {\n  LANDMARKS.forEach((lm) => {\n    const px = chart.xAxis[0].toPixels(lm.x, false);\n    const py = chart.yAxis[0].toPixels(lm.y, false);\n    chart.renderer.rect(px - 5, py - 5, 10, 10).attr({ fill: t.inkSoft, opacity: 0.6, \"stroke-width\": 0, zIndex: 2 }).add();\n    chart.renderer\n      .text(lm.name, px + 10, py + 4)\n      .css({ color: t.inkSoft, fontSize: \"12px\", fontWeight: \"600\" })\n      .add();\n  });\n}\n\nHighcharts.chart(\n  \"container\",\n  {\n    chart: {\n      type: \"scatter\",\n      backgroundColor: \"transparent\",\n      plotBackgroundColor: BLOCK_FILL,\n      animation: false,\n      style: { fontFamily: \"inherit\" },\n      marginLeft: MARGIN_LEFT,\n      marginRight: MARGIN_RIGHT,\n      marginTop: MARGIN_TOP,\n      marginBottom: MARGIN_BOTTOM,\n      zooming: { type: \"xy\" }, // interactive HTML: drag to zoom into any part of the surface\n    },\n    credits: { enabled: false },\n    colors: t.palette,\n    title: {\n      text: title,\n      align: \"left\",\n      style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n    },\n    subtitle: {\n      text: `${N_PINGS.toLocaleString()} anonymized foot-traffic pings smoothed into a Gaussian KDE surface (bandwidth ${Math.round(BANDWIDTH_KM * 1000)} m) — drag to zoom`,\n      align: \"left\",\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n    },\n    xAxis: {\n      min: X_MIN,\n      max: X_MAX,\n      startOnTick: false,\n      endOnTick: false,\n      title: { text: \"km east of district center\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n      lineColor: t.inkSoft,\n      tickColor: t.inkSoft,\n      gridLineWidth: 0,\n      labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    },\n    yAxis: {\n      min: Y_MIN,\n      max: Y_MAX,\n      startOnTick: false,\n      endOnTick: false,\n      title: { text: \"km north of district center\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n      lineColor: t.inkSoft,\n      tickColor: t.inkSoft,\n      gridLineWidth: 0,\n      labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    },\n    legend: { enabled: false },\n    tooltip: {\n      backgroundColor: t.elevatedBg,\n      borderColor: t.inkSoft,\n      style: { color: t.ink, fontSize: \"13px\" },\n      useHTML: false,\n    },\n    plotOptions: {\n      series: { animation: false },\n    },\n    series: [\n      ...streetGridSeries,\n      {\n        name: \"Ping density\",\n        type: \"scatter\",\n        data: heatCells,\n        showInLegend: false,\n        zIndex: 1,\n        marker: {\n          symbol: \"square\",\n          radius: CELL_PX / 2,\n          lineWidth: 0,\n          states: { hover: { enabled: false } },\n        },\n        turboThreshold: 0,\n        tooltip: {\n          pointFormatter() {\n            return (\n              `Relative density: ${(this.frac * 100).toFixed(0)}%<br/>` +\n              `≈ ${this.lat}°N, ${Math.abs(this.lon)}°W`\n            );\n          },\n        },\n      },\n    ],\n  },\n  function (chart) {\n    drawColorLegend(chart);\n    drawHotspotLabels(chart);\n    drawLandmarks(chart);\n  },\n);\n"}