{"spec_id":"heatmap-geographic","library":"d3","language":"javascript","code":"// anyplot.ai\n// heatmap-geographic: Geographic Heatmap for Spatial Density\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-02\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Deterministic PRNG (browser has no seeded Math.random) -----------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction gaussianJitter(sigma) {\n  const u1 = Math.max(rand(), 1e-6);\n  const u2 = rand();\n  return sigma * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// --- Basemap: stylized coastal-district outline (illustrative, not a survey) -\nconst centerLon = -74.01; // Downtown Manhattan / Financial District\nconst centerLat = 40.71;\nconst halfLonDeg = 0.095;\nconst halfLatDeg = 0.1;\nconst coastPoints = 56;\nconst coastline = d3.range(coastPoints).map((i) => {\n  const theta = (i / coastPoints) * 2 * Math.PI;\n  const r =\n    1 +\n    0.14 * Math.sin(3 * theta + 0.6) +\n    0.08 * Math.sin(7 * theta + 1.7) +\n    0.05 * Math.sin(11 * theta + 2.9);\n  return [centerLon + halfLonDeg * r * Math.cos(theta), centerLat + halfLatDeg * r * Math.sin(theta)];\n});\n\n// --- Data: ride-share pickup locations, clustered around activity hubs ------\nconst hotspots = [\n  { lon: centerLon - 0.028, lat: centerLat + 0.06, count: 420, sigma: 0.016 }, // north business district\n  { lon: centerLon + 0.02, lat: centerLat + 0.005, count: 520, sigma: 0.013 }, // central transit hub\n  { lon: centerLon - 0.008, lat: centerLat - 0.055, count: 300, sigma: 0.018 }, // south waterfront\n  { lon: centerLon + 0.032, lat: centerLat - 0.075, count: 260, sigma: 0.014 }, // south tip nightlife\n  { lon: centerLon + 0.004, lat: centerLat + 0.09, count: 180, sigma: 0.019 }, // north residential node\n];\n\nconst pickups = [];\nfor (const h of hotspots) {\n  for (let i = 0; i < h.count; i += 1) {\n    pickups.push({\n      lon: h.lon + gaussianJitter(h.sigma),\n      lat: h.lat + gaussianJitter(h.sigma * 0.85),\n    });\n  }\n}\nfor (let i = 0; i < 220; i += 1) {\n  pickups.push({\n    lon: centerLon + gaussianJitter(halfLonDeg * 0.5),\n    lat: centerLat + gaussianJitter(halfLatDeg * 0.5),\n  });\n}\n\n// --- Layout -------------------------------------------------------------\nconst margin = { top: 110, right: 200, bottom: 110, left: 110 };\nconst mapAreaW = width - margin.left - margin.right;\nconst mapAreaH = height - margin.top - margin.bottom;\n\nconst lonValues = coastline.map((d) => d[0]).concat(pickups.map((d) => d.lon));\nconst latValues = coastline.map((d) => d[1]).concat(pickups.map((d) => d.lat));\nconst [lonMin0, lonMax0] = d3.extent(lonValues);\nconst [latMin0, latMax0] = d3.extent(latValues);\nconst lonPad = (lonMax0 - lonMin0) * 0.06;\nconst latPad = (latMax0 - latMin0) * 0.06;\nconst lonExtent = [lonMin0 - lonPad, lonMax0 + lonPad];\nconst latExtent = [latMin0 - latPad, latMax0 + latPad];\n\nconst lonSpan = lonExtent[1] - lonExtent[0];\nconst latSpan = latExtent[1] - latExtent[0];\nconst effectiveLonSpan = lonSpan * Math.cos((centerLat * Math.PI) / 180);\nconst dataAspect = latSpan / effectiveLonSpan; // height / width, true-distance corrected\n\nlet mapW;\nlet mapH;\nif (dataAspect > mapAreaH / mapAreaW) {\n  mapH = mapAreaH;\n  mapW = mapH / dataAspect;\n} else {\n  mapW = mapAreaW;\n  mapH = mapW * dataAspect;\n}\nconst offsetX = margin.left + (mapAreaW - mapW) / 2;\nconst offsetY = margin.top + (mapAreaH - mapH) / 2;\n\nconst xScale = d3.scaleLinear().domain(lonExtent).range([offsetX, offsetX + mapW]);\nconst yScale = d3.scaleLinear().domain(latExtent).range([offsetY + mapH, offsetY]);\n\n// --- Kernel density estimate over the pickup points -------------------------\nconst density = d3\n  .contourDensity()\n  .x((d) => xScale(d.lon))\n  .y((d) => yScale(d.lat))\n  .size([width, height])\n  .bandwidth(45)\n  .thresholds(12)(pickups);\n\nconst maxDensity = Math.max(d3.max(density, (d) => d.value), 1e-6);\nconst densityColor = (v) => d3.interpolateRgbBasis(t.seq)(Math.min(v / maxDensity, 1));\n\n// --- SVG mount ----------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\nconst lineGen = d3\n  .line()\n  .x((d) => xScale(d[0]))\n  .y((d) => yScale(d[1]))\n  .curve(d3.curveCatmullRomClosed.alpha(0.5));\nconst landD = lineGen(coastline);\n\nsvg.append(\"defs\").append(\"clipPath\").attr(\"id\", \"landClip\").append(\"path\").attr(\"d\", landD);\n\n// map-layer: everything that pans/scales together under d3.zoom()\nconst mapLayer = svg.append(\"g\").attr(\"class\", \"map-layer\");\n\n// land base\nmapLayer.append(\"path\").attr(\"d\", landD).attr(\"fill\", t.elevatedBg);\n\n// density contours + point texture, clipped to the landmass\nconst clipped = mapLayer.append(\"g\").attr(\"clip-path\", \"url(#landClip)\");\nclipped\n  .selectAll(\"path.contour\")\n  .data(density)\n  .join(\"path\")\n  .attr(\"class\", \"contour\")\n  .attr(\"d\", d3.geoPath())\n  .attr(\"fill\", (d) => densityColor(d.value))\n  .attr(\"fill-opacity\", (d) => 0.15 + 0.7 * (d.value / maxDensity))\n  .attr(\"stroke\", \"none\");\n\nclipped\n  .selectAll(\"circle\")\n  .data(pickups)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => xScale(d.lon))\n  .attr(\"cy\", (d) => yScale(d.lat))\n  .attr(\"r\", 1.3)\n  .attr(\"fill\", t.ink)\n  .attr(\"opacity\", 0.1);\n\n// coastline outline, crisp on top of the fills\nmapLayer.append(\"path\").attr(\"d\", landD).attr(\"fill\", \"none\").attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.5);\n\n// --- Annotation: highlight the single strongest density peak ---------------\nconst peakContour = density.reduce((a, b) => (b.value > a.value ? b : a));\nconst [peakX, peakY] = d3.polygonCentroid(peakContour.coordinates[0][0]);\nconst peakLabelX = peakX - 40;\nconst peakLabelY = peakY - 40;\n\nconst annotation = svg.append(\"g\").attr(\"class\", \"peak-annotation\");\nannotation\n  .append(\"circle\")\n  .attr(\"cx\", peakX)\n  .attr(\"cy\", peakY)\n  .attr(\"r\", 30)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"4,3\")\n  .attr(\"opacity\", 0.75);\nannotation\n  .append(\"line\")\n  .attr(\"x1\", peakX - 21)\n  .attr(\"y1\", peakY - 21)\n  .attr(\"x2\", peakLabelX + 4)\n  .attr(\"y2\", peakLabelY + 6)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1)\n  .attr(\"opacity\", 0.6);\nannotation\n  .append(\"text\")\n  .attr(\"x\", peakLabelX)\n  .attr(\"y\", peakLabelY)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Peak density\");\n\n// --- Axes (lon/lat reference grid) -------------------------------------\nconst lonFormat = (d) => `${Math.abs(d).toFixed(2)}°${d < 0 ? \"W\" : \"E\"}`;\nconst latFormat = (d) => `${Math.abs(d).toFixed(2)}°${d < 0 ? \"S\" : \"N\"}`;\n\nconst xAxis = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${offsetY + mapH})`)\n  .call(d3.axisBottom(xScale).ticks(5).tickFormat(lonFormat).tickSize(-mapH));\nconst yAxis = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${offsetX},0)`)\n  .call(d3.axisLeft(yScale).ticks(5).tickFormat(latFormat).tickSize(-mapW));\n\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.grid);\n  axis.select(\".domain\").remove();\n}\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", offsetX + mapW / 2)\n  .attr(\"y\", offsetY + mapH + 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Longitude (°)\");\n\nsvg\n  .append(\"text\")\n  .attr(\"transform\", `translate(${offsetX - 62},${offsetY + mapH / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Latitude (°)\");\n\n// --- Zoom: pan/scale the map to explore density at different scales --------\nconst zoom = d3\n  .zoom()\n  .scaleExtent([1, 6])\n  .translateExtent([\n    [offsetX, offsetY],\n    [offsetX + mapW, offsetY + mapH],\n  ])\n  .extent([\n    [offsetX, offsetY],\n    [offsetX + mapW, offsetY + mapH],\n  ])\n  .on(\"zoom\", (event) => {\n    mapLayer.attr(\"transform\", event.transform);\n    const zx = event.transform.rescaleX(xScale);\n    const zy = event.transform.rescaleY(yScale);\n    xAxis.call(d3.axisBottom(zx).ticks(5).tickFormat(lonFormat).tickSize(-mapH));\n    yAxis.call(d3.axisLeft(zy).ticks(5).tickFormat(latFormat).tickSize(-mapW));\n    for (const axis of [xAxis, yAxis]) {\n      axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n      axis.selectAll(\"line\").attr(\"stroke\", t.grid);\n      axis.select(\".domain\").remove();\n    }\n  });\n\nsvg.call(zoom);\n\n// --- Legend: density gradient -------------------------------------------\nconst legendX = offsetX + mapW + 55;\nconst legendW = 26;\n\nconst gradient = svg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", \"imprintSeqGradient\")\n  .attr(\"x1\", \"0%\")\n  .attr(\"y1\", \"100%\")\n  .attr(\"x2\", \"0%\")\n  .attr(\"y2\", \"0%\");\ngradient\n  .selectAll(\"stop\")\n  .data(d3.range(0, 1.0001, 0.1))\n  .join(\"stop\")\n  .attr(\"offset\", (d) => `${d * 100}%`)\n  .attr(\"stop-color\", (d) => d3.interpolateRgbBasis(t.seq)(d));\n\nsvg\n  .append(\"rect\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", offsetY)\n  .attr(\"width\", legendW)\n  .attr(\"height\", mapH)\n  .attr(\"fill\", \"url(#imprintSeqGradient)\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendW / 2)\n  .attr(\"y\", offsetY - 18)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Pickup density\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendW + 8)\n  .attr(\"y\", offsetY + mapH - 4)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"Low\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendW + 8)\n  .attr(\"y\", offsetY + 12)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"High\");\n\n// --- Title ---------------------------------------------------------------\nconst titleText = \"Downtown Ride-Share Pickup Density · heatmap-geographic · javascript · d3 · anyplot.ai\";\nconst defaultTitleSize = 24;\nconst titleSize =\n  titleText.length > 67 ? Math.max(16, Math.round((defaultTitleSize * 67) / titleText.length)) : defaultTitleSize;\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(titleText);\n"}