{"spec_id":"contour-decision-boundary","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// contour-decision-boundary: Decision Boundary Classifier Visualization\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 81/100 | Created: 2026-09-04\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Data: synthetic customer segments (monthly spend vs. visit frequency) --\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function lcg() {\n    state = (1664525 * state + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\n\nfunction gaussian(rng) {\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\nconst rng = makeLcg(42);\nconst segments = [\n  { name: \"Budget\", spendMean: 25, spendSd: 9, visitMean: 3.0, visitSd: 1.0 },\n  { name: \"Regular\", spendMean: 60, spendSd: 11, visitMean: 6.2, visitSd: 1.3 },\n  { name: \"Premium\", spendMean: 102, spendSd: 13, visitMean: 9.6, visitSd: 1.2 },\n];\nconst pointsPerSegment = 50;\n\nconst trainSpend = [];\nconst trainVisits = [];\nconst trainClass = [];\nsegments.forEach((segment, classIndex) => {\n  for (let i = 0; i < pointsPerSegment; i++) {\n    trainSpend.push(segment.spendMean + gaussian(rng) * segment.spendSd);\n    trainVisits.push(segment.visitMean + gaussian(rng) * segment.visitSd);\n    trainClass.push(classIndex);\n  }\n});\n\n// --- k-NN classifier (k=5, Euclidean distance on standardized features) ----\nfunction meanStd(values) {\n  const mean = values.reduce((sum, v) => sum + v, 0) / values.length;\n  const variance = values.reduce((sum, v) => sum + (v - mean) * (v - mean), 0) / values.length;\n  return [mean, Math.sqrt(variance)];\n}\n\nconst [spendMean, spendSd] = meanStd(trainSpend);\nconst [visitMean, visitSd] = meanStd(trainVisits);\nconst trainSpendZ = trainSpend.map((v) => (v - spendMean) / spendSd);\nconst trainVisitZ = trainVisits.map((v) => (v - visitMean) / visitSd);\n\nconst K_NEIGHBORS = 5;\n\nfunction knnPredict(spendZ, visitZ, excludeIndex) {\n  const distances = [];\n  for (let i = 0; i < trainSpendZ.length; i++) {\n    if (i === excludeIndex) continue;\n    const dSpend = spendZ - trainSpendZ[i];\n    const dVisit = visitZ - trainVisitZ[i];\n    distances.push([dSpend * dSpend + dVisit * dVisit, trainClass[i]]);\n  }\n  distances.sort((a, b) => a[0] - b[0]);\n  const votes = new Array(segments.length).fill(0);\n  for (let i = 0; i < K_NEIGHBORS; i++) votes[distances[i][1]] += 1;\n  let bestClass = 0;\n  for (let c = 1; c < votes.length; c++) if (votes[c] > votes[bestClass]) bestClass = c;\n  return bestClass;\n}\n\n// Leave-one-out prediction flags every training point as correct/misclassified.\nconst trainPredicted = trainClass.map((_, i) => knnPredict(trainSpendZ[i], trainVisitZ[i], i));\n\n// --- Mesh grid: classify a dense grid to paint the decision regions --------\nconst margin = 0.08;\nconst spendRange = Math.max(...trainSpend) - Math.min(...trainSpend);\nconst visitRange = Math.max(...trainVisits) - Math.min(...trainVisits);\nconst spendMin = Math.min(...trainSpend) - margin * spendRange;\nconst spendMax = Math.max(...trainSpend) + margin * spendRange;\nconst visitMin = Math.min(...trainVisits) - margin * visitRange;\nconst visitMax = Math.max(...trainVisits) + margin * visitRange;\n\n// Grid resolution follows the mount's pixel aspect so each cell renders ~square.\n// Dense enough (per spec: 100x100-200x200) that the boundary reads as a smooth\n// frontier rather than a staircase-stepped mesh.\nconst gridCols = 130;\nconst gridRows = Math.max(36, Math.round(gridCols * (size.height / size.width)));\nconst cellPx = (size.width - 150) / gridCols;\n// Squares overlap heavily (each cell covered by several neighbors) so the\n// per-square alpha compounds into a flat, seamless wash instead of a visible\n// grid of tile edges; the per-square opacity below is lowered to compensate.\nconst cellRadius = Math.ceil(cellPx * 2.4);\n\nconst regionPoints = segments.map(() => []);\nfor (let i = 0; i < gridCols; i++) {\n  const spend = spendMin + ((i + 0.5) * (spendMax - spendMin)) / gridCols;\n  const spendZ = (spend - spendMean) / spendSd;\n  for (let j = 0; j < gridRows; j++) {\n    const visits = visitMin + ((j + 0.5) * (visitMax - visitMin)) / gridRows;\n    const visitZ = (visits - visitMean) / visitSd;\n    const predicted = knnPredict(spendZ, visitZ, -1);\n    regionPoints[predicted].push([spend, visits]);\n  }\n}\n\n// --- Chart -------------------------------------------------------------------\nconst regionSeries = segments.map((segment, classIndex) => ({\n  type: \"scatter\",\n  name: segment.name + \" region\",\n  data: regionPoints[classIndex],\n  marker: {\n    symbol: \"square\",\n    radius: cellRadius,\n    fillColor: Highcharts.color(t.palette[classIndex]).setOpacity(0.035).get(),\n    lineWidth: 0,\n  },\n  enableMouseTracking: false,\n  showInLegend: false,\n  states: { hover: { enabled: false } },\n}));\n\nconst trainingSeries = segments.map((segment, classIndex) => {\n  const data = [];\n  for (let i = 0; i < trainClass.length; i++) {\n    if (trainClass[i] !== classIndex) continue;\n    const correct = trainPredicted[i] === trainClass[i];\n    data.push({\n      x: trainSpend[i],\n      y: trainVisits[i],\n      marker: correct\n        ? { symbol: \"circle\", radius: 6, lineWidth: 1, lineColor: t.pageBg }\n        : { symbol: \"diamond\", radius: 8, lineWidth: 2, lineColor: t.ink },\n    });\n  }\n  return {\n    type: \"scatter\",\n    name: segment.name,\n    color: t.palette[classIndex],\n    marker: { symbol: \"circle\", radius: 6, lineWidth: 1, lineColor: t.pageBg },\n    data,\n    states: { hover: { enabled: false } },\n  };\n});\n\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"contour-decision-boundary · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"5-NN decision regions · diamonds mark leave-one-out misclassifications\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    title: { text: \"Monthly Spend ($)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: spendMin,\n    max: spendMax,\n    startOnTick: false,\n    endOnTick: false,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineWidth: 0,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    title: { text: \"Visits per Month\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: visitMin,\n    max: visitMax,\n    startOnTick: false,\n    endOnTick: false,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineWidth: 0,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: {\n    pointFormat: \"Spend: {point.x:.0f}<br/>Visits: {point.y:.1f}\",\n  },\n  plotOptions: {\n    series: { animation: false },\n  },\n  series: [...regionSeries, ...trainingSeries],\n});\n"}