{"spec_id":"contour-decision-boundary","library":"muix","language":"javascript","code":"// anyplot.ai\n// contour-decision-boundary: Decision Boundary Classifier Visualization\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-04\n\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Deterministic LCG (seed 42) — no Math.random() in the browser harness\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rng = makeLcg(42);\n\n// Box-Muller standard normal draw, fed by the LCG above.\nfunction gaussian() {\n  const u1 = Math.max(rng(), 1e-9);\n  const u2 = rng();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// --- Data: synthetic iris-like petal measurements, 3 species ---------------\n// Setosa is fully separable on these two features; versicolor and virginica\n// overlap near the boundary — the same pattern the real iris dataset shows,\n// which is what makes a few training points fall on the wrong side.\nconst SPECIES = [\n  { name: \"Setosa\", meanX: 1.5, meanY: 0.25, stdX: 0.17, stdY: 0.1, n: 50 },\n  { name: \"Versicolor\", meanX: 4.3, meanY: 1.3, stdX: 0.47, stdY: 0.2, n: 55 },\n  { name: \"Virginica\", meanX: 5.55, meanY: 2.03, stdX: 0.55, stdY: 0.27, n: 55 },\n];\n\nlet pointId = 0;\nconst trainingPoints = SPECIES.flatMap((sp, cls) =>\n  Array.from({ length: sp.n }, () => ({\n    id: `iris-${pointId++}`,\n    x: Math.round((sp.meanX + gaussian() * sp.stdX) * 100) / 100,\n    y: Math.round((sp.meanY + gaussian() * sp.stdY) * 100) / 100,\n    cls,\n  })),\n);\n\n// --- k-nearest-neighbors classifier (k=13, squared distance, majority vote) -\n// k=13 (vs. a smaller k) widens each point's neighborhood just enough to pull\n// in 2-3 genuine misclassifications near different spots along the\n// Versicolor/Virginica boundary, instead of only one.\nconst K = 13;\nfunction knnPredict(px, py, excludeId) {\n  const neighbors = [];\n  for (const p of trainingPoints) {\n    if (p.id === excludeId) continue;\n    const dx = px - p.x;\n    const dy = py - p.y;\n    neighbors.push({ d2: dx * dx + dy * dy, cls: p.cls });\n  }\n  neighbors.sort((a, b) => a.d2 - b.d2);\n  const votes = new Map();\n  for (let i = 0; i < K && i < neighbors.length; i += 1) {\n    votes.set(neighbors[i].cls, (votes.get(neighbors[i].cls) || 0) + 1);\n  }\n  let bestCls = 0;\n  let bestVotes = -1;\n  votes.forEach((count, cls) => {\n    if (count > bestVotes) {\n      bestVotes = count;\n      bestCls = cls;\n    }\n  });\n  return bestCls;\n}\n\n// Leave-one-out prediction — predicting a point against a set that includes\n// itself would trivially match, hiding real misclassifications.\ntrainingPoints.forEach((p) => {\n  p.predicted = knnPredict(p.x, p.y, p.id);\n  p.correct = p.predicted === p.cls;\n});\n\n// --- Dense mesh grid: classifier prediction at every cell -------------------\nconst GRID = 110;\nconst trainX = trainingPoints.map((p) => p.x);\nconst trainY = trainingPoints.map((p) => p.y);\nconst padX = (Math.max(...trainX) - Math.min(...trainX)) * 0.12;\nconst padY = (Math.max(...trainY) - Math.min(...trainY)) * 0.12;\nconst X_MIN = Math.min(...trainX) - padX;\nconst X_MAX = Math.max(...trainX) + padX;\nconst Y_MIN = Math.min(...trainY) - padY;\nconst Y_MAX = Math.max(...trainY) + padY;\n\nconst xs = Array.from({ length: GRID }, (_, i) => X_MIN + (i / (GRID - 1)) * (X_MAX - X_MIN));\nconst ys = Array.from({ length: GRID }, (_, j) => Y_MIN + (j / (GRID - 1)) * (Y_MAX - Y_MIN));\nconst classGrid = ys.map((y) => xs.map((x) => knnPredict(x, y)));\n\n// --- Region fill: rasterize the classifier's predicted class at every mesh\n// cell into an off-screen canvas, one pixel per cell, then stretch it under\n// the chart as a plain <img>. This paints a genuinely smooth, contiguous\n// region fill straight from the real grid predictions (no fake data) instead\n// of thousands of overlapping scatter markers, which read as a stippled dot\n// texture rather than a deliberate area fill.\nconst CLASS_COLORS = [t.palette[0], t.palette[1], t.palette[2]];\nconst REGION_ALPHA = 0.24;\n\nfunction hexToRgb(hex) {\n  return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];\n}\nconst classRgb = CLASS_COLORS.map(hexToRgb);\n\nfunction buildRegionFillDataUrl() {\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = GRID;\n  canvas.height = GRID;\n  const ctx = canvas.getContext(\"2d\");\n  const imageData = ctx.createImageData(GRID, GRID);\n  ys.forEach((_y, j) => {\n    // Canvas row 0 is the top of the image; ys[0] is Y_MIN (the bottom of the\n    // plot), so row = GRID - 1 - j flips the mesh into image space.\n    const row = GRID - 1 - j;\n    xs.forEach((_x, i) => {\n      const [r, g, b] = classRgb[classGrid[j][i]];\n      const idx = (row * GRID + i) * 4;\n      imageData.data[idx] = r;\n      imageData.data[idx + 1] = g;\n      imageData.data[idx + 2] = b;\n      imageData.data[idx + 3] = Math.round(REGION_ALPHA * 255);\n    });\n  });\n  ctx.putImageData(imageData, 0, 0);\n  return canvas.toDataURL();\n}\nconst regionFillDataUrl = buildRegionFillDataUrl();\n\n// --- Boundary trace: a staircase along the mesh cells where the predicted\n// class changes between neighbours — the actual decision boundary read off\n// the grid, not a fitted curve.\nfunction boundarySegments() {\n  const halfDx = (xs[1] - xs[0]) / 2;\n  const halfDy = (ys[1] - ys[0]) / 2;\n  const segs = [];\n  for (let j = 0; j < GRID; j += 1) {\n    for (let i = 0; i < GRID - 1; i += 1) {\n      if (classGrid[j][i] !== classGrid[j][i + 1]) {\n        const xm = (xs[i] + xs[i + 1]) / 2;\n        segs.push([\n          [xm, ys[j] - halfDy],\n          [xm, ys[j] + halfDy],\n        ]);\n      }\n    }\n  }\n  for (let j = 0; j < GRID - 1; j += 1) {\n    for (let i = 0; i < GRID; i += 1) {\n      if (classGrid[j][i] !== classGrid[j + 1][i]) {\n        const ym = (ys[j] + ys[j + 1]) / 2;\n        segs.push([\n          [xs[i] - halfDx, ym],\n          [xs[i] + halfDx, ym],\n        ]);\n      }\n    }\n  }\n  return segs;\n}\nconst boundary = boundarySegments();\n\n// --- SVG boundary overlay (rendered as a ScatterChart child) ---------------\nfunction BoundaryOverlay() {\n  const { left, top, width, height } = useDrawingArea();\n  const toSVG = (dx, dy) => [\n    left + ((dx - X_MIN) / (X_MAX - X_MIN)) * width,\n    top + (1 - (dy - Y_MIN) / (Y_MAX - Y_MIN)) * height,\n  ];\n  const d = boundary\n    .map(([p0, p1]) => {\n      const [x0, y0] = toSVG(p0[0], p0[1]);\n      const [x1, y1] = toSVG(p1[0], p1[1]);\n      return `M ${x0.toFixed(1)},${y0.toFixed(1)} L ${x1.toFixed(1)},${y1.toFixed(1)}`;\n    })\n    .join(\" \");\n  return <path d={d} stroke={t.ink} strokeWidth={2} strokeOpacity={0.55} fill=\"none\" />;\n}\n\n// --- Training-point overlay --------------------------------------------------\n// Correctly classified points keep their species color; misclassified points\n// switch to the amber warning anchor so the classifier's mistakes stand out\n// against the region fill.\nconst correctSeries = SPECIES.map((sp, cls) => ({\n  id: `species-${cls}`,\n  label: sp.name,\n  data: trainingPoints\n    .filter((p) => p.cls === cls && p.correct)\n    .map((p) => ({ x: p.x, y: p.y, id: p.id })),\n  color: CLASS_COLORS[cls],\n  markerSize: 9,\n}));\n\nconst misclassifiedSeries = {\n  id: \"misclassified\",\n  label: \"Misclassified\",\n  data: trainingPoints\n    .filter((p) => !p.correct)\n    .map((p) => ({ x: p.x, y: p.y, id: p.id })),\n  color: t.amber,\n  markerSize: 11,\n};\n\nconst TITLE = \"contour-decision-boundary · javascript · muix · anyplot.ai\";\n\n// Fixed pixel margin, shared by the ScatterChart's own `margin` prop and the\n// region-fill <img> below — both need to agree on exactly where the drawing\n// area starts.\nconst MARGIN = { left: 70, right: 200, top: 12, bottom: 58 };\n\n// --- Chart (default-exported component — the harness mounts it) -----------\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  const chartW = W - 64;\n  const chartH = H - 28 - 36 - 8;\n  const drawW = chartW - MARGIN.left - MARGIN.right;\n  const drawH = chartH - MARGIN.top - MARGIN.bottom;\n  return (\n    <Box\n      sx={{\n        width: W,\n        height: H,\n        display: \"flex\",\n        flexDirection: \"column\",\n        bgcolor: t.pageBg,\n        boxSizing: \"border-box\",\n        pt: \"28px\",\n        px: \"32px\",\n        pb: \"8px\",\n      }}\n    >\n      <Typography\n        component=\"div\"\n        sx={{ fontSize: 22, fontWeight: 500, color: t.ink, textAlign: \"center\", lineHeight: 1.3, mb: \"6px\", flexShrink: 0 }}\n      >\n        {TITLE}\n      </Typography>\n      <Box sx={{ position: \"relative\", width: chartW, height: chartH, flexShrink: 0 }}>\n        {/* Region-fill wash sits behind the chart; the ScatterChart itself stays\n            transparent so its dots and boundary line render crisply on top. */}\n        <Box\n          component=\"img\"\n          src={regionFillDataUrl}\n          alt=\"\"\n          sx={{ position: \"absolute\", left: MARGIN.left, top: MARGIN.top, width: drawW, height: drawH, pointerEvents: \"none\" }}\n        />\n        <ScatterChart\n          width={chartW}\n          height={chartH}\n          skipAnimation\n          tooltip={{ trigger: \"none\" }}\n          series={[...correctSeries, misclassifiedSeries]}\n          xAxis={[\n            {\n              min: X_MIN,\n              max: X_MAX,\n              label: \"Petal length (cm)\",\n              disableLine: true,\n              labelStyle: { fontSize: 15, fill: t.ink },\n              tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n            },\n          ]}\n          yAxis={[\n            {\n              min: Y_MIN,\n              max: Y_MAX,\n              label: \"Petal width (cm)\",\n              disableLine: true,\n              labelStyle: { fontSize: 15, fill: t.ink },\n              tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n            },\n          ]}\n          margin={MARGIN}\n          sx={{ position: \"relative\" }}\n          slotProps={{\n            legend: {\n              direction: \"column\",\n              position: { vertical: \"middle\", horizontal: \"right\" },\n              itemMarkWidth: 14,\n              itemMarkHeight: 14,\n              labelStyle: { fontSize: 13, fill: t.inkSoft },\n            },\n          }}\n        >\n          <BoundaryOverlay />\n        </ScatterChart>\n      </Box>\n    </Box>\n  );\n}\n"}