{"spec_id":"hexbin-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// hexbin-basic: Basic Hexbin Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-08-25\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: simulated GPS pings around three city hotspots (deterministic LCG) ---\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\n\nfunction 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\nconst hotspots = [\n  { cx: 30, cy: 62, sx: 9, sy: 7, n: 8000 }, // downtown core\n  { cx: 74, cy: 50, sx: 11, sy: 9, n: 6200 }, // stadium district\n  { cx: 48, cy: 18, sx: 7, sy: 6, n: 5000 }, // airport corridor\n];\n\nconst pings = [];\nhotspots.forEach(({ cx, cy, sx, sy, n }) => {\n  for (let i = 0; i < n; i++) {\n    pings.push([cx + gaussian() * sx, cy + gaussian() * sy]);\n  }\n});\n\nconst xs = pings.map((p) => p[0]);\nconst ys = pings.map((p) => p[1]);\nconst padX = (Math.max(...xs) - Math.min(...xs)) * 0.06;\nconst padY = (Math.max(...ys) - Math.min(...ys)) * 0.06;\nconst xMin = Math.min(...xs) - padX;\nconst xMax = Math.max(...xs) + padX;\nconst yMin = Math.min(...ys) - padY;\nconst yMax = Math.max(...ys) + padY;\n\n// --- Hexagonal binning geometry (pointy-top, pixel space — the standard\n// hexbin technique of binning post-projection so cells tile evenly on\n// screen regardless of the underlying data scale) -----------------------\nconst HEX_SIZE = 16; // center-to-vertex radius, CSS px in the 1600x900 mount\nconst SQRT3 = Math.sqrt(3);\n\nfunction axialRound(qf, rf) {\n  const x = qf;\n  const z = rf;\n  const y = -x - z;\n  let rx = Math.round(x);\n  let ry = Math.round(y);\n  let rz = Math.round(z);\n  const dx = Math.abs(rx - x);\n  const dy = Math.abs(ry - y);\n  const dz = Math.abs(rz - z);\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];\n}\n\nfunction pixelToAxial(px, py, size) {\n  const qf = ((SQRT3 / 3) * px - (1 / 3) * py) / size;\n  const rf = ((2 / 3) * py) / size;\n  return axialRound(qf, rf);\n}\n\nfunction axialToPixel(q, r, size) {\n  return [size * (SQRT3 * q + (SQRT3 / 2) * r), size * (1.5 * r)];\n}\n\nfunction hexPath(cx, cy, size) {\n  const corners = [];\n  for (let i = 0; i < 6; i++) {\n    const angle = (Math.PI / 180) * (60 * i - 30);\n    corners.push(`${cx + size * Math.cos(angle)},${cy + size * Math.sin(angle)}`);\n  }\n  return `M ${corners.join(\" L \")} Z`;\n}\n\n// --- Chart -------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    marginRight: 130,\n    events: {\n      load: function () {\n        const chart = this;\n        const xAxis = chart.xAxis[0];\n        const yAxis = chart.yAxis[0];\n\n        // Bin every ping into a pixel-space hex cell.\n        const bins = new Map();\n        pings.forEach(([x, y]) => {\n          const px = xAxis.toPixels(x, false);\n          const py = yAxis.toPixels(y, false);\n          const [q, r] = pixelToAxial(px, py, HEX_SIZE);\n          const key = `${q},${r}`;\n          bins.set(key, (bins.get(key) || 0) + 1);\n        });\n\n        const counts = Array.from(bins.values());\n        const cMin = Math.min(...counts);\n        const cMax = Math.max(...counts);\n        const logMin = Math.log1p(cMin);\n        const logSpan = Math.log1p(cMax) - logMin || 1;\n\n        const seqLo = Highcharts.color(t.seq[0]);\n        const seqHi = Highcharts.color(t.seq[1]);\n\n        const hexGroup = chart.renderer.g(\"hexbin-cells\").add();\n        bins.forEach((count, key) => {\n          const [q, r] = key.split(\",\").map(Number);\n          const [cx, cy] = axialToPixel(q, r, HEX_SIZE);\n          const norm = (Math.log1p(count) - logMin) / logSpan;\n          chart.renderer\n            .path()\n            .attr({\n              d: hexPath(cx, cy, HEX_SIZE * 0.96),\n              fill: seqLo.tweenTo(seqHi, norm),\n              stroke: t.pageBg,\n              \"stroke-width\": 1,\n            })\n            .add(hexGroup);\n        });\n\n        // Manual continuous color bar — colorAxis lives in a module that\n        // isn't loaded in the core bundle, so the density scale is drawn\n        // directly with the SVG renderer, using Highcharts' native gradient\n        // fill (SVGRenderer.complexColor, core — no module needed).\n        const barWidth = 18;\n        const barX = chart.chartWidth - barWidth - 78;\n        const barTop = chart.plotTop + 6;\n        const barHeight = chart.plotHeight - 12;\n\n        chart.renderer\n          .rect(barX, barTop, barWidth, barHeight)\n          .attr({\n            fill: {\n              linearGradient: { x1: 0, y1: 1, x2: 0, y2: 0 },\n              stops: [\n                [0, t.seq[0]],\n                [1, t.seq[1]],\n              ],\n            },\n            stroke: t.inkSoft,\n            \"stroke-width\": 1,\n          })\n          .add();\n\n        chart.renderer\n          .text(\"Pings / bin\", barX - 6, chart.plotTop - 10)\n          .css({ color: t.inkSoft, fontSize: \"13px\" })\n          .add();\n        chart.renderer\n          .text(String(cMax), barX + barWidth + 8, barTop + 12)\n          .css({ color: t.inkSoft, fontSize: \"13px\" })\n          .add();\n        chart.renderer\n          .text(String(cMin), barX + barWidth + 8, barTop + barHeight)\n          .css({ color: t.inkSoft, fontSize: \"13px\" })\n          .add();\n      },\n    },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"hexbin-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  legend: { enabled: false },\n  plotOptions: { series: { animation: false } },\n  xAxis: {\n    min: xMin,\n    max: xMax,\n    startOnTick: false,\n    endOnTick: false,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    title: {\n      text: \"Longitude offset from city center (km)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n  },\n  yAxis: {\n    min: yMin,\n    max: yMax,\n    startOnTick: false,\n    endOnTick: false,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    title: {\n      text: \"Latitude offset from city center (km)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n  },\n  // An invisible boundary series — Highcharts only lays out cartesian axes\n  // when at least one series uses them; the hexagons themselves are drawn\n  // by hand in the `load` handler above.\n  series: [\n    {\n      data: [\n        [xMin, yMin],\n        [xMax, yMax],\n      ],\n      marker: { enabled: false },\n      lineWidth: 0,\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n  ],\n});\n"}