{"spec_id":"contour-decision-boundary","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// contour-decision-boundary: Decision Boundary Classifier Visualization\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-04\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) + Gaussian via Box-Muller ---------------------\nlet lcgState = 42;\nconst rand = () => {\n  lcgState = (lcgState * 1664525 + 1013904223) % 4294967296;\n  return lcgState / 4294967296;\n};\nconst 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\n// --- Data: two interleaving crescents (make_moons-style), 220 points -------\nconst perMoon = 110;\nconst noiseStd = 0.18;\nconst trainX1 = [];\nconst trainX2 = [];\nconst trainY = [];\n\nfor (let i = 0; i < perMoon; i++) {\n  const theta = (Math.PI * i) / (perMoon - 1);\n  trainX1.push(Math.cos(theta) + gaussian() * noiseStd);\n  trainX2.push(Math.sin(theta) + gaussian() * noiseStd);\n  trainY.push(0);\n}\nfor (let i = 0; i < perMoon; i++) {\n  const theta = (Math.PI * i) / (perMoon - 1);\n  trainX1.push(1 - Math.cos(theta) + gaussian() * noiseStd);\n  trainX2.push(0.5 - Math.sin(theta) + gaussian() * noiseStd);\n  trainY.push(1);\n}\n\n// --- k-NN classifier (k=15, majority vote on squared Euclidean distance) ---\nconst K = 15;\nconst knnPredict = (px, py) => {\n  const dists = new Array(trainX1.length);\n  for (let i = 0; i < trainX1.length; i++) {\n    const dx = px - trainX1[i];\n    const dy = py - trainX2[i];\n    dists[i] = { d: dx * dx + dy * dy, y: trainY[i] };\n  }\n  dists.sort((a, b) => a.d - b.d);\n  let votes0 = 0;\n  let votes1 = 0;\n  for (let i = 0; i < K; i++) {\n    if (dists[i].y === 0) votes0++;\n    else votes1++;\n  }\n  return votes0 >= votes1 ? 0 : 1;\n};\n\nconst trainPred = trainX1.map((x, i) => knnPredict(x, trainX2[i]));\n\n// --- Mesh grid over feature space, classified to paint decision regions ----\nconst pad = 0.5;\nconst xMin = Math.min(...trainX1) - pad;\nconst xMax = Math.max(...trainX1) + pad;\nconst yMin = Math.min(...trainX2) - pad;\nconst yMax = Math.max(...trainX2) + pad;\nconst nx = 120;\nconst ny = 110;\nconst stepX = (xMax - xMin) / (nx - 1);\nconst stepY = (yMax - yMin) / (ny - 1);\n\n// Each mesh point becomes an exact half-step-wide rectangle so the plugin\n// below can tile the decision regions with no gaps and no overlap.\nconst cellsA = [];\nconst cellsB = [];\nfor (let i = 0; i < nx; i++) {\n  const x = xMin + stepX * i;\n  for (let j = 0; j < ny; j++) {\n    const y = yMin + stepY * j;\n    (knnPredict(x, y) === 0 ? cellsA : cellsB).push({\n      x0: x - stepX / 2,\n      x1: x + stepX / 2,\n      y0: y - stepY / 2,\n      y1: y + stepY / 2,\n    });\n  }\n}\n\n// --- Split training points into correct / misclassified per class ---------\nconst groups = { a0: [], a1: [], b0: [], b1: [] };\ntrainX1.forEach((x, i) => {\n  const y = trainX2[i];\n  const correct = trainPred[i] === trainY[i];\n  if (trainY[i] === 0) (correct ? groups.a0 : groups.a1).push({ x, y });\n  else (correct ? groups.b0 : groups.b1).push({ x, y });\n});\n\n// --- Colors ------------------------------------------------------------\nconst hexToRgba = (hex, alpha) => {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n};\nconst colorA = t.palette[0]; // #009E73 brand green — Class A\nconst colorB = t.palette[1]; // lavender — Class B\nconst fillA = hexToRgba(colorA, 0.25);\nconst fillB = hexToRgba(colorB, 0.25);\n\n// --- Decision-region plugin --------------------------------------------\n// A point-marker mesh approximates a filled contour with translucent rects,\n// but neighboring markers never tile perfectly against the axis grid\n// spacing, leaving a crosshatch of background/gridline pixels showing\n// through. Painting exact pixel rectangles from the scales at draw time\n// (one per mesh cell, edge-to-edge, no overlap) gives a truly smooth fill.\nconst decisionRegionPlugin = {\n  id: \"decisionRegions\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    ctx.save();\n    ctx.beginPath();\n    ctx.rect(\n      chartArea.left,\n      chartArea.top,\n      chartArea.right - chartArea.left,\n      chartArea.bottom - chartArea.top,\n    );\n    ctx.clip();\n    const paintCells = (cells, color) => {\n      ctx.fillStyle = color;\n      for (const c of cells) {\n        const px0 = scales.x.getPixelForValue(c.x0);\n        const px1 = scales.x.getPixelForValue(c.x1);\n        const py0 = scales.y.getPixelForValue(c.y0);\n        const py1 = scales.y.getPixelForValue(c.y1);\n        ctx.fillRect(\n          Math.min(px0, px1),\n          Math.min(py0, py1),\n          Math.abs(px1 - px0),\n          Math.abs(py1 - py0),\n        );\n      }\n    };\n    paintCells(cellsA, fillA);\n    paintCells(cellsB, fillB);\n    ctx.restore();\n  },\n};\n\n// --- Mount -----------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart -----------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        label: \"Class A\",\n        data: groups.a0,\n        backgroundColor: colorA,\n        borderColor: t.pageBg,\n        borderWidth: 1.5,\n        pointStyle: \"circle\",\n        pointRadius: 9,\n        pointHoverRadius: 9,\n      },\n      {\n        label: \"Class B\",\n        data: groups.b0,\n        backgroundColor: colorB,\n        borderColor: t.pageBg,\n        borderWidth: 1.5,\n        pointStyle: \"circle\",\n        pointRadius: 9,\n        pointHoverRadius: 9,\n      },\n      // Misclassified markers are drawn last so a correctly-classified\n      // point never occludes a rarer, more important misclassified one.\n      {\n        label: \"Class A (misclassified)\",\n        data: groups.a1,\n        backgroundColor: colorA,\n        borderColor: t.ink,\n        borderWidth: 2,\n        pointStyle: \"triangle\",\n        pointRadius: 10,\n        pointHoverRadius: 10,\n      },\n      {\n        label: \"Class B (misclassified)\",\n        data: groups.b1,\n        backgroundColor: colorB,\n        borderColor: t.ink,\n        borderWidth: 2,\n        pointStyle: \"triangle\",\n        pointRadius: 10,\n        pointHoverRadius: 10,\n      },\n    ],\n  },\n  plugins: [decisionRegionPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"contour-decision-boundary · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 16 },\n      },\n      legend: {\n        position: \"top\",\n        labels: {\n          color: t.ink,\n          font: { size: 15 },\n          boxWidth: 16,\n        },\n      },\n    },\n    scales: {\n      x: {\n        min: xMin,\n        max: xMax,\n        title: {\n          display: true,\n          text: \"Feature 1 (X1)\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n      },\n      y: {\n        min: yMin,\n        max: yMax,\n        title: {\n          display: true,\n          text: \"Feature 2 (X2)\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n      },\n    },\n  },\n});\n"}