{"spec_id":"voronoi-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// voronoi-basic: Voronoi Diagram for Spatial Partitioning\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-02\n//# anyplot-orientation: square\n// anyplot.ai\n// voronoi-basic: Voronoi Diagram for Spatial Partitioning\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-02\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst TITLE = \"voronoi-basic · javascript · muix · anyplot.ai\";\n\n// --- Data: retail store locations across a city grid (in-memory, deterministic) ---\n// Small fixed-seed LCG — the browser has no seeded RNG.\nlet seed = 42;\nfunction nextRandom() {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n}\n\nconst STORE_COUNT = 18;\nconst DOMAIN_MIN = 0;\nconst DOMAIN_MAX = 100;\nconst MARGIN_DATA = 10; // keep sites off the very edge so every cell stays visible\n\nconst stores = Array.from({ length: STORE_COUNT }, (_, i) => ({\n  id: `S${i + 1}`,\n  x: DOMAIN_MIN + MARGIN_DATA + nextRandom() * (DOMAIN_MAX - DOMAIN_MIN - 2 * MARGIN_DATA),\n  y: DOMAIN_MIN + MARGIN_DATA + nextRandom() * (DOMAIN_MAX - DOMAIN_MIN - 2 * MARGIN_DATA),\n  monthlyRevenueK: Math.round(40 + nextRandom() * 160), // $k / month\n}));\n\nconst revenues = stores.map((s) => s.monthlyRevenueK);\nconst minRevenue = Math.min(...revenues);\nconst maxRevenue = Math.max(...revenues);\n\nfunction hexToRgb(hex) {\n  const clean = hex.replace(\"#\", \"\");\n  return {\n    r: parseInt(clean.substring(0, 2), 16),\n    g: parseInt(clean.substring(2, 4), 16),\n    b: parseInt(clean.substring(4, 6), 16),\n  };\n}\n\nfunction revenueColor(value) {\n  const ratio = (value - minRevenue) / (maxRevenue - minRevenue || 1);\n  const a = hexToRgb(t.seq[0]);\n  const b = hexToRgb(t.seq[1]);\n  const r = Math.round(a.r + (b.r - a.r) * ratio);\n  const g = Math.round(a.g + (b.g - a.g) * ratio);\n  const bl = Math.round(a.b + (b.b - a.b) * ratio);\n  return `rgb(${r}, ${g}, ${bl})`;\n}\n\n// --- Voronoi geometry: half-plane intersection via Sutherland-Hodgman clip --\n// Each cell starts as the bounding box, then gets clipped by the perpendicular\n// bisector half-plane against every other seed (the region strictly closer to\n// this site than to that one). No external geometry library — this is plain\n// polygon math, not a charting engine.\nconst BBOX = [\n  [DOMAIN_MIN, DOMAIN_MIN],\n  [DOMAIN_MAX, DOMAIN_MIN],\n  [DOMAIN_MAX, DOMAIN_MAX],\n  [DOMAIN_MIN, DOMAIN_MAX],\n];\n\nfunction clipHalfPlane(polygon, site, other) {\n  const midX = (site.x + other.x) / 2;\n  const midY = (site.y + other.y) / 2;\n  const dirX = other.x - site.x;\n  const dirY = other.y - site.y;\n  const side = ([px, py]) => (px - midX) * dirX + (py - midY) * dirY;\n\n  const output = [];\n  for (let i = 0; i < polygon.length; i++) {\n    const curr = polygon[i];\n    const prev = polygon[(i - 1 + polygon.length) % polygon.length];\n    const sCurr = side(curr);\n    const sPrev = side(prev);\n    const currInside = sCurr <= 0;\n    const prevInside = sPrev <= 0;\n\n    if (currInside !== prevInside) {\n      const ratio = sPrev / (sPrev - sCurr);\n      output.push([prev[0] + ratio * (curr[0] - prev[0]), prev[1] + ratio * (curr[1] - prev[1])]);\n    }\n    if (currInside) output.push(curr);\n  }\n  return output;\n}\n\nfunction voronoiCell(site, sites) {\n  let polygon = BBOX;\n  for (const other of sites) {\n    if (other.id === site.id || polygon.length === 0) continue;\n    polygon = clipHalfPlane(polygon, site, other);\n  }\n  return polygon;\n}\n\nconst cells = stores.map((site) => ({ site, polygon: voronoiCell(site, stores) }));\n\n// --- Overlay: Voronoi cells, fill encodes store revenue -----------------------\nfunction VoronoiCells() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n\n  return (\n    <g>\n      {cells.map(({ site, polygon }) => {\n        if (polygon.length < 3) return null;\n        const points = polygon.map(([px, py]) => `${xScale(px)},${yScale(py)}`).join(\" \");\n        return (\n          <polygon\n            key={site.id}\n            points={points}\n            fill={revenueColor(site.monthlyRevenueK)}\n            fillOpacity={0.82}\n            stroke={t.pageBg}\n            strokeWidth={4.5}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Overlay: seed markers (store locations, brand green) --------------------\nfunction SeedMarkers() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n\n  return (\n    <g>\n      {stores.map((s) => (\n        <circle key={s.id} cx={xScale(s.x)} cy={yScale(s.y)} r={9} fill={t.palette[0]} stroke={t.pageBg} strokeWidth={2.5} />\n      ))}\n    </g>\n  );\n}\n\n// --- Overlay: title drawn in the reserved top margin --------------------------\nfunction DiagramTitle() {\n  const { width } = window.ANYPLOT_SIZE;\n  return (\n    <text x={width / 2} y={40} textAnchor=\"middle\" dominantBaseline=\"hanging\" fontSize={22} fontWeight={500} fill={t.ink}>\n      {TITLE}\n    </text>\n  );\n}\n\n// --- Overlay: sequential color-scale legend for the cell fill -----------------\nfunction RevenueLegend() {\n  const drawingArea = useDrawingArea();\n  const legendWidth = 240;\n  const legendX = drawingArea.left + drawingArea.width - legendWidth;\n  const legendY = drawingArea.top - 40;\n  return (\n    <g>\n      <defs>\n        <linearGradient id=\"revenueGradient\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n          <stop offset=\"0%\" stopColor={t.seq[0]} />\n          <stop offset=\"100%\" stopColor={t.seq[1]} />\n        </linearGradient>\n      </defs>\n      <text x={legendX} y={legendY - 18} fontSize={14} fill={t.inkSoft}>\n        Store monthly revenue ($k)\n      </text>\n      <rect\n        x={legendX}\n        y={legendY}\n        width={legendWidth}\n        height={14}\n        fill=\"url(#revenueGradient)\"\n        rx={7}\n        stroke={t.grid}\n        strokeWidth={1}\n      />\n      <text x={legendX} y={legendY + 30} fontSize={13} fill={t.inkSoft}>\n        {`$${minRevenue}k`}\n      </text>\n      <text x={legendX + legendWidth} y={legendY + 30} textAnchor=\"end\" fontSize={13} fill={t.inkSoft}>\n        {`$${maxRevenue}k`}\n      </text>\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) --------------\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={window.ANYPLOT_SIZE.width}\n      height={window.ANYPLOT_SIZE.height}\n      margin={{ top: 150, right: 90, bottom: 100, left: 100 }}\n      series={[]}\n      skipAnimation\n      disableAxisListener\n      xAxis={[\n        {\n          scaleType: \"linear\",\n          min: DOMAIN_MIN,\n          max: DOMAIN_MAX,\n          label: \"X coordinate (km)\",\n          labelStyle: { fontSize: 16, fill: t.ink },\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        },\n      ]}\n      yAxis={[\n        {\n          scaleType: \"linear\",\n          min: DOMAIN_MIN,\n          max: DOMAIN_MAX,\n          label: \"Y coordinate (km)\",\n          labelStyle: { fontSize: 16, fill: t.ink },\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        },\n      ]}\n    >\n      <VoronoiCells />\n      <SeedMarkers />\n      <ChartsXAxis />\n      <ChartsYAxis />\n      <DiagramTitle />\n      <RevenueLegend />\n    </ChartContainer>\n  );\n}\n"}