mlnjsh commited on
Commit
17cb2ed
Β·
verified Β·
1 Parent(s): 988a817

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +528 -18
index.html CHANGED
@@ -1,19 +1,529 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>KNN Explorer β€” How KNN Learns | ML-II Book</title>
7
+ <style>
8
+ *{margin:0;padding:0;box-sizing:border-box}
9
+ body{background:#0d1117;color:#c9d1d9;font-family:'Segoe UI',system-ui,sans-serif;overflow-x:hidden}
10
+ .header{text-align:center;padding:24px 16px 8px;background:linear-gradient(180deg,#161b22 0%,#0d1117 100%)}
11
+ .header h1{font-size:1.6rem;background:linear-gradient(135deg,#58a6ff,#3fb950);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:4px}
12
+ .header p{color:#8b949e;font-size:0.85rem}
13
+ .main{display:flex;flex-wrap:wrap;gap:16px;padding:16px;max-width:1400px;margin:0 auto}
14
+ .canvas-panel{flex:1 1 720px;min-width:320px}
15
+ .controls-panel{flex:0 0 340px;min-width:280px}
16
+ canvas{width:100%;border-radius:12px;border:1px solid #30363d;background:#0d1117;cursor:crosshair}
17
+ .card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:16px;margin-bottom:12px}
18
+ .card h3{font-size:0.95rem;color:#58a6ff;margin-bottom:10px;display:flex;align-items:center;gap:6px}
19
+ .card h3 .dot{width:8px;height:8px;border-radius:50%;display:inline-block}
20
+ label{display:block;color:#8b949e;font-size:0.8rem;margin-bottom:4px}
21
+ .slider-row{display:flex;align-items:center;gap:10px;margin-bottom:12px}
22
+ .slider-row input[type=range]{flex:1;accent-color:#58a6ff;height:6px}
23
+ .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}
24
+ .btn-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
25
+ .btn{padding:8px 14px;border-radius:8px;border:1px solid #30363d;background:#21262d;color:#c9d1d9;cursor:pointer;font-size:0.8rem;transition:all .15s}
26
+ .btn:hover{background:#30363d;border-color:#58a6ff}
27
+ .btn.active{background:#1a3a5c;border-color:#58a6ff;color:#58a6ff}
28
+ .btn.green{border-color:#3fb950;color:#3fb950}.btn.green:hover,.btn.green.active{background:#0d2818;border-color:#3fb950}
29
+ .btn.red{border-color:#f85149;color:#f85149}.btn.red:hover,.btn.red.active{background:#2d1014;border-color:#f85149}
30
+ .btn.orange{border-color:#f0883e;color:#f0883e}.btn.orange:hover{background:#2a1a08}
31
+ .btn.purple{border-color:#bc8cff;color:#bc8cff}.btn.purple:hover{background:#1a0d2e}
32
+ .legend{display:flex;gap:16px;flex-wrap:wrap;margin-top:8px}
33
+ .legend-item{display:flex;align-items:center;gap:6px;font-size:0.78rem;color:#8b949e}
34
+ .legend-item .swatch{width:12px;height:12px;border-radius:3px}
35
+ .metric-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
36
+ .metric{background:#21262d;border-radius:8px;padding:10px;text-align:center}
37
+ .metric .val{font-size:1.3rem;font-weight:700;color:#3fb950}
38
+ .metric .lbl{font-size:0.7rem;color:#8b949e;margin-top:2px}
39
+ .metric .val.warn{color:#d29922}
40
+ .metric .val.bad{color:#f85149}
41
+ .animate-bar{height:4px;background:#21262d;border-radius:2px;margin-top:8px;overflow:hidden}
42
+ .animate-bar .fill{height:100%;background:linear-gradient(90deg,#58a6ff,#3fb950);width:0%;transition:width .3s;border-radius:2px}
43
+ .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}
44
+ .speed-row{display:flex;align-items:center;gap:8px;margin-top:6px}
45
+ .speed-row .btn{padding:4px 10px;font-size:0.75rem}
46
+ select{background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:6px;padding:6px 10px;font-size:0.8rem;width:100%;margin-bottom:10px}
47
+ .footer{text-align:center;padding:16px;color:#484f58;font-size:0.75rem}
48
+ .footer a{color:#58a6ff;text-decoration:none}
49
+ @media(max-width:800px){.controls-panel{flex:1 1 100%}}
50
+ </style>
51
+ </head>
52
+ <body>
53
+
54
+ <div class="header">
55
+ <h1>KNN Explorer</h1>
56
+ <p>Interactive K-Nearest Neighbors Visualization &mdash; ML-II Book by Dr Milan Amrut Joshi</p>
57
+ </div>
58
+
59
+ <div class="main">
60
+ <div class="canvas-panel">
61
+ <canvas id="canvas" width="720" height="560"></canvas>
62
+ <div class="legend">
63
+ <div class="legend-item"><div class="swatch" style="background:#3fb950"></div> Train β€” Class A</div>
64
+ <div class="legend-item"><div class="swatch" style="background:#f85149"></div> Train β€” Class B</div>
65
+ <div class="legend-item"><div class="swatch" style="background:#6e7681"></div> Test points</div>
66
+ <div class="legend-item"><div class="swatch" style="background:#58a6ff"></div> Predicted A</div>
67
+ <div class="legend-item"><div class="swatch" style="background:#f0883e"></div> Predicted B</div>
68
+ <div class="legend-item"><div class="swatch" style="background:rgba(88,166,255,0.12)"></div> Decision region</div>
69
+ </div>
70
+ </div>
71
+
72
+ <div class="controls-panel">
73
+ <div class="card">
74
+ <h3><span class="dot" style="background:#58a6ff"></span> K Value</h3>
75
+ <div class="slider-row">
76
+ <input type="range" id="kSlider" min="1" max="25" value="3">
77
+ <div class="slider-val" id="kVal">3</div>
78
+ </div>
79
+ <div class="animate-bar"><div class="fill" id="animBar"></div></div>
80
+ <div class="speed-row">
81
+ <button class="btn purple" id="animBtn" onclick="toggleAnimate()">Animate K: 1β†’15</button>
82
+ <button class="btn" onclick="setSpeed(0.5)">0.5x</button>
83
+ <button class="btn active" id="sp1" onclick="setSpeed(1)">1x</button>
84
+ <button class="btn" onclick="setSpeed(2)">2x</button>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="card">
89
+ <h3><span class="dot" style="background:#3fb950"></span> Dataset</h3>
90
+ <select id="datasetSel" onchange="changeDataset()">
91
+ <option value="moons">Two Moons</option>
92
+ <option value="circles">Concentric Circles</option>
93
+ <option value="blobs">Gaussian Blobs</option>
94
+ <option value="spiral">Spiral</option>
95
+ <option value="xor">XOR Pattern</option>
96
+ </select>
97
+ <label>Training samples</label>
98
+ <div class="slider-row">
99
+ <input type="range" id="nSlider" min="20" max="200" value="80" step="10">
100
+ <div class="slider-val" id="nVal">80</div>
101
+ </div>
102
+ <label>Noise level</label>
103
+ <div class="slider-row">
104
+ <input type="range" id="noiseSlider" min="0" max="50" value="15">
105
+ <div class="slider-val" id="noiseVal">15%</div>
106
+ </div>
107
+ <div class="btn-row">
108
+ <button class="btn green" onclick="regenerate()">New Data</button>
109
+ <button class="btn orange" onclick="clearTest()">Clear Test</button>
110
+ <button class="btn" onclick="toggleBoundary()">Toggle Boundary</button>
111
+ </div>
112
+ </div>
113
+
114
+ <div class="card">
115
+ <h3><span class="dot" style="background:#d29922"></span> Distance Metric</h3>
116
+ <div class="btn-row">
117
+ <button class="btn active" id="distEuc" onclick="setDist('euclidean')">Euclidean</button>
118
+ <button class="btn" id="distMan" onclick="setDist('manhattan')">Manhattan</button>
119
+ <button class="btn" id="distMinkowski" onclick="setDist('minkowski')">Minkowski p=3</button>
120
+ </div>
121
+ <div class="btn-row" style="margin-top:4px">
122
+ <button class="btn" id="weightUni" onclick="setWeight('uniform')">Uniform votes</button>
123
+ <button class="btn active" id="weightDist" onclick="setWeight('distance')">Distance-weighted</button>
124
+ </div>
125
+ </div>
126
+
127
+ <div class="card">
128
+ <h3><span class="dot" style="background:#f85149"></span> Metrics</h3>
129
+ <div class="metric-grid">
130
+ <div class="metric"><div class="val" id="accVal">β€”</div><div class="lbl">Test Accuracy</div></div>
131
+ <div class="metric"><div class="val" id="kCur">3</div><div class="lbl">Current K</div></div>
132
+ <div class="metric"><div class="val" id="trainN">80</div><div class="lbl">Train Points</div></div>
133
+ <div class="metric"><div class="val" id="testN">0</div><div class="lbl">Test Points</div></div>
134
+ </div>
135
+ <div class="info-box" id="infoBox">
136
+ Click anywhere on the canvas to add a test point (grey). KNN will classify it using the K nearest training neighbors.
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="footer">
143
+ ML-II Book: Supervised Learning Classification &mdash; <a href="https://github.com/drmilanajoshi" target="_blank">Dr Milan Amrut Joshi</a> &mdash; Great Learning
144
+ </div>
145
+
146
+ <script>
147
+ // ═══════════════════════════════════════════════════════════════
148
+ // STATE
149
+ // ═══════════════════════════════════════════════════════════════
150
+ const C = document.getElementById('canvas');
151
+ const ctx = C.getContext('2d');
152
+ let W, H, dpr;
153
+ let K = 3, distMetric = 'euclidean', weightMode = 'distance';
154
+ let trainData = [], testData = [];
155
+ let showBoundary = true;
156
+ let animating = false, animSpeed = 1, animTimer = null;
157
+ let boundaryCache = null, boundaryCacheDirty = true;
158
+
159
+ function resize() {
160
+ const rect = C.getBoundingClientRect();
161
+ dpr = window.devicePixelRatio || 1;
162
+ W = rect.width; H = rect.height;
163
+ C.width = W * dpr; C.height = H * dpr;
164
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
165
+ boundaryCacheDirty = true;
166
+ }
167
+ window.addEventListener('resize', () => { resize(); draw(); });
168
+
169
+ // ════════════════════════════════════════════════════���══════════
170
+ // DATA GENERATION
171
+ // ═══════════════════════════════════════════════════════════════
172
+ function rand() { return Math.random(); }
173
+ 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); }
174
+
175
+ function generateData(type, n, noise) {
176
+ const data = [];
177
+ const ns = noise / 100;
178
+ const half = Math.floor(n / 2);
179
+
180
+ if (type === 'moons') {
181
+ for (let i = 0; i < half; i++) {
182
+ const t = Math.PI * i / half;
183
+ data.push({ x: Math.cos(t) + randn()*ns*0.4, y: Math.sin(t) + randn()*ns*0.4, cls: 0 });
184
+ }
185
+ for (let i = 0; i < n - half; i++) {
186
+ const t = Math.PI * i / (n - half);
187
+ data.push({ x: 1 - Math.cos(t) + randn()*ns*0.4, y: 0.5 - Math.sin(t) + randn()*ns*0.4, cls: 1 });
188
+ }
189
+ } else if (type === 'circles') {
190
+ for (let i = 0; i < half; i++) {
191
+ const a = 2*Math.PI*rand();
192
+ const r = 0.3 + randn()*ns*0.15;
193
+ data.push({ x: r*Math.cos(a)+1, y: r*Math.sin(a)+0.5, cls: 0 });
194
+ }
195
+ for (let i = 0; i < n - half; i++) {
196
+ const a = 2*Math.PI*rand();
197
+ const r = 0.8 + randn()*ns*0.15;
198
+ data.push({ x: r*Math.cos(a)+1, y: r*Math.sin(a)+0.5, cls: 1 });
199
+ }
200
+ } else if (type === 'blobs') {
201
+ for (let i = 0; i < half; i++) {
202
+ data.push({ x: 0.5 + randn()*0.25*(1+ns), y: 0.7 + randn()*0.25*(1+ns), cls: 0 });
203
+ }
204
+ for (let i = 0; i < n - half; i++) {
205
+ data.push({ x: 1.5 + randn()*0.25*(1+ns), y: 0.3 + randn()*0.25*(1+ns), cls: 1 });
206
+ }
207
+ } else if (type === 'spiral') {
208
+ for (let i = 0; i < half; i++) {
209
+ const t = 1.5*Math.PI*i/half + 0.5;
210
+ const r = 0.4*t/(1.5*Math.PI);
211
+ 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 });
212
+ }
213
+ for (let i = 0; i < n - half; i++) {
214
+ const t = 1.5*Math.PI*i/(n-half) + 0.5;
215
+ const r = 0.4*t/(1.5*Math.PI);
216
+ 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 });
217
+ }
218
+ } else if (type === 'xor') {
219
+ for (let i = 0; i < n; i++) {
220
+ const x = rand()*2, y = rand();
221
+ const cls = ((x > 1) ^ (y > 0.5)) ? 1 : 0;
222
+ data.push({ x: x + randn()*ns*0.15, y: y + randn()*ns*0.15, cls });
223
+ }
224
+ }
225
+ return data;
226
+ }
227
+
228
+ // ═══════════════════════════════════════════════════════════════
229
+ // DISTANCE & KNN
230
+ // ═══════════════════════════════════════════════════════════════
231
+ function dist(a, b) {
232
+ const dx = a.x - b.x, dy = a.y - b.y;
233
+ if (distMetric === 'euclidean') return Math.sqrt(dx*dx + dy*dy);
234
+ if (distMetric === 'manhattan') return Math.abs(dx) + Math.abs(dy);
235
+ if (distMetric === 'minkowski') { const p=3; return Math.pow(Math.pow(Math.abs(dx),p)+Math.pow(Math.abs(dy),p),1/p); }
236
+ return Math.sqrt(dx*dx+dy*dy);
237
+ }
238
+
239
+ function knnClassify(point, k) {
240
+ if (trainData.length === 0) return { cls: -1, neighbors: [], conf: 0 };
241
+ const dists = trainData.map((t, i) => ({ i, d: dist(point, t), cls: t.cls }));
242
+ dists.sort((a, b) => a.d - b.d);
243
+ const neighbors = dists.slice(0, Math.min(k, dists.length));
244
+
245
+ let scores = [0, 0];
246
+ if (weightMode === 'uniform') {
247
+ neighbors.forEach(n => scores[n.cls]++);
248
+ } else {
249
+ neighbors.forEach(n => {
250
+ const w = n.d < 1e-9 ? 1e6 : 1 / n.d;
251
+ scores[n.cls] += w;
252
+ });
253
+ }
254
+ const total = scores[0] + scores[1];
255
+ const cls = scores[0] >= scores[1] ? 0 : 1;
256
+ const conf = total > 0 ? Math.max(scores[0], scores[1]) / total : 0.5;
257
+ return { cls, neighbors, conf };
258
+ }
259
+
260
+ // ═══════════════════════════════════════════════════════════════
261
+ // COORDINATE TRANSFORMS
262
+ // ═══════════════════════════════════════════════════════════════
263
+ const pad = 40;
264
+ function toCanvas(pt) {
265
+ return { x: pad + (pt.x - viewMinX) / (viewMaxX - viewMinX) * (W - 2*pad),
266
+ y: (H - pad) - (pt.y - viewMinY) / (viewMaxY - viewMinY) * (H - 2*pad) };
267
+ }
268
+ function fromCanvas(cx, cy) {
269
+ return { x: viewMinX + (cx - pad) / (W - 2*pad) * (viewMaxX - viewMinX),
270
+ y: viewMinY + (H - pad - cy) / (H - 2*pad) * (viewMaxY - viewMinY) };
271
+ }
272
+
273
+ let viewMinX = -0.5, viewMaxX = 2.5, viewMinY = -0.5, viewMaxY = 1.5;
274
+ function fitView() {
275
+ if (trainData.length === 0) { viewMinX=-0.5; viewMaxX=2.5; viewMinY=-0.5; viewMaxY=1.5; return; }
276
+ let mnx=Infinity,mxx=-Infinity,mny=Infinity,mxy=-Infinity;
277
+ 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); });
278
+ const mx = (mxx-mnx)*0.15 || 0.5, my = (mxy-mny)*0.15 || 0.5;
279
+ viewMinX=mnx-mx; viewMaxX=mxx+mx; viewMinY=mny-my; viewMaxY=mxy+my;
280
+ // Ensure aspect ratio
281
+ const aspect = W / H;
282
+ const cx = (viewMinX+viewMaxX)/2, cy = (viewMinY+viewMaxY)/2;
283
+ let rw = (viewMaxX-viewMinX)/2, rh = (viewMaxY-viewMinY)/2;
284
+ if (rw/rh < aspect) rw = rh*aspect; else rh = rw/aspect;
285
+ viewMinX=cx-rw; viewMaxX=cx+rw; viewMinY=cy-rh; viewMaxY=cy+rh;
286
+ }
287
+
288
+ // ═══════════════════════════════════════════════════════════════
289
+ // DRAWING
290
+ // ═══════════════════════════════════════════════════════════════
291
+ function draw() {
292
+ ctx.clearRect(0, 0, W, H);
293
+
294
+ // Grid
295
+ ctx.strokeStyle = '#21262d'; ctx.lineWidth = 0.5;
296
+ for (let i = 0; i <= 10; i++) {
297
+ const x = pad + i*(W-2*pad)/10, y = pad + i*(H-2*pad)/10;
298
+ ctx.beginPath(); ctx.moveTo(x, pad); ctx.lineTo(x, H-pad); ctx.stroke();
299
+ ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(W-pad, y); ctx.stroke();
300
+ }
301
+
302
+ // Axes
303
+ ctx.strokeStyle = '#30363d'; ctx.lineWidth = 1;
304
+ ctx.beginPath(); ctx.moveTo(pad, pad); ctx.lineTo(pad, H-pad); ctx.lineTo(W-pad, H-pad); ctx.stroke();
305
+
306
+ // Decision boundary (background)
307
+ if (showBoundary && trainData.length > 0) drawBoundary();
308
+
309
+ // Train data
310
+ trainData.forEach(p => {
311
+ const cp = toCanvas(p);
312
+ ctx.beginPath(); ctx.arc(cp.x, cp.y, 6, 0, 2*Math.PI);
313
+ ctx.fillStyle = p.cls === 0 ? '#3fb950' : '#f85149';
314
+ ctx.globalAlpha = 0.85; ctx.fill();
315
+ ctx.globalAlpha = 1;
316
+ ctx.strokeStyle = p.cls === 0 ? '#238636' : '#da3633'; ctx.lineWidth = 1.5; ctx.stroke();
317
+ });
318
+
319
+ // Test data with neighbor lines
320
+ testData.forEach(tp => {
321
+ const cp = toCanvas(tp);
322
+ const res = knnClassify(tp, K);
323
+ tp._cls = res.cls; tp._conf = res.conf;
324
+
325
+ // Neighbor connection lines
326
+ res.neighbors.forEach((n, idx) => {
327
+ const np = toCanvas(trainData[n.i]);
328
+ const alpha = 0.6 - idx * 0.04;
329
+ ctx.strokeStyle = n.cls === 0 ? `rgba(63,185,80,${alpha})` : `rgba(248,81,73,${alpha})`;
330
+ ctx.lineWidth = 2 - idx * 0.1;
331
+ ctx.setLineDash([4, 3]);
332
+ ctx.beginPath(); ctx.moveTo(cp.x, cp.y); ctx.lineTo(np.x, np.y); ctx.stroke();
333
+ ctx.setLineDash([]);
334
+ });
335
+
336
+ // Test point
337
+ ctx.beginPath(); ctx.arc(cp.x, cp.y, 8, 0, 2*Math.PI);
338
+ ctx.fillStyle = '#6e7681'; ctx.globalAlpha = 0.5; ctx.fill(); ctx.globalAlpha = 1;
339
+ // Prediction ring
340
+ ctx.strokeStyle = res.cls === 0 ? '#58a6ff' : '#f0883e'; ctx.lineWidth = 2.5; ctx.stroke();
341
+
342
+ // K-circle (distance to Kth neighbor)
343
+ if (res.neighbors.length === K) {
344
+ const kthDist = res.neighbors[K-1].d;
345
+ const radiusPx = kthDist / (viewMaxX - viewMinX) * (W - 2*pad);
346
+ ctx.beginPath(); ctx.arc(cp.x, cp.y, radiusPx, 0, 2*Math.PI);
347
+ ctx.strokeStyle = 'rgba(188,140,255,0.3)'; ctx.lineWidth = 1; ctx.setLineDash([3,3]); ctx.stroke(); ctx.setLineDash([]);
348
+ }
349
+
350
+ // Confidence label
351
+ ctx.font = '10px system-ui'; ctx.fillStyle = '#c9d1d9';
352
+ ctx.textAlign = 'center';
353
+ ctx.fillText(`${(res.conf*100).toFixed(0)}%`, cp.x, cp.y - 13);
354
+ ctx.textAlign = 'left';
355
+ });
356
+
357
+ updateMetrics();
358
+ }
359
+
360
+ function drawBoundary() {
361
+ const res = 3;
362
+ const stepsX = Math.ceil((W - 2*pad) / res);
363
+ const stepsY = Math.ceil((H - 2*pad) / res);
364
+
365
+ for (let ix = 0; ix < stepsX; ix++) {
366
+ for (let iy = 0; iy < stepsY; iy++) {
367
+ const cx = pad + ix * res + res/2;
368
+ const cy = pad + iy * res + res/2;
369
+ const pt = fromCanvas(cx, cy);
370
+ const r = knnClassify(pt, K);
371
+ if (r.cls === 0) {
372
+ ctx.fillStyle = `rgba(63,185,80,${0.04 + r.conf*0.08})`;
373
+ } else {
374
+ ctx.fillStyle = `rgba(248,81,73,${0.04 + r.conf*0.08})`;
375
+ }
376
+ ctx.fillRect(cx - res/2, cy - res/2, res, res);
377
+ }
378
+ }
379
+ }
380
+
381
+ function updateMetrics() {
382
+ document.getElementById('kCur').textContent = K;
383
+ document.getElementById('trainN').textContent = trainData.length;
384
+ document.getElementById('testN').textContent = testData.length;
385
+
386
+ if (testData.length === 0) {
387
+ document.getElementById('accVal').textContent = 'β€”';
388
+ document.getElementById('accVal').className = 'val';
389
+ return;
390
+ }
391
+ // We don't have ground truth for click-added test points, show avg confidence
392
+ const avgConf = testData.reduce((s, t) => s + (t._conf || 0.5), 0) / testData.length;
393
+ const pct = (avgConf * 100).toFixed(1) + '%';
394
+ const el = document.getElementById('accVal');
395
+ el.textContent = pct;
396
+ el.className = avgConf > 0.75 ? 'val' : avgConf > 0.5 ? 'val warn' : 'val bad';
397
+
398
+ // Update info text
399
+ const predA = testData.filter(t => t._cls === 0).length;
400
+ const predB = testData.filter(t => t._cls === 1).length;
401
+ document.getElementById('infoBox').innerHTML =
402
+ `K=${K}: ${predA} points β†’ Class A, ${predB} points β†’ Class B<br>` +
403
+ `Avg confidence: ${pct} | Metric: ${distMetric} | Weight: ${weightMode}`;
404
+ }
405
+
406
+ // ═══════════════════════════════════════════════════════════════
407
+ // INTERACTIONS
408
+ // ═══════════════════════════════════════════════════════════════
409
+ C.addEventListener('click', e => {
410
+ const rect = C.getBoundingClientRect();
411
+ const cx = e.clientX - rect.left, cy = e.clientY - rect.top;
412
+ const pt = fromCanvas(cx, cy);
413
+ testData.push(pt);
414
+ draw();
415
+ });
416
+
417
+ // Sliders
418
+ document.getElementById('kSlider').addEventListener('input', e => {
419
+ K = parseInt(e.target.value);
420
+ document.getElementById('kVal').textContent = K;
421
+ draw();
422
+ });
423
+
424
+ document.getElementById('nSlider').addEventListener('input', e => {
425
+ document.getElementById('nVal').textContent = e.target.value;
426
+ });
427
+ document.getElementById('nSlider').addEventListener('change', regenerate);
428
+
429
+ document.getElementById('noiseSlider').addEventListener('input', e => {
430
+ document.getElementById('noiseVal').textContent = e.target.value + '%';
431
+ });
432
+ document.getElementById('noiseSlider').addEventListener('change', regenerate);
433
+
434
+ function regenerate() {
435
+ const type = document.getElementById('datasetSel').value;
436
+ const n = parseInt(document.getElementById('nSlider').value);
437
+ const noise = parseInt(document.getElementById('noiseSlider').value);
438
+ trainData = generateData(type, n, noise);
439
+ testData = [];
440
+ fitView();
441
+ draw();
442
+ }
443
+
444
+ function changeDataset() { regenerate(); }
445
+ function clearTest() { testData = []; draw(); }
446
+
447
+ function toggleBoundary() {
448
+ showBoundary = !showBoundary;
449
+ draw();
450
+ }
451
+
452
+ function setDist(m) {
453
+ distMetric = m;
454
+ ['distEuc','distMan','distMinkowski'].forEach(id => document.getElementById(id).classList.remove('active'));
455
+ if (m === 'euclidean') document.getElementById('distEuc').classList.add('active');
456
+ else if (m === 'manhattan') document.getElementById('distMan').classList.add('active');
457
+ else document.getElementById('distMinkowski').classList.add('active');
458
+ draw();
459
+ }
460
+
461
+ function setWeight(w) {
462
+ weightMode = w;
463
+ document.getElementById('weightUni').classList.toggle('active', w==='uniform');
464
+ document.getElementById('weightDist').classList.toggle('active', w==='distance');
465
+ draw();
466
+ }
467
+
468
+ // ═══════════════════════════════════════════════════════════════
469
+ // ANIMATION: Sweep K from 1 to 15
470
+ // ═══════════════════════════════════════════════════════════════
471
+ function toggleAnimate() {
472
+ if (animating) { stopAnimate(); return; }
473
+ animating = true;
474
+ document.getElementById('animBtn').textContent = 'Stop Animation';
475
+ document.getElementById('animBtn').classList.add('active');
476
+
477
+ // Add test points if none exist
478
+ if (testData.length === 0) {
479
+ for (let i = 0; i < 30; i++) {
480
+ testData.push({
481
+ x: viewMinX + rand() * (viewMaxX - viewMinX),
482
+ y: viewMinY + rand() * (viewMaxY - viewMinY)
483
+ });
484
+ }
485
+ }
486
+
487
+ let kAnim = 1;
488
+ const maxK = Math.min(15, trainData.length);
489
+ const interval = 1200 / animSpeed;
490
+
491
+ function step() {
492
+ if (!animating) return;
493
+ K = kAnim;
494
+ document.getElementById('kSlider').value = K;
495
+ document.getElementById('kVal').textContent = K;
496
+ document.getElementById('animBar').style.width = ((kAnim / maxK) * 100) + '%';
497
+ draw();
498
+ kAnim++;
499
+ if (kAnim > maxK) kAnim = 1;
500
+ animTimer = setTimeout(step, interval);
501
+ }
502
+ step();
503
+ }
504
+
505
+ function stopAnimate() {
506
+ animating = false;
507
+ clearTimeout(animTimer);
508
+ document.getElementById('animBtn').textContent = 'Animate K: 1β†’15';
509
+ document.getElementById('animBtn').classList.remove('active');
510
+ document.getElementById('animBar').style.width = '0%';
511
+ }
512
+
513
+ function setSpeed(s) {
514
+ animSpeed = s;
515
+ document.querySelectorAll('.speed-row .btn').forEach(b => b.classList.remove('active'));
516
+ if (s === 0.5) document.querySelectorAll('.speed-row .btn')[1].classList.add('active');
517
+ else if (s === 1) document.getElementById('sp1').classList.add('active');
518
+ else document.querySelectorAll('.speed-row .btn')[3].classList.add('active');
519
+ if (animating) { stopAnimate(); toggleAnimate(); }
520
+ }
521
+
522
+ // ═══════════════════════════════════════════════════════════════
523
+ // INIT
524
+ // ═══════════════════════════════════════════════════════════════
525
+ resize();
526
+ regenerate();
527
+ </script>
528
+ </body>
529
  </html>