{"spec_id":"hexbin-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// hexbin-basic: Basic Hexbin Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-25\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Tiny fixed-seed LCG + Box-Muller (no seeded RNG in the browser) -------\nlet seed = 42;\nconst rand = () => {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n};\nconst randNormal = () => {\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: ride-share pickup requests across a metro area (km from center) -\nconst clusters = [\n  { cx: -3.2, cy: 2.1, sx: 1.1, sy: 0.9, n: 2200 }, // downtown core\n  { cx: 2.6, cy: -1.4, sx: 1.6, sy: 1.3, n: 1600 }, // transit hub\n  { cx: -0.5, cy: -3.0, sx: 0.9, sy: 0.7, n: 900 }, // stadium district\n];\nconst points = [];\nfor (const c of clusters) {\n  for (let i = 0; i < c.n; i++) {\n    points.push([c.cx + randNormal() * c.sx, c.cy + randNormal() * c.sy]);\n  }\n}\n// Diffuse background demand across the wider metro area.\nfor (let i = 0; i < 1300; i++) {\n  points.push([(rand() * 2 - 1) * 7, (rand() * 2 - 1) * 5]);\n}\n\n// --- Layout -------------------------------------------------------------\nconst margin = { top: 110, right: 200, bottom: 100, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Scales ---------------------------------------------------------------\nconst [x0, x1] = d3.extent(points, (d) => d[0]);\nconst [y0, y1] = d3.extent(points, (d) => d[1]);\nconst xPad = (x1 - x0) * 0.04;\nconst yPad = (y1 - y0) * 0.04;\nconst x = d3.scaleLinear().domain([x0 - xPad, x1 + xPad]).range([0, iw]);\nconst y = d3.scaleLinear().domain([y0 - yPad, y1 + yPad]).range([ih, 0]);\n\n// --- Hexagonal binning ------------------------------------------------------\n// Bins run on pixel space (after x/y scaling), using a pointy-top axial hex\n// grid: convert each point to fractional axial (q, r), then round to the\n// nearest hex via cube-coordinate rounding (the standard technique for\n// snapping to the closest hexagon center on a regular grid).\nconst gridSize = 26; // hexagons across the plot width — controls bin resolution\nconst hexRadius = iw / gridSize / Math.sqrt(3);\n\nconst cubeRound = (xf, yf, zf) => {\n  let rx = Math.round(xf);\n  let ry = Math.round(yf);\n  let rz = Math.round(zf);\n  const dx = Math.abs(rx - xf);\n  const dy = Math.abs(ry - yf);\n  const dz = Math.abs(rz - zf);\n  if (dx > dy && dx > dz) rx = -ry - rz;\n  else if (dy > dz) ry = -rx - rz;\n  else rz = -rx - ry;\n  return [rx, rz]; // axial (q, r)\n};\nconst pixelToHex = (px, py) => {\n  const qf = ((Math.sqrt(3) / 3) * px - py / 3) / hexRadius;\n  const rf = ((2 / 3) * py) / hexRadius;\n  return cubeRound(qf, -qf - rf, rf);\n};\nconst hexCenter = (q, r) => [hexRadius * Math.sqrt(3) * (q + r / 2), hexRadius * 1.5 * r];\nconst hexPath = (cx, cy, r) => {\n  const pts = d3.range(6).map((i) => {\n    const angle = (Math.PI / 180) * (60 * i - 30);\n    return [cx + r * Math.cos(angle), cy + r * Math.sin(angle)];\n  });\n  return `M${pts.map((p) => p.join(\",\")).join(\"L\")}Z`;\n};\n\nconst counts = new Map();\nfor (const [px, py] of points) {\n  const [q, r] = pixelToHex(x(px), y(py));\n  const key = `${q},${r}`;\n  counts.set(key, (counts.get(key) || 0) + 1);\n}\nconst bins = Array.from(counts, ([key, count]) => {\n  const [q, r] = key.split(\",\").map(Number);\n  const [cx, cy] = hexCenter(q, r);\n  return { cx, cy, count };\n});\nconst maxCount = d3.max(bins, (d) => d.count);\n\n// Sqrt-transformed domain so the single densest hexagon doesn't swamp the\n// scale (the spec's \"consider log scale when density varies widely\"); sqrt is\n// preferred over log here because bin counts start at 1 (log(1) = 0 would\n// collapse the low end of the ramp) and the count range (1-155) is moderate\n// enough that sqrt already spreads it well without a log's extreme low-end stretch.\nconst colorScale = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([0, Math.sqrt(maxCount)]);\nconst hotspot = bins.reduce((best, d) => (d.count > best.count ? d : best), bins[0]);\n\n// --- SVG mount --------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\nconst drawRadius = hexRadius * 0.98; // hairline gap between neighboring hexes\ng.selectAll(\".hex\")\n  .data(bins)\n  .join(\"path\")\n  .attr(\"class\", \"hex\")\n  .attr(\"d\", (d) => hexPath(d.cx, d.cy, drawRadius))\n  .attr(\"fill\", (d) => colorScale(Math.sqrt(d.count)))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 0.75);\n\n// Direct on-chart label for the densest hexagon (the \"downtown core\" cluster),\n// with a page-background halo stroke so it reads over any hex fill color.\ng.append(\"text\")\n  .attr(\"x\", hotspot.cx)\n  .attr(\"y\", hotspot.cy - drawRadius - 10)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .style(\"paint-order\", \"stroke\")\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 4)\n  .attr(\"stroke-linejoin\", \"round\")\n  .text(\"Downtown core\");\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(8));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"East–West distance from city center (km)\");\n\nsvg\n  .append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(margin.top + ih / 2))\n  .attr(\"y\", 24)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"North–South distance from city center (km)\");\n\n// --- Color legend: vertical Imprint-seq gradient bar -----------------------\nconst legendX = margin.left + iw + 50;\nconst legendTop = margin.top;\nconst legendH = ih * 0.65;\nconst legendW = 26;\n\nconst grad = svg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", \"hexbin-seq\")\n  .attr(\"x1\", \"0%\")\n  .attr(\"y1\", \"100%\")\n  .attr(\"x2\", \"0%\")\n  .attr(\"y2\", \"0%\");\nd3.range(0, 101, 10).forEach((p) => {\n  grad.append(\"stop\").attr(\"offset\", `${p}%`).attr(\"stop-color\", colorScale(Math.sqrt(maxCount * (p / 100))));\n});\n\nsvg.append(\"rect\").attr(\"x\", legendX).attr(\"y\", legendTop).attr(\"width\", legendW).attr(\"height\", legendH).attr(\"fill\", \"url(#hexbin-seq)\");\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendW + 12)\n  .attr(\"y\", legendTop)\n  .attr(\"dominant-baseline\", \"hanging\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(maxCount.toLocaleString());\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendW + 12)\n  .attr(\"y\", legendTop + legendH)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"1\");\nsvg\n  .append(\"text\")\n  .attr(\"transform\", `translate(${legendX - 14}, ${legendTop + legendH / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .text(\"Pickups per bin\");\n\n// --- Title & subtitle ---------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"32px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"hexbin-basic · javascript · d3 · anyplot.ai\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 82)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(`${points.length.toLocaleString()} ride-share pickup requests, binned into ${bins.length} hexagons`);\n"}