{"spec_id":"voronoi-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// voronoi-basic: Voronoi Diagram for Spatial Partitioning\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-02\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: retail store locations across a service area (km) ---------------\n// Fixed-seed LCG (Numerical Recipes constants, Math.imul keeps it 32-bit safe)\n// since the browser has no seeded RNG.\nfunction lcg(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nconst BOUNDS = { minX: 0, maxX: 100, minY: 0, maxY: 100 };\nconst storeNames = [\n  \"Downtown\", \"Riverside\", \"Hillcrest\", \"Northgate\", \"Eastwood\",\n  \"Westfield\", \"Lakeside\", \"Summit\", \"Brookline\", \"Fairview\",\n  \"Cedar Park\", \"Maple Grove\", \"Oakridge\", \"Pinehurst\", \"Meadowbrook\",\n  \"Sunnyvale\",\n];\n// Sites stay inset from the bbox edges so their data labels never collide\n// with the top/bottom plot boundary — the Voronoi cells still reach the\n// full BOUNDS since the clip polygon starts at the bbox corners.\nconst SITE_MARGIN = 8;\nconst stores = storeNames.map((label) => ({\n  x: BOUNDS.minX + SITE_MARGIN + rand() * (BOUNDS.maxX - BOUNDS.minX - 2 * SITE_MARGIN),\n  y: BOUNDS.minY + SITE_MARGIN + rand() * (BOUNDS.maxY - BOUNDS.minY - 2 * SITE_MARGIN),\n  label,\n}));\n\n// --- Voronoi cells: half-plane intersection per site, clipped to BOUNDS ----\n// Highcharts core has no polygon/voronoi series (only highcharts-more/modules\n// ship those, and they are not vendored here) — so each cell is a convex\n// polygon built by intersecting, for every other site, the half-plane closer\n// to this site than to that one (Sutherland-Hodgman clip against the bbox).\nfunction clipHalfPlane(poly, p, q) {\n  const mid = { x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 };\n  const dir = { x: q.x - p.x, y: q.y - p.y };\n  const side = (v) => (v.x - mid.x) * dir.x + (v.y - mid.y) * dir.y;\n  const intersect = (a, b) => {\n    const sa = side(a);\n    const sb = side(b);\n    const f = sa / (sa - sb);\n    return { x: a.x + f * (b.x - a.x), y: a.y + f * (b.y - a.y) };\n  };\n  const out = [];\n  for (let i = 0; i < poly.length; i++) {\n    const curr = poly[i];\n    const prev = poly[(i - 1 + poly.length) % poly.length];\n    const currIn = side(curr) <= 1e-9;\n    const prevIn = side(prev) <= 1e-9;\n    if (currIn) {\n      if (!prevIn) out.push(intersect(prev, curr));\n      out.push(curr);\n    } else if (prevIn) {\n      out.push(intersect(prev, curr));\n    }\n  }\n  return out;\n}\n\nfunction voronoiCell(site, others) {\n  let poly = [\n    { x: BOUNDS.minX, y: BOUNDS.minY },\n    { x: BOUNDS.maxX, y: BOUNDS.minY },\n    { x: BOUNDS.maxX, y: BOUNDS.maxY },\n    { x: BOUNDS.minX, y: BOUNDS.maxY },\n  ];\n  for (const other of others) {\n    if (poly.length === 0) break;\n    poly = clipHalfPlane(poly, site, other);\n  }\n  return poly;\n}\n\nconst cells = stores.map((site, i) => {\n  const others = stores.filter((_, j) => j !== i);\n  return { site, polygon: voronoiCell(site, others) };\n});\n\n// --- Adjacency-aware cell coloring -------------------------------------------\n// Two cells are adjacent when they share a clipped bisector edge (the two\n// polygons list that edge as exact reverses of one another). A flat\n// index-mod-palette cycle can land neighboring cells on the same hue; instead\n// run a small greedy graph coloring so every cell differs from its neighbors.\nfunction pointsClose(a, b, eps = 1e-6) {\n  return Math.abs(a.x - b.x) < eps && Math.abs(a.y - b.y) < eps;\n}\nfunction edgesOf(poly) {\n  return poly.map((v, idx) => [v, poly[(idx + 1) % poly.length]]);\n}\nfunction cellsAdjacent(polyA, polyB) {\n  for (const [a1, a2] of edgesOf(polyA)) {\n    for (const [b1, b2] of edgesOf(polyB)) {\n      if (pointsClose(a1, b2) && pointsClose(a2, b1)) return true;\n    }\n  }\n  return false;\n}\nconst adjacency = cells.map(() => new Set());\nfor (let i = 0; i < cells.length; i++) {\n  for (let j = i + 1; j < cells.length; j++) {\n    if (cellsAdjacent(cells[i].polygon, cells[j].polygon)) {\n      adjacency[i].add(j);\n      adjacency[j].add(i);\n    }\n  }\n}\nconst cellColors = cells.map(() => -1);\ncells.forEach((_, i) => {\n  const used = new Set();\n  adjacency[i].forEach((neighbor) => {\n    if (cellColors[neighbor] !== -1) used.add(cellColors[neighbor]);\n  });\n  let colorIdx = 0;\n  while (used.has(colorIdx % t.palette.length)) colorIdx++;\n  cellColors[i] = colorIdx % t.palette.length;\n});\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      load() {\n        const xAxis = this.xAxis[0];\n        const yAxis = this.yAxis[0];\n        const group = this.renderer.g(\"voronoi-cells\").add();\n        group.attr({ zIndex: 2 });\n        cells.forEach(({ polygon }, i) => {\n          if (polygon.length < 3) return;\n          const path = polygon.map((v, idx) => [\n            idx === 0 ? \"M\" : \"L\",\n            xAxis.toPixels(v.x),\n            yAxis.toPixels(v.y),\n          ]);\n          path.push([\"Z\"]);\n          this.renderer\n            .path(path)\n            .attr({\n              fill: Highcharts.color(t.palette[cellColors[i]])\n                .setOpacity(0.4)\n                .get(),\n              stroke: t.pageBg,\n              \"stroke-width\": 3,\n            })\n            .add(group);\n        });\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"voronoi-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    min: BOUNDS.minX,\n    max: BOUNDS.maxX,\n    title: { text: \"Distance east (km)\", 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: BOUNDS.minY,\n    max: BOUNDS.maxY,\n    title: { text: \"Distance north (km)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    gridLineWidth: 0,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: { enabled: false },\n  tooltip: {\n    pointFormat: \"<b>{point.name}</b><br/>({point.x:.1f}, {point.y:.1f}) km\",\n  },\n  plotOptions: {\n    series: { animation: false },\n  },\n  series: [\n    {\n      name: \"Store location\",\n      data: stores.map((s) => ({ x: s.x, y: s.y, name: s.label })),\n      color: t.ink,\n      marker: { radius: 9, fillColor: t.ink, lineColor: t.pageBg, lineWidth: 2 },\n      zIndex: 3,\n      dataLabels: {\n        enabled: true,\n        format: \"{point.name}\",\n        style: {\n          color: t.ink,\n          fontSize: \"13px\",\n          fontWeight: \"500\",\n          textOutline: \"none\",\n        },\n        y: -14,\n      },\n    },\n  ],\n});\n"}