{"spec_id":"contour-decision-boundary","library":"echarts","language":"javascript","code":"// anyplot.ai\n// contour-decision-boundary: Decision Boundary Classifier Visualization\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-04\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG + Box-Muller) ----------------------------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction gaussian(mean, std) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return mean + std * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\nfunction blend(hex, bgHex, weight) {\n  const c1 = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));\n  const c2 = [1, 3, 5].map((i) => parseInt(bgHex.slice(i, i + 2), 16));\n  const [r, g, b] = c1.map((v, i) => Math.round(v * weight + c2[i] * (1 - weight)));\n  return `rgb(${r}, ${g}, ${b})`;\n}\n\n// --- Training data: synthetic petal measurements, 3 overlapping species -----\nconst classColors = [t.palette[0], t.palette[1], t.palette[2]];\nconst classSpecs = [\n  { name: \"Setosa\", n: 50, lengthMean: 1.5, lengthStd: 0.18, widthMean: 0.25, widthStd: 0.09 },\n  { name: \"Versicolor\", n: 50, lengthMean: 4.3, lengthStd: 0.5, widthMean: 1.3, widthStd: 0.2 },\n  { name: \"Virginica\", n: 50, lengthMean: 5.6, lengthStd: 0.55, widthMean: 2.0, widthStd: 0.27 },\n];\n\nconst trainingPoints = []; // [length, width, classIdx]\nclassSpecs.forEach((spec, classIdx) => {\n  for (let i = 0; i < spec.n; i++) {\n    const length = Math.max(0.1, gaussian(spec.lengthMean, spec.lengthStd));\n    const width = Math.max(0.05, gaussian(spec.widthMean, spec.widthStd));\n    trainingPoints.push([length, width, classIdx]);\n  }\n});\n\n// --- k-nearest-neighbors classifier ------------------------------------------\n// Trained on `trainingPoints`; `excludeIdx` supports leave-one-out evaluation\n// so a training point never votes for itself.\nconst K = 9;\nfunction knnPredict(px, py, excludeIdx) {\n  const neighbors = [];\n  for (let i = 0; i < trainingPoints.length; i++) {\n    if (i === excludeIdx) continue;\n    const [x, y, label] = trainingPoints[i];\n    const dx = x - px;\n    const dy = y - py;\n    neighbors.push({ d: dx * dx + dy * dy, label });\n  }\n  neighbors.sort((a, b) => a.d - b.d);\n  const votes = new Map();\n  let bestLabel = neighbors[0].label;\n  let bestVotes = -1;\n  for (let i = 0; i < K; i++) {\n    const label = neighbors[i].label;\n    const count = (votes.get(label) || 0) + 1;\n    votes.set(label, count);\n    if (count > bestVotes) {\n      bestVotes = count;\n      bestLabel = label;\n    }\n  }\n  return bestLabel;\n}\n\n// --- Decision surface: dense mesh classified by the trained model -----------\nconst lengths = trainingPoints.map((p) => p[0]);\nconst widths = trainingPoints.map((p) => p[1]);\nconst xMin = Math.floor((Math.min(...lengths) - 0.6) * 2) / 2;\nconst xMax = Math.ceil((Math.max(...lengths) + 0.6) * 2) / 2;\nconst yMin = Math.max(0, Math.floor((Math.min(...widths) - 0.35) * 2) / 2);\nconst yMax = Math.ceil((Math.max(...widths) + 0.35) * 2) / 2;\n\nconst MESH_NX = 130;\nconst MESH_NY = 130;\nconst cellW = (xMax - xMin) / MESH_NX;\nconst cellH = (yMax - yMin) / MESH_NY;\n\nconst meshData = [];\nfor (let ix = 0; ix < MESH_NX; ix++) {\n  const mx = xMin + (ix + 0.5) * cellW;\n  for (let iy = 0; iy < MESH_NY; iy++) {\n    const my = yMin + (iy + 0.5) * cellH;\n    meshData.push([mx, my, knnPredict(mx, my, -1)]);\n  }\n}\n// Pre-blended flat colors (not canvas alpha) so overlapping mesh cells never\n// stack opacity into a visible seam grid.\nconst regionColors = classColors.map((c) => blend(c, t.pageBg, 0.32));\n\n// --- Training points split into per-class series + leave-one-out errors -----\nconst pointsByClass = [[], [], []];\nconst misclassified = [];\ntrainingPoints.forEach((p, idx) => {\n  const [x, y, trueLabel] = p;\n  pointsByClass[trueLabel].push([x, y]);\n  if (knnPredict(x, y, idx) !== trueLabel) {\n    misclassified.push({ value: [x, y], itemStyle: { color: classColors[trueLabel] } });\n  }\n});\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  color: [...classColors, t.amber],\n  title: {\n    text: \"contour-decision-boundary · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  legend: {\n    data: [...classSpecs.map((s) => s.name), \"Misclassified\"],\n    top: 58,\n    textStyle: { color: t.ink, fontSize: 16 },\n  },\n  grid: { left: 110, right: 60, top: 140, bottom: 90 },\n  xAxis: {\n    type: \"value\",\n    name: \"Petal Length (cm)\",\n    nameLocation: \"middle\",\n    nameGap: 42,\n    nameTextStyle: { color: t.ink, fontSize: 18 },\n    min: xMin,\n    max: xMax,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Petal Width (cm)\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 18 },\n    min: yMin,\n    max: yMax,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { show: false },\n  },\n  series: [\n    {\n      type: \"custom\",\n      coordinateSystem: \"cartesian2d\",\n      encode: { x: 0, y: 1 },\n      silent: true,\n      z: 1,\n      tooltip: { show: false },\n      renderItem: (params, api) => {\n        const point = api.coord([api.value(0), api.value(1)]);\n        const size = api.size([cellW, cellH]);\n        return {\n          type: \"rect\",\n          shape: {\n            x: point[0] - size[0] / 2 - 0.75,\n            y: point[1] - size[1] / 2 - 0.75,\n            width: size[0] + 1.5,\n            height: size[1] + 1.5,\n          },\n          style: { fill: regionColors[api.value(2)] },\n        };\n      },\n      data: meshData,\n    },\n    ...classSpecs.map((spec, classIdx) => ({\n      name: spec.name,\n      type: \"scatter\",\n      z: 3,\n      symbol: \"circle\",\n      symbolSize: 20,\n      itemStyle: { color: classColors[classIdx], borderColor: t.pageBg, borderWidth: 1.5 },\n      data: pointsByClass[classIdx],\n    })),\n    {\n      name: \"Misclassified\",\n      type: \"scatter\",\n      z: 4,\n      symbol: \"diamond\",\n      symbolSize: 26,\n      itemStyle: { borderColor: t.amber, borderWidth: 3 },\n      data: misclassified,\n    },\n  ],\n});\n"}