Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>KNN Explorer β How KNN Learns | ML-II Book</title> | |
| <style> | |
| *{margin:0;padding:0;box-sizing:border-box} | |
| body{background:#0d1117;color:#c9d1d9;font-family:'Segoe UI',system-ui,sans-serif;overflow-x:hidden} | |
| .header{text-align:center;padding:24px 16px 8px;background:linear-gradient(180deg,#161b22 0%,#0d1117 100%)} | |
| .header h1{font-size:1.6rem;background:linear-gradient(135deg,#58a6ff,#3fb950);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:4px} | |
| .header p{color:#8b949e;font-size:0.85rem} | |
| .main{display:flex;flex-wrap:wrap;gap:16px;padding:16px;max-width:1400px;margin:0 auto} | |
| .canvas-panel{flex:1 1 720px;min-width:320px} | |
| .controls-panel{flex:0 0 340px;min-width:280px} | |
| canvas{width:100%;border-radius:12px;border:1px solid #30363d;background:#0d1117;cursor:crosshair} | |
| .card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:16px;margin-bottom:12px} | |
| .card h3{font-size:0.95rem;color:#58a6ff;margin-bottom:10px;display:flex;align-items:center;gap:6px} | |
| .card h3 .dot{width:8px;height:8px;border-radius:50%;display:inline-block} | |
| label{display:block;color:#8b949e;font-size:0.8rem;margin-bottom:4px} | |
| .slider-row{display:flex;align-items:center;gap:10px;margin-bottom:12px} | |
| .slider-row input[type=range]{flex:1;accent-color:#58a6ff;height:6px} | |
| .slider-val{background:#21262d;color:#58a6ff;font-weight:700;padding:2px 10px;border-radius:6px;font-size:1.1rem;min-width:40px;text-align:center} | |
| .btn-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px} | |
| .btn{padding:8px 14px;border-radius:8px;border:1px solid #30363d;background:#21262d;color:#c9d1d9;cursor:pointer;font-size:0.8rem;transition:all .15s} | |
| .btn:hover{background:#30363d;border-color:#58a6ff} | |
| .btn.active{background:#1a3a5c;border-color:#58a6ff;color:#58a6ff} | |
| .btn.green{border-color:#3fb950;color:#3fb950}.btn.green:hover,.btn.green.active{background:#0d2818;border-color:#3fb950} | |
| .btn.red{border-color:#f85149;color:#f85149}.btn.red:hover,.btn.red.active{background:#2d1014;border-color:#f85149} | |
| .btn.orange{border-color:#f0883e;color:#f0883e}.btn.orange:hover{background:#2a1a08} | |
| .btn.purple{border-color:#bc8cff;color:#bc8cff}.btn.purple:hover{background:#1a0d2e} | |
| .legend{display:flex;gap:16px;flex-wrap:wrap;margin-top:8px} | |
| .legend-item{display:flex;align-items:center;gap:6px;font-size:0.78rem;color:#8b949e} | |
| .legend-item .swatch{width:12px;height:12px;border-radius:3px} | |
| .metric-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px} | |
| .metric{background:#21262d;border-radius:8px;padding:10px;text-align:center} | |
| .metric .val{font-size:1.3rem;font-weight:700;color:#3fb950} | |
| .metric .lbl{font-size:0.7rem;color:#8b949e;margin-top:2px} | |
| .metric .val.warn{color:#d29922} | |
| .metric .val.bad{color:#f85149} | |
| .animate-bar{height:4px;background:#21262d;border-radius:2px;margin-top:8px;overflow:hidden} | |
| .animate-bar .fill{height:100%;background:linear-gradient(90deg,#58a6ff,#3fb950);width:0%;transition:width .3s;border-radius:2px} | |
| .info-box{background:#0d2818;border:1px solid #238636;border-radius:8px;padding:10px;margin-top:10px;font-size:0.78rem;color:#3fb950;line-height:1.5} | |
| .speed-row{display:flex;align-items:center;gap:8px;margin-top:6px} | |
| .speed-row .btn{padding:4px 10px;font-size:0.75rem} | |
| select{background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:6px;padding:6px 10px;font-size:0.8rem;width:100%;margin-bottom:10px} | |
| .footer{text-align:center;padding:16px;color:#484f58;font-size:0.75rem} | |
| .footer a{color:#58a6ff;text-decoration:none} | |
| @media(max-width:800px){.controls-panel{flex:1 1 100%}} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <h1>KNN Explorer</h1> | |
| <p>Interactive K-Nearest Neighbors Visualization — ML-II Book by Dr Milan Amrut Joshi</p> | |
| </div> | |
| <div class="main"> | |
| <div class="canvas-panel"> | |
| <canvas id="canvas" width="720" height="560"></canvas> | |
| <div class="legend"> | |
| <div class="legend-item"><div class="swatch" style="background:#3fb950"></div> Train β Class A</div> | |
| <div class="legend-item"><div class="swatch" style="background:#f85149"></div> Train β Class B</div> | |
| <div class="legend-item"><div class="swatch" style="background:#6e7681"></div> Test points</div> | |
| <div class="legend-item"><div class="swatch" style="background:#58a6ff"></div> Predicted A</div> | |
| <div class="legend-item"><div class="swatch" style="background:#f0883e"></div> Predicted B</div> | |
| <div class="legend-item"><div class="swatch" style="background:rgba(88,166,255,0.12)"></div> Decision region</div> | |
| </div> | |
| </div> | |
| <div class="controls-panel"> | |
| <div class="card"> | |
| <h3><span class="dot" style="background:#58a6ff"></span> K Value</h3> | |
| <div class="slider-row"> | |
| <input type="range" id="kSlider" min="1" max="25" value="3"> | |
| <div class="slider-val" id="kVal">3</div> | |
| </div> | |
| <div class="animate-bar"><div class="fill" id="animBar"></div></div> | |
| <div class="speed-row"> | |
| <button class="btn purple" id="animBtn" onclick="toggleAnimate()">Animate K: 1β15</button> | |
| <button class="btn" onclick="setSpeed(0.5)">0.5x</button> | |
| <button class="btn active" id="sp1" onclick="setSpeed(1)">1x</button> | |
| <button class="btn" onclick="setSpeed(2)">2x</button> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3><span class="dot" style="background:#3fb950"></span> Dataset</h3> | |
| <select id="datasetSel" onchange="changeDataset()"> | |
| <option value="moons">Two Moons</option> | |
| <option value="circles">Concentric Circles</option> | |
| <option value="blobs">Gaussian Blobs</option> | |
| <option value="spiral">Spiral</option> | |
| <option value="xor">XOR Pattern</option> | |
| </select> | |
| <label>Training samples</label> | |
| <div class="slider-row"> | |
| <input type="range" id="nSlider" min="20" max="200" value="80" step="10"> | |
| <div class="slider-val" id="nVal">80</div> | |
| </div> | |
| <label>Noise level</label> | |
| <div class="slider-row"> | |
| <input type="range" id="noiseSlider" min="0" max="50" value="15"> | |
| <div class="slider-val" id="noiseVal">15%</div> | |
| </div> | |
| <div class="btn-row"> | |
| <button class="btn green" onclick="regenerate()">New Data</button> | |
| <button class="btn orange" onclick="clearTest()">Clear Test</button> | |
| <button class="btn" onclick="toggleBoundary()">Toggle Boundary</button> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3><span class="dot" style="background:#d29922"></span> Distance Metric</h3> | |
| <div class="btn-row"> | |
| <button class="btn active" id="distEuc" onclick="setDist('euclidean')">Euclidean</button> | |
| <button class="btn" id="distMan" onclick="setDist('manhattan')">Manhattan</button> | |
| <button class="btn" id="distMinkowski" onclick="setDist('minkowski')">Minkowski p=3</button> | |
| </div> | |
| <div class="btn-row" style="margin-top:4px"> | |
| <button class="btn" id="weightUni" onclick="setWeight('uniform')">Uniform votes</button> | |
| <button class="btn active" id="weightDist" onclick="setWeight('distance')">Distance-weighted</button> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3><span class="dot" style="background:#f85149"></span> Metrics</h3> | |
| <div class="metric-grid"> | |
| <div class="metric"><div class="val" id="accVal">β</div><div class="lbl">Test Accuracy</div></div> | |
| <div class="metric"><div class="val" id="kCur">3</div><div class="lbl">Current K</div></div> | |
| <div class="metric"><div class="val" id="trainN">80</div><div class="lbl">Train Points</div></div> | |
| <div class="metric"><div class="val" id="testN">0</div><div class="lbl">Test Points</div></div> | |
| </div> | |
| <div class="info-box" id="infoBox"> | |
| Click anywhere on the canvas to add a test point (grey). KNN will classify it using the K nearest training neighbors. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| ML-II Book: Supervised Learning Classification — <a href="https://github.com/drmilanajoshi" target="_blank">Dr Milan Amrut Joshi</a> — Great Learning | |
| </div> | |
| <script> | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // STATE | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const C = document.getElementById('canvas'); | |
| const ctx = C.getContext('2d'); | |
| let W, H, dpr; | |
| let K = 3, distMetric = 'euclidean', weightMode = 'distance'; | |
| let trainData = [], testData = []; | |
| let showBoundary = true; | |
| let animating = false, animSpeed = 1, animTimer = null; | |
| let boundaryCache = null, boundaryCacheDirty = true; | |
| function resize() { | |
| const rect = C.getBoundingClientRect(); | |
| dpr = window.devicePixelRatio || 1; | |
| W = rect.width; H = rect.height; | |
| C.width = W * dpr; C.height = H * dpr; | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| boundaryCacheDirty = true; | |
| } | |
| window.addEventListener('resize', () => { resize(); draw(); }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // DATA GENERATION | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function rand() { return Math.random(); } | |
| function randn() { let u=0,v=0; while(!u) u=rand(); while(!v) v=rand(); return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v); } | |
| function generateData(type, n, noise) { | |
| const data = []; | |
| const ns = noise / 100; | |
| const half = Math.floor(n / 2); | |
| if (type === 'moons') { | |
| for (let i = 0; i < half; i++) { | |
| const t = Math.PI * i / half; | |
| data.push({ x: Math.cos(t) + randn()*ns*0.4, y: Math.sin(t) + randn()*ns*0.4, cls: 0 }); | |
| } | |
| for (let i = 0; i < n - half; i++) { | |
| const t = Math.PI * i / (n - half); | |
| data.push({ x: 1 - Math.cos(t) + randn()*ns*0.4, y: 0.5 - Math.sin(t) + randn()*ns*0.4, cls: 1 }); | |
| } | |
| } else if (type === 'circles') { | |
| for (let i = 0; i < half; i++) { | |
| const a = 2*Math.PI*rand(); | |
| const r = 0.3 + randn()*ns*0.15; | |
| data.push({ x: r*Math.cos(a)+1, y: r*Math.sin(a)+0.5, cls: 0 }); | |
| } | |
| for (let i = 0; i < n - half; i++) { | |
| const a = 2*Math.PI*rand(); | |
| const r = 0.8 + randn()*ns*0.15; | |
| data.push({ x: r*Math.cos(a)+1, y: r*Math.sin(a)+0.5, cls: 1 }); | |
| } | |
| } else if (type === 'blobs') { | |
| for (let i = 0; i < half; i++) { | |
| data.push({ x: 0.5 + randn()*0.25*(1+ns), y: 0.7 + randn()*0.25*(1+ns), cls: 0 }); | |
| } | |
| for (let i = 0; i < n - half; i++) { | |
| data.push({ x: 1.5 + randn()*0.25*(1+ns), y: 0.3 + randn()*0.25*(1+ns), cls: 1 }); | |
| } | |
| } else if (type === 'spiral') { | |
| for (let i = 0; i < half; i++) { | |
| const t = 1.5*Math.PI*i/half + 0.5; | |
| const r = 0.4*t/(1.5*Math.PI); | |
| data.push({ x: r*Math.cos(t)+1+randn()*ns*0.12, y: r*Math.sin(t)+0.5+randn()*ns*0.12, cls: 0 }); | |
| } | |
| for (let i = 0; i < n - half; i++) { | |
| const t = 1.5*Math.PI*i/(n-half) + 0.5; | |
| const r = 0.4*t/(1.5*Math.PI); | |
| data.push({ x: -r*Math.cos(t)+1+randn()*ns*0.12, y: -r*Math.sin(t)+0.5+randn()*ns*0.12, cls: 1 }); | |
| } | |
| } else if (type === 'xor') { | |
| for (let i = 0; i < n; i++) { | |
| const x = rand()*2, y = rand(); | |
| const cls = ((x > 1) ^ (y > 0.5)) ? 1 : 0; | |
| data.push({ x: x + randn()*ns*0.15, y: y + randn()*ns*0.15, cls }); | |
| } | |
| } | |
| return data; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // DISTANCE & KNN | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function dist(a, b) { | |
| const dx = a.x - b.x, dy = a.y - b.y; | |
| if (distMetric === 'euclidean') return Math.sqrt(dx*dx + dy*dy); | |
| if (distMetric === 'manhattan') return Math.abs(dx) + Math.abs(dy); | |
| if (distMetric === 'minkowski') { const p=3; return Math.pow(Math.pow(Math.abs(dx),p)+Math.pow(Math.abs(dy),p),1/p); } | |
| return Math.sqrt(dx*dx+dy*dy); | |
| } | |
| function knnClassify(point, k) { | |
| if (trainData.length === 0) return { cls: -1, neighbors: [], conf: 0 }; | |
| const dists = trainData.map((t, i) => ({ i, d: dist(point, t), cls: t.cls })); | |
| dists.sort((a, b) => a.d - b.d); | |
| const neighbors = dists.slice(0, Math.min(k, dists.length)); | |
| let scores = [0, 0]; | |
| if (weightMode === 'uniform') { | |
| neighbors.forEach(n => scores[n.cls]++); | |
| } else { | |
| neighbors.forEach(n => { | |
| const w = n.d < 1e-9 ? 1e6 : 1 / n.d; | |
| scores[n.cls] += w; | |
| }); | |
| } | |
| const total = scores[0] + scores[1]; | |
| const cls = scores[0] >= scores[1] ? 0 : 1; | |
| const conf = total > 0 ? Math.max(scores[0], scores[1]) / total : 0.5; | |
| return { cls, neighbors, conf }; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // COORDINATE TRANSFORMS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const pad = 40; | |
| function toCanvas(pt) { | |
| return { x: pad + (pt.x - viewMinX) / (viewMaxX - viewMinX) * (W - 2*pad), | |
| y: (H - pad) - (pt.y - viewMinY) / (viewMaxY - viewMinY) * (H - 2*pad) }; | |
| } | |
| function fromCanvas(cx, cy) { | |
| return { x: viewMinX + (cx - pad) / (W - 2*pad) * (viewMaxX - viewMinX), | |
| y: viewMinY + (H - pad - cy) / (H - 2*pad) * (viewMaxY - viewMinY) }; | |
| } | |
| let viewMinX = -0.5, viewMaxX = 2.5, viewMinY = -0.5, viewMaxY = 1.5; | |
| function fitView() { | |
| if (trainData.length === 0) { viewMinX=-0.5; viewMaxX=2.5; viewMinY=-0.5; viewMaxY=1.5; return; } | |
| let mnx=Infinity,mxx=-Infinity,mny=Infinity,mxy=-Infinity; | |
| trainData.forEach(p => { mnx=Math.min(mnx,p.x); mxx=Math.max(mxx,p.x); mny=Math.min(mny,p.y); mxy=Math.max(mxy,p.y); }); | |
| const mx = (mxx-mnx)*0.15 || 0.5, my = (mxy-mny)*0.15 || 0.5; | |
| viewMinX=mnx-mx; viewMaxX=mxx+mx; viewMinY=mny-my; viewMaxY=mxy+my; | |
| // Ensure aspect ratio | |
| const aspect = W / H; | |
| const cx = (viewMinX+viewMaxX)/2, cy = (viewMinY+viewMaxY)/2; | |
| let rw = (viewMaxX-viewMinX)/2, rh = (viewMaxY-viewMinY)/2; | |
| if (rw/rh < aspect) rw = rh*aspect; else rh = rw/aspect; | |
| viewMinX=cx-rw; viewMaxX=cx+rw; viewMinY=cy-rh; viewMaxY=cy+rh; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // DRAWING | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function draw() { | |
| ctx.clearRect(0, 0, W, H); | |
| // Grid | |
| ctx.strokeStyle = '#21262d'; ctx.lineWidth = 0.5; | |
| for (let i = 0; i <= 10; i++) { | |
| const x = pad + i*(W-2*pad)/10, y = pad + i*(H-2*pad)/10; | |
| ctx.beginPath(); ctx.moveTo(x, pad); ctx.lineTo(x, H-pad); ctx.stroke(); | |
| ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(W-pad, y); ctx.stroke(); | |
| } | |
| // Axes | |
| ctx.strokeStyle = '#30363d'; ctx.lineWidth = 1; | |
| ctx.beginPath(); ctx.moveTo(pad, pad); ctx.lineTo(pad, H-pad); ctx.lineTo(W-pad, H-pad); ctx.stroke(); | |
| // Decision boundary (background) | |
| if (showBoundary && trainData.length > 0) drawBoundary(); | |
| // Train data | |
| trainData.forEach(p => { | |
| const cp = toCanvas(p); | |
| ctx.beginPath(); ctx.arc(cp.x, cp.y, 6, 0, 2*Math.PI); | |
| ctx.fillStyle = p.cls === 0 ? '#3fb950' : '#f85149'; | |
| ctx.globalAlpha = 0.85; ctx.fill(); | |
| ctx.globalAlpha = 1; | |
| ctx.strokeStyle = p.cls === 0 ? '#238636' : '#da3633'; ctx.lineWidth = 1.5; ctx.stroke(); | |
| }); | |
| // Test data with neighbor lines | |
| testData.forEach(tp => { | |
| const cp = toCanvas(tp); | |
| const res = knnClassify(tp, K); | |
| tp._cls = res.cls; tp._conf = res.conf; | |
| // Neighbor connection lines | |
| res.neighbors.forEach((n, idx) => { | |
| const np = toCanvas(trainData[n.i]); | |
| const alpha = 0.6 - idx * 0.04; | |
| ctx.strokeStyle = n.cls === 0 ? `rgba(63,185,80,${alpha})` : `rgba(248,81,73,${alpha})`; | |
| ctx.lineWidth = 2 - idx * 0.1; | |
| ctx.setLineDash([4, 3]); | |
| ctx.beginPath(); ctx.moveTo(cp.x, cp.y); ctx.lineTo(np.x, np.y); ctx.stroke(); | |
| ctx.setLineDash([]); | |
| }); | |
| // Test point | |
| ctx.beginPath(); ctx.arc(cp.x, cp.y, 8, 0, 2*Math.PI); | |
| ctx.fillStyle = '#6e7681'; ctx.globalAlpha = 0.5; ctx.fill(); ctx.globalAlpha = 1; | |
| // Prediction ring | |
| ctx.strokeStyle = res.cls === 0 ? '#58a6ff' : '#f0883e'; ctx.lineWidth = 2.5; ctx.stroke(); | |
| // K-circle (distance to Kth neighbor) | |
| if (res.neighbors.length === K) { | |
| const kthDist = res.neighbors[K-1].d; | |
| const radiusPx = kthDist / (viewMaxX - viewMinX) * (W - 2*pad); | |
| ctx.beginPath(); ctx.arc(cp.x, cp.y, radiusPx, 0, 2*Math.PI); | |
| ctx.strokeStyle = 'rgba(188,140,255,0.3)'; ctx.lineWidth = 1; ctx.setLineDash([3,3]); ctx.stroke(); ctx.setLineDash([]); | |
| } | |
| // Confidence label | |
| ctx.font = '10px system-ui'; ctx.fillStyle = '#c9d1d9'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(`${(res.conf*100).toFixed(0)}%`, cp.x, cp.y - 13); | |
| ctx.textAlign = 'left'; | |
| }); | |
| updateMetrics(); | |
| } | |
| function drawBoundary() { | |
| const res = 3; | |
| const stepsX = Math.ceil((W - 2*pad) / res); | |
| const stepsY = Math.ceil((H - 2*pad) / res); | |
| for (let ix = 0; ix < stepsX; ix++) { | |
| for (let iy = 0; iy < stepsY; iy++) { | |
| const cx = pad + ix * res + res/2; | |
| const cy = pad + iy * res + res/2; | |
| const pt = fromCanvas(cx, cy); | |
| const r = knnClassify(pt, K); | |
| if (r.cls === 0) { | |
| ctx.fillStyle = `rgba(63,185,80,${0.04 + r.conf*0.08})`; | |
| } else { | |
| ctx.fillStyle = `rgba(248,81,73,${0.04 + r.conf*0.08})`; | |
| } | |
| ctx.fillRect(cx - res/2, cy - res/2, res, res); | |
| } | |
| } | |
| } | |
| function updateMetrics() { | |
| document.getElementById('kCur').textContent = K; | |
| document.getElementById('trainN').textContent = trainData.length; | |
| document.getElementById('testN').textContent = testData.length; | |
| if (testData.length === 0) { | |
| document.getElementById('accVal').textContent = 'β'; | |
| document.getElementById('accVal').className = 'val'; | |
| return; | |
| } | |
| // We don't have ground truth for click-added test points, show avg confidence | |
| const avgConf = testData.reduce((s, t) => s + (t._conf || 0.5), 0) / testData.length; | |
| const pct = (avgConf * 100).toFixed(1) + '%'; | |
| const el = document.getElementById('accVal'); | |
| el.textContent = pct; | |
| el.className = avgConf > 0.75 ? 'val' : avgConf > 0.5 ? 'val warn' : 'val bad'; | |
| // Update info text | |
| const predA = testData.filter(t => t._cls === 0).length; | |
| const predB = testData.filter(t => t._cls === 1).length; | |
| document.getElementById('infoBox').innerHTML = | |
| `K=${K}: ${predA} points β Class A, ${predB} points β Class B<br>` + | |
| `Avg confidence: ${pct} | Metric: ${distMetric} | Weight: ${weightMode}`; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // INTERACTIONS | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| C.addEventListener('click', e => { | |
| const rect = C.getBoundingClientRect(); | |
| const cx = e.clientX - rect.left, cy = e.clientY - rect.top; | |
| const pt = fromCanvas(cx, cy); | |
| testData.push(pt); | |
| draw(); | |
| }); | |
| // Sliders | |
| document.getElementById('kSlider').addEventListener('input', e => { | |
| K = parseInt(e.target.value); | |
| document.getElementById('kVal').textContent = K; | |
| draw(); | |
| }); | |
| document.getElementById('nSlider').addEventListener('input', e => { | |
| document.getElementById('nVal').textContent = e.target.value; | |
| }); | |
| document.getElementById('nSlider').addEventListener('change', regenerate); | |
| document.getElementById('noiseSlider').addEventListener('input', e => { | |
| document.getElementById('noiseVal').textContent = e.target.value + '%'; | |
| }); | |
| document.getElementById('noiseSlider').addEventListener('change', regenerate); | |
| function regenerate() { | |
| const type = document.getElementById('datasetSel').value; | |
| const n = parseInt(document.getElementById('nSlider').value); | |
| const noise = parseInt(document.getElementById('noiseSlider').value); | |
| trainData = generateData(type, n, noise); | |
| testData = []; | |
| fitView(); | |
| draw(); | |
| } | |
| function changeDataset() { regenerate(); } | |
| function clearTest() { testData = []; draw(); } | |
| function toggleBoundary() { | |
| showBoundary = !showBoundary; | |
| draw(); | |
| } | |
| function setDist(m) { | |
| distMetric = m; | |
| ['distEuc','distMan','distMinkowski'].forEach(id => document.getElementById(id).classList.remove('active')); | |
| if (m === 'euclidean') document.getElementById('distEuc').classList.add('active'); | |
| else if (m === 'manhattan') document.getElementById('distMan').classList.add('active'); | |
| else document.getElementById('distMinkowski').classList.add('active'); | |
| draw(); | |
| } | |
| function setWeight(w) { | |
| weightMode = w; | |
| document.getElementById('weightUni').classList.toggle('active', w==='uniform'); | |
| document.getElementById('weightDist').classList.toggle('active', w==='distance'); | |
| draw(); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ANIMATION: Sweep K from 1 to 15 | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function toggleAnimate() { | |
| if (animating) { stopAnimate(); return; } | |
| animating = true; | |
| document.getElementById('animBtn').textContent = 'Stop Animation'; | |
| document.getElementById('animBtn').classList.add('active'); | |
| // Add test points if none exist | |
| if (testData.length === 0) { | |
| for (let i = 0; i < 30; i++) { | |
| testData.push({ | |
| x: viewMinX + rand() * (viewMaxX - viewMinX), | |
| y: viewMinY + rand() * (viewMaxY - viewMinY) | |
| }); | |
| } | |
| } | |
| let kAnim = 1; | |
| const maxK = Math.min(15, trainData.length); | |
| const interval = 1200 / animSpeed; | |
| function step() { | |
| if (!animating) return; | |
| K = kAnim; | |
| document.getElementById('kSlider').value = K; | |
| document.getElementById('kVal').textContent = K; | |
| document.getElementById('animBar').style.width = ((kAnim / maxK) * 100) + '%'; | |
| draw(); | |
| kAnim++; | |
| if (kAnim > maxK) kAnim = 1; | |
| animTimer = setTimeout(step, interval); | |
| } | |
| step(); | |
| } | |
| function stopAnimate() { | |
| animating = false; | |
| clearTimeout(animTimer); | |
| document.getElementById('animBtn').textContent = 'Animate K: 1β15'; | |
| document.getElementById('animBtn').classList.remove('active'); | |
| document.getElementById('animBar').style.width = '0%'; | |
| } | |
| function setSpeed(s) { | |
| animSpeed = s; | |
| document.querySelectorAll('.speed-row .btn').forEach(b => b.classList.remove('active')); | |
| if (s === 0.5) document.querySelectorAll('.speed-row .btn')[1].classList.add('active'); | |
| else if (s === 1) document.getElementById('sp1').classList.add('active'); | |
| else document.querySelectorAll('.speed-row .btn')[3].classList.add('active'); | |
| if (animating) { stopAnimate(); toggleAnimate(); } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // INIT | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| resize(); | |
| regenerate(); | |
| </script> | |
| </body> | |
| </html> | |