{"spec_id":"contour-decision-boundary","library":"d3","language":"javascript","code":"// anyplot.ai\n// contour-decision-boundary: Decision Boundary Classifier Visualization\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-04\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 150, right: 90, bottom: 110, left: 130 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Deterministic data: two interleaving quality-inspection clusters ------\n// A tiny fixed-seed LCG stands in for a seeded RNG (the browser has none).\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);\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\n// Two crescent-shaped clusters (classic nonlinear separation) rescaled onto\n// realistic inspection measurements: vibration amplitude vs. temperature\n// deviation from two batches of manufactured parts (Pass / Fail).\nconst PER_CLASS = 125;\nconst NOISE = 0.22;\nconst X1_SCALE = 3.4;\nconst X1_OFFSET = 6.2;\nconst X2_SCALE = 4.6;\nconst X2_OFFSET = 3.0;\n\nconst points = [];\nfor (let i = 0; i < PER_CLASS; i++) {\n  const angle = (Math.PI * i) / (PER_CLASS - 1);\n  points.push({\n    x1: (Math.cos(angle) + gaussian() * NOISE) * X1_SCALE + X1_OFFSET,\n    x2: (Math.sin(angle) + gaussian() * NOISE) * X2_SCALE + X2_OFFSET,\n    label: 0, // Pass\n  });\n}\nfor (let i = 0; i < PER_CLASS; i++) {\n  const angle = (Math.PI * i) / (PER_CLASS - 1);\n  points.push({\n    x1: (1 - Math.cos(angle) + gaussian() * NOISE) * X1_SCALE + X1_OFFSET,\n    x2: (1 - Math.sin(angle) - 0.5 + gaussian() * NOISE) * X2_SCALE + X2_OFFSET,\n    label: 1, // Fail\n  });\n}\n\n// --- k-NN classifier (standardized feature space, k=5) ----------------------\nconst K = 5;\nconst mean1 = d3.mean(points, (d) => d.x1);\nconst mean2 = d3.mean(points, (d) => d.x2);\nconst std1 = d3.deviation(points, (d) => d.x1);\nconst std2 = d3.deviation(points, (d) => d.x2);\nconst trainZ = points.map((d) => ({\n  zx: (d.x1 - mean1) / std1,\n  zy: (d.x2 - mean2) / std2,\n  label: d.label,\n}));\n\n// Fraction of the k nearest neighbors labeled Fail — a continuous field\n// suitable for marching-squares contouring (0 = unanimous Pass, 1 = unanimous Fail).\nfunction knnFailFraction(zx, zy, excludeIdx) {\n  const dists = [];\n  for (let i = 0; i < trainZ.length; i++) {\n    if (i === excludeIdx) continue;\n    const dx = zx - trainZ[i].zx;\n    const dy = zy - trainZ[i].zy;\n    dists.push([dx * dx + dy * dy, trainZ[i].label]);\n  }\n  dists.sort((a, b) => a[0] - b[0]);\n  let fail = 0;\n  for (let i = 0; i < K; i++) {\n    if (dists[i][1] === 1) fail++;\n  }\n  return fail / K;\n}\n\nfunction classify(zx, zy, excludeIdx) {\n  return knnFailFraction(zx, zy, excludeIdx) > 0.5 ? 1 : 0;\n}\n\n// Leave-one-out prediction flags which training points the classifier misses.\nconst trainWithPred = points.map((d, i) => {\n  const zx = (d.x1 - mean1) / std1;\n  const zy = (d.x2 - mean2) / std2;\n  return { ...d, correct: classify(zx, zy, i) === d.label };\n});\n\n// --- Scales -------------------------------------------------------------\nconst x1Pad = (d3.max(points, (d) => d.x1) - d3.min(points, (d) => d.x1)) * 0.12;\nconst x2Pad = (d3.max(points, (d) => d.x2) - d3.min(points, (d) => d.x2)) * 0.12;\nconst x = d3\n  .scaleLinear()\n  .domain([d3.min(points, (d) => d.x1) - x1Pad, d3.max(points, (d) => d.x1) + x1Pad])\n  .range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([d3.min(points, (d) => d.x2) - x2Pad, d3.max(points, (d) => d.x2) + x2Pad])\n  .range([ih, 0]);\n\n// --- Decision regions: marching-squares contour of the classifier field ----\n// Sample a dense grid (edge-to-edge, so the contoured fill reaches the plot\n// borders) of the continuous Fail-fraction field, then let d3.contours()\n// trace a single smooth boundary per class instead of a raster mesh of rects.\nconst GRID = 100;\nconst cellW = iw / (GRID - 1);\nconst cellH = ih / (GRID - 1);\nconst classColors = [t.palette[0], t.palette[4]]; // Pass -> brand green, Fail -> semantic red\n\nconst failField = new Float64Array(GRID * GRID);\nfor (let row = 0; row < GRID; row++) {\n  const dataX2 = y.invert(row * cellH);\n  const zy = (dataX2 - mean2) / std2;\n  for (let col = 0; col < GRID; col++) {\n    const dataX1 = x.invert(col * cellW);\n    const zx = (dataX1 - mean1) / std1;\n    failField[row * GRID + col] = knnFailFraction(zx, zy, -1);\n  }\n}\nconst passField = failField.map((v) => 1 - v);\n\nconst contourGen = d3.contours().size([GRID, GRID]);\nconst failGeo = contourGen.contour(failField, 0.5);\nconst passGeo = contourGen.contour(passField, 0.5);\n\n// Grid-index space -> plot-pixel space (grid samples are cellW/cellH apart).\nconst gridToPixel = d3.geoTransform({\n  point(gx, gy) {\n    this.stream.point(gx * cellW, gy * cellH);\n  },\n});\nconst contourPath = d3.geoPath(gridToPixel);\n\n// A distinct marker shape for Fail (in addition to color) so class is never\n// signaled by color alone.\nconst failSymbol = d3.symbol().type(d3.symbolSquare).size(190);\n\n// --- SVG mount ----------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Decision regions (drawn first, everything else layers on top)\nconst regionsG = g.append(\"g\");\nregionsG\n  .append(\"path\")\n  .datum(passGeo)\n  .attr(\"d\", contourPath)\n  .attr(\"fill\", classColors[0])\n  .attr(\"stroke\", \"none\")\n  .attr(\"opacity\", 0.22);\nregionsG\n  .append(\"path\")\n  .datum(failGeo)\n  .attr(\"d\", contourPath)\n  .attr(\"fill\", classColors[1])\n  .attr(\"stroke\", \"none\")\n  .attr(\"opacity\", 0.22);\n\n// --- Axes -----------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(8));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Vibration Amplitude (mm/s)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -96)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Temperature Deviation (°C)\");\n\n// --- Training points: Pass = filled circle, Fail = filled square (shape\n// carries the class distinction alongside color); misclassified points also\n// get an ink ring -------------------------------------------------------\nconst pointsG = g.append(\"g\");\npointsG\n  .selectAll(\"circle.sample\")\n  .data(trainWithPred.filter((d) => d.label === 0))\n  .join(\"circle\")\n  .attr(\"class\", \"sample\")\n  .attr(\"cx\", (d) => x(d.x1))\n  .attr(\"cy\", (d) => y(d.x2))\n  .attr(\"r\", 8)\n  .attr(\"fill\", classColors[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\npointsG\n  .selectAll(\"path.sample\")\n  .data(trainWithPred.filter((d) => d.label === 1))\n  .join(\"path\")\n  .attr(\"class\", \"sample\")\n  .attr(\"d\", failSymbol())\n  .attr(\"transform\", (d) => `translate(${x(d.x1)},${y(d.x2)})`)\n  .attr(\"fill\", classColors[1])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\npointsG\n  .selectAll(\"circle.flag\")\n  .data(trainWithPred.filter((d) => !d.correct))\n  .join(\"circle\")\n  .attr(\"class\", \"flag\")\n  .attr(\"cx\", (d) => x(d.x1))\n  .attr(\"cy\", (d) => y(d.x2))\n  .attr(\"r\", 12.5)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2);\n\n// --- Legend ---------------------------------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${width / 2 - 300},${102})`);\nconst legendItems = [\n  { label: \"Pass\", color: classColors[0], shape: \"dot\" },\n  { label: \"Fail\", color: classColors[1], shape: \"square\" },\n  { label: \"Misclassified\", color: t.ink, shape: \"ring\" },\n];\nlet lx = 0;\nfor (const item of legendItems) {\n  const entry = legend.append(\"g\").attr(\"transform\", `translate(${lx},0)`);\n  if (item.shape === \"dot\") {\n    entry.append(\"circle\").attr(\"r\", 9).attr(\"fill\", item.color);\n  } else if (item.shape === \"square\") {\n    entry.append(\"path\").attr(\"d\", failSymbol()).attr(\"fill\", item.color);\n  } else {\n    entry.append(\"circle\").attr(\"r\", 9).attr(\"fill\", \"none\").attr(\"stroke\", item.color).attr(\"stroke-width\", 2);\n  }\n  const label = entry\n    .append(\"text\")\n    .attr(\"x\", 18)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"16px\")\n    .text(item.label);\n  lx += 18 + label.node().getComputedTextLength() + 38;\n}\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 54)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"contour-decision-boundary · javascript · d3 · anyplot.ai\");\n"}