abril4416 commited on
Commit
b2061c4
·
1 Parent(s): 90efbc3

Redesign linear/logistic cost landscape visualizations

Browse files
linear-regression/gradient-descent.html CHANGED
@@ -70,17 +70,27 @@
70
 
71
  <section class="panel output">
72
  <h2>Cost Landscape (Descent Direction)</h2>
73
- <div class="row">
74
- <label for="landscapeMode">Landscape View</label>
75
- <select id="landscapeMode">
76
- <option value="2d">2D Contour-Style + Arrows</option>
77
- <option value="3d">3D Surface + Trajectory</option>
78
- </select>
 
 
79
  </div>
80
- <p class="slice-label">
81
- 3D axes: x=theta0, y=theta1, z=cost J(theta). Upward along z means higher cost; the trajectory goes from higher to lower cost.
82
- </p>
83
- <canvas id="landscapeCanvas" width="980" height="420"></canvas>
 
 
 
 
 
 
 
 
84
  </section>
85
 
86
  <section class="panel output">
@@ -111,6 +121,7 @@
111
  </section>
112
  </main>
113
 
114
- <script src="gradient.js?v=20260812g"></script>
 
115
  </body>
116
  </html>
 
70
 
71
  <section class="panel output">
72
  <h2>Cost Landscape (Descent Direction)</h2>
73
+ <div id="landscapeSummary" class="landscape-summary"></div>
74
+
75
+ <div class="landscape-controls">
76
+ <button id="resetViewBtn" type="button" class="viz-btn">Reset View</button>
77
+ <button id="plotSmallerBtn" type="button" class="viz-btn">-</button>
78
+ <button id="plotFitBtn" type="button" class="viz-btn">Fit</button>
79
+ <button id="plotLargerBtn" type="button" class="viz-btn">+</button>
80
+ <button id="plotFullscreenBtn" type="button" class="viz-btn">Fullscreen</button>
81
  </div>
82
+
83
+ <div class="landscape-controls toggles">
84
+ <label class="viz-toggle"><input id="toggleSurface" type="checkbox" checked /> Surface</label>
85
+ <label class="viz-toggle"><input id="toggleContours" type="checkbox" checked /> Contours</label>
86
+ <label class="viz-toggle"><input id="toggleTrajectory" type="checkbox" checked /> Trajectory</label>
87
+ </div>
88
+
89
+ <div id="landscapePlotWrap" class="landscape-plot-wrap">
90
+ <div id="landscapePlot" class="landscape-plot"></div>
91
+ </div>
92
+
93
+ <p class="slice-label">Drag to rotate · Scroll to zoom · Resize from the corner</p>
94
  </section>
95
 
96
  <section class="panel output">
 
121
  </section>
122
  </main>
123
 
124
+ <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
125
+ <script src="gradient.js?v=20260813a"></script>
126
  </body>
127
  </html>
linear-regression/gradient.js CHANGED
@@ -7,17 +7,35 @@ const gdStep = document.getElementById("gdStep");
7
  const gdStepLabel = document.getElementById("gdStepLabel");
8
  const playGdBtn = document.getElementById("playGdBtn");
9
  const resetGdBtn = document.getElementById("resetGdBtn");
 
10
  const fitCanvas = document.getElementById("fitCanvas");
11
  const costCanvas = document.getElementById("costCanvas");
12
- const landscapeMode = document.getElementById("landscapeMode");
13
- const landscapeCanvas = document.getElementById("landscapeCanvas");
 
 
 
 
 
 
 
 
 
 
 
 
14
  const costSummary = document.getElementById("costSummary");
15
  const stepSummary = document.getElementById("stepSummary");
16
  const gdLogBody = document.getElementById("gdLogBody");
17
 
18
  const fitCtx = fitCanvas.getContext("2d");
19
  const costCtx = costCanvas.getContext("2d");
20
- const landscapeCtx = landscapeCanvas.getContext("2d");
 
 
 
 
 
21
 
22
  const state = {
23
  data: [],
@@ -25,13 +43,17 @@ const state = {
25
  currentStep: 0,
26
  timer: null,
27
  landscapeCache: null,
 
 
 
28
  };
29
 
30
  function init() {
31
  resizeCanvases();
 
 
32
 
33
  runGdBtn.addEventListener("click", runGradientDescent);
34
- landscapeMode.addEventListener("change", renderAll);
35
  window.addEventListener("resize", handleResize);
36
 
37
  gdStep.addEventListener("input", () => {
@@ -57,15 +79,43 @@ function init() {
57
  runGradientDescent();
58
  }
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  function handleResize() {
61
  resizeCanvases();
 
 
 
 
 
62
  if (state.steps.length) renderAll();
63
  }
64
 
65
  function resizeCanvases() {
66
  resizeCanvasToContainer(fitCanvas, 0.46, 240, 480);
67
  resizeCanvasToContainer(costCanvas, 0.28, 200, 320);
68
- resizeCanvasToContainer(landscapeCanvas, 0.5, 280, 520);
69
  }
70
 
71
  function resizeCanvasToContainer(canvas, ratio, minHeight, maxHeight) {
@@ -82,6 +132,79 @@ function resizeCanvasToContainer(canvas, ratio, minHeight, maxHeight) {
82
  }
83
  }
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  function runGradientDescent() {
86
  stopPlayback();
87
 
@@ -242,8 +365,8 @@ function renderCostPlot() {
242
 
243
  function renderCostLandscape() {
244
  const data = getLandscapeData();
245
- if (landscapeMode.value === "3d") renderLandscape3D(data);
246
- else renderLandscape2D(data);
247
  }
248
 
249
  function getLandscapeData() {
@@ -265,16 +388,24 @@ function getLandscapeData() {
265
  const t1Min = t1MinRaw - t1Pad;
266
  const t1Max = t1MaxRaw + t1Pad;
267
 
268
- const gridN = 44;
 
 
 
 
 
 
 
 
269
  const grid = [];
270
  let zMin = Infinity;
271
  let zMax = -Infinity;
272
 
273
  for (let gy = 0; gy < gridN; gy += 1) {
274
- const t1 = t1Min + (gy / (gridN - 1)) * (t1Max - t1Min);
275
  const row = [];
276
  for (let gx = 0; gx < gridN; gx += 1) {
277
- const t0 = t0Min + (gx / (gridN - 1)) * (t0Max - t0Min);
278
  const cost = costAndGradient(state.data, [t0, t1]).cost;
279
  row.push(cost);
280
  zMin = Math.min(zMin, cost);
@@ -284,6 +415,7 @@ function getLandscapeData() {
284
  }
285
 
286
  const path = state.steps.map((s) => ({
 
287
  t0: s.theta[0],
288
  t1: s.theta[1],
289
  z: s.cost,
@@ -291,9 +423,13 @@ function getLandscapeData() {
291
  g1: s.grad[1],
292
  }));
293
 
 
 
294
  state.landscapeCache = {
295
  grid,
296
  gridN,
 
 
297
  t0Min,
298
  t0Max,
299
  t1Min,
@@ -301,275 +437,259 @@ function getLandscapeData() {
301
  zMin,
302
  zMax,
303
  path,
 
304
  };
305
 
306
  return state.landscapeCache;
307
  }
308
 
309
- function renderLandscape2D(data) {
310
- const ctx = landscapeCtx;
311
- const { width, height } = landscapeCanvas;
312
- const pad = 54;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
 
314
- ctx.clearRect(0, 0, width, height);
 
315
 
316
- const toX = (t0) => pad + ((t0 - data.t0Min) / (data.t0Max - data.t0Min || 1)) * (width - 2 * pad);
317
- const toY = (t1) => height - pad - ((t1 - data.t1Min) / (data.t1Max - data.t1Min || 1)) * (height - 2 * pad);
 
318
 
319
- const cellW = (width - 2 * pad) / (data.gridN - 1);
320
- const cellH = (height - 2 * pad) / (data.gridN - 1);
321
 
322
- for (let gy = 0; gy < data.gridN - 1; gy += 1) {
323
- for (let gx = 0; gx < data.gridN - 1; gx += 1) {
324
- const z = data.grid[gy][gx];
325
- const ratio = (z - data.zMin) / (data.zMax - data.zMin || 1);
326
- ctx.fillStyle = heatColor(ratio);
327
- ctx.fillRect(pad + gx * cellW, pad + gy * cellH, cellW + 1, cellH + 1);
328
- }
329
- }
330
 
331
- drawAxes2D(ctx, width, height, pad, "theta0", "theta1");
332
 
333
- ctx.strokeStyle = "#f4f1ea";
334
- ctx.lineWidth = 2.4;
335
- ctx.beginPath();
336
- data.path.forEach((p, idx) => {
337
- const x = toX(p.t0);
338
- const y = toY(p.t1);
339
- if (idx === 0) ctx.moveTo(x, y);
340
- else ctx.lineTo(x, y);
341
  });
342
- ctx.stroke();
343
 
344
- for (let idx = 0; idx < data.path.length - 1; idx += Math.max(1, Math.floor(data.path.length / 14))) {
345
- const p1 = data.path[idx];
346
- const p2 = data.path[Math.min(idx + 1, data.path.length - 1)];
347
- drawArrow(ctx, toX(p1.t0), toY(p1.t1), toX(p2.t0), toY(p2.t1), "#14212b", 8);
348
  }
349
 
350
- const cur = data.path[state.currentStep];
351
- drawArrow(
352
- ctx,
353
- toX(cur.t0),
354
- toY(cur.t1),
355
- toX(cur.t0 - cur.g0 * 0.15),
356
- toY(cur.t1 - cur.g1 * 0.15),
357
- "#dd5e2f",
358
- 11
359
- );
360
-
361
- ctx.fillStyle = "#dd5e2f";
362
- ctx.beginPath();
363
- ctx.arc(toX(cur.t0), toY(cur.t1), 5.5, 0, Math.PI * 2);
364
- ctx.fill();
365
  }
366
 
367
- function renderLandscape3D(data) {
368
- const ctx = landscapeCtx;
369
- const { width, height } = landscapeCanvas;
370
 
371
- ctx.clearRect(0, 0, width, height);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
373
- const t0Mid = (data.t0Min + data.t0Max) / 2;
374
- const t1Mid = (data.t1Min + data.t1Max) / 2;
375
- const t0Range = data.t0Max - data.t0Min || 1;
376
- const t1Range = data.t1Max - data.t1Min || 1;
377
- const zRange = data.zMax - data.zMin || 1;
378
- const zScale = Math.max(t0Range, t1Range) * 0.95;
379
-
380
- const baseCamera = {
381
- yaw: -1.08,
382
- pitch: 0.66,
383
- dist: 21,
384
- lockCostUp: true,
385
- groundTilt: 0.62,
386
- costTilt: 1.05,
387
- scale: 1,
388
- cx: 0,
389
- cy: 0,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  };
391
 
392
- const camera = fitCameraToScene(data, width, height, zScale, t0Mid, t1Mid, baseCamera);
393
-
394
- const project = (t0, t1, z) => {
395
- const x1 = t0 - t0Mid;
396
- const x2 = t1 - t1Mid;
397
- const y = ((z - data.zMin) / zRange) * zScale;
398
- return project3D(x1, x2, y, camera);
399
  };
400
 
401
- ctx.strokeStyle = "rgba(30, 44, 53, 0.26)";
402
- ctx.lineWidth = 1;
403
-
404
- for (let gy = 0; gy < data.gridN; gy += 1) {
405
- ctx.beginPath();
406
- let first = true;
407
- for (let gx = 0; gx < data.gridN; gx += 1) {
408
- const t0 = data.t0Min + (gx / (data.gridN - 1)) * (data.t0Max - data.t0Min);
409
- const t1 = data.t1Min + (gy / (data.gridN - 1)) * (data.t1Max - data.t1Min);
410
- const p = project(t0, t1, data.grid[gy][gx]);
411
- if (first) {
412
- ctx.moveTo(p.x, p.y);
413
- first = false;
414
- } else {
415
- ctx.lineTo(p.x, p.y);
416
- }
417
  }
418
- ctx.stroke();
419
- }
420
 
421
- for (let gx = 0; gx < data.gridN; gx += 1) {
422
- ctx.beginPath();
423
- let first = true;
424
- for (let gy = 0; gy < data.gridN; gy += 1) {
425
- const t0 = data.t0Min + (gx / (data.gridN - 1)) * (data.t0Max - data.t0Min);
426
- const t1 = data.t1Min + (gy / (data.gridN - 1)) * (data.t1Max - data.t1Min);
427
- const p = project(t0, t1, data.grid[gy][gx]);
428
- if (first) {
429
- ctx.moveTo(p.x, p.y);
430
- first = false;
431
- } else {
432
- ctx.lineTo(p.x, p.y);
433
- }
434
- }
435
- ctx.stroke();
436
- }
437
-
438
- drawSurfaceAxes3D(ctx, data, project);
439
- drawAxisLegend3D(ctx, width, height);
440
-
441
- for (let idx = 0; idx < data.path.length - 1; idx += 1) {
442
- const p1m = data.path[idx];
443
- const p2m = data.path[idx + 1];
444
- const p1 = project(p1m.t0, p1m.t1, p1m.z);
445
- const p2 = project(p2m.t0, p2m.t1, p2m.z);
446
- const t = idx / Math.max(1, data.path.length - 2);
447
- ctx.strokeStyle = trajectoryColor(t);
448
- ctx.lineWidth = 2.8;
449
- ctx.beginPath();
450
- ctx.moveTo(p1.x, p1.y);
451
- ctx.lineTo(p2.x, p2.y);
452
- ctx.stroke();
453
- }
454
-
455
- for (let idx = 0; idx < data.path.length - 1; idx += Math.max(1, Math.floor(data.path.length / 10))) {
456
- const p1m = data.path[idx];
457
- const p2m = data.path[Math.min(idx + 1, data.path.length - 1)];
458
- const p1 = project(p1m.t0, p1m.t1, p1m.z);
459
- const p2 = project(p2m.t0, p2m.t1, p2m.z);
460
- const t = idx / Math.max(1, data.path.length - 2);
461
- drawArrow(ctx, p1.x, p1.y, p2.x, p2.y, trajectoryColor(t), 9);
462
- }
463
-
464
- const cur = data.path[state.currentStep];
465
- const curP = project(cur.t0, cur.t1, cur.z);
466
- ctx.fillStyle = "#dd5e2f";
467
- ctx.beginPath();
468
- ctx.arc(curP.x, curP.y, 5.5, 0, Math.PI * 2);
469
- ctx.fill();
470
-
471
- const start = data.path[0];
472
- const end = data.path[data.path.length - 1];
473
- const startP = project(start.t0, start.t1, start.z);
474
- const endP = project(end.t0, end.t1, end.z);
475
-
476
- ctx.fillStyle = "#1e4d4b";
477
- ctx.beginPath();
478
- ctx.arc(startP.x, startP.y, 4.2, 0, Math.PI * 2);
479
- ctx.fill();
480
- draw3DLabel(ctx, startP.x + 8, startP.y - 8, `start J=${start.z.toFixed(2)}`);
481
-
482
- ctx.fillStyle = "#dd5e2f";
483
- ctx.beginPath();
484
- ctx.arc(endP.x, endP.y, 4.2, 0, Math.PI * 2);
485
- ctx.fill();
486
- draw3DLabel(ctx, endP.x + 8, endP.y + 14, `end J=${end.z.toFixed(2)}`);
487
-
488
- const dropPct = ((start.z - end.z) / Math.max(1e-9, start.z)) * 100;
489
- draw3DLabel(
490
- ctx,
491
- Math.min(startP.x, endP.x) + 14,
492
- Math.min(startP.y, endP.y) - 18,
493
- `cost drop: ${dropPct.toFixed(1)}%`
494
- );
495
- }
496
-
497
- function fitCameraToScene(data, width, height, zScale, t0Mid, t1Mid, baseCamera) {
498
- const points = [];
499
- const gridStep = Math.max(1, Math.floor(data.gridN / 18));
500
-
501
- for (let gy = 0; gy < data.gridN; gy += gridStep) {
502
- for (let gx = 0; gx < data.gridN; gx += gridStep) {
503
- const t0 = data.t0Min + (gx / (data.gridN - 1)) * (data.t0Max - data.t0Min);
504
- const t1 = data.t1Min + (gy / (data.gridN - 1)) * (data.t1Max - data.t1Min);
505
- const z = data.grid[gy][gx];
506
- points.push(toCameraPoint(t0, t1, z, data, zScale, t0Mid, t1Mid, baseCamera));
507
- }
508
- }
509
-
510
- for (const p of data.path) {
511
- points.push(toCameraPoint(p.t0, p.t1, p.z, data, zScale, t0Mid, t1Mid, baseCamera));
512
- }
513
-
514
- const axisPoints = [
515
- [data.t0Min, data.t1Min, data.zMin], // axis origin
516
- [data.t0Max, data.t1Min, data.zMin], // theta0 end
517
- [data.t0Min, data.t1Max, data.zMin], // theta1 end
518
- [data.t0Min, data.t1Min, data.zMax], // cost end
519
- ];
520
- for (const [t0, t1, z] of axisPoints) {
521
- points.push(toCameraPoint(t0, t1, z, data, zScale, t0Mid, t1Mid, baseCamera));
522
- }
523
-
524
- let minX = Infinity;
525
- let maxX = -Infinity;
526
- let minY = Infinity;
527
- let maxY = -Infinity;
528
-
529
- for (const p of points) {
530
- if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) continue;
531
- minX = Math.min(minX, p.x);
532
- maxX = Math.max(maxX, p.x);
533
- minY = Math.min(minY, p.y);
534
- maxY = Math.max(maxY, p.y);
535
- }
536
-
537
- if (!Number.isFinite(minX) || !Number.isFinite(maxX) || !Number.isFinite(minY) || !Number.isFinite(maxY)) {
538
- return {
539
- ...baseCamera,
540
- scale: 24,
541
- cx: width * 0.5,
542
- cy: height * 0.58,
543
- };
544
- }
545
-
546
- const sceneW = Math.max(1e-6, maxX - minX);
547
- const sceneH = Math.max(1e-6, maxY - minY);
548
- const padX = Math.max(74, Math.floor(width * 0.2));
549
- const padY = Math.max(72, Math.floor(height * 0.22));
550
- const availW = Math.max(120, width - padX * 2);
551
- const availH = Math.max(120, height - padY * 2);
552
- const rawScale = Math.min(availW / sceneW, availH / sceneH);
553
- const scale = Math.max(1.5, Math.min(90, rawScale * 0.62));
554
- const midX = (minX + maxX) / 2;
555
- const midY = (minY + maxY) / 2;
556
 
 
557
  return {
558
- ...baseCamera,
559
- scale,
560
- cx: width / 2 - midX * scale,
561
- cy: height / 2 - midY * scale,
562
  };
563
  }
564
 
565
- function toCameraPoint(t0, t1, z, data, zScale, t0Mid, t1Mid, camera) {
566
- const zRange = data.zMax - data.zMin || 1;
567
- const x1 = t0 - t0Mid;
568
- const x2 = t1 - t1Mid;
569
- const y = ((z - data.zMin) / zRange) * zScale;
570
- return project3D(x1, x2, y, camera);
571
- }
572
-
573
  function renderCostSummary() {
574
  const first = state.steps[0];
575
  const last = state.steps[state.steps.length - 1];
@@ -577,7 +697,7 @@ function renderCostSummary() {
577
  costSummary.innerHTML = `
578
  <article class="matrix-card">
579
  <p class="matrix-title">Cost Function</p>
580
- <p class="formula-line">J(θ) = (1/2m) Σ(θ0 + θ1*x1 - y)^2</p>
581
  </article>
582
  <article class="matrix-card">
583
  <p class="matrix-title">Result</p>
@@ -645,137 +765,6 @@ function drawAxes2D(ctx, width, height, pad, xLabel, yLabel) {
645
  ctx.fillText(yLabel, pad - 30, pad - 10);
646
  }
647
 
648
- function drawSurfaceAxes3D(ctx, data, project) {
649
- const origin = project(data.t0Min, data.t1Min, data.zMin);
650
- const t0End = project(data.t0Max, data.t1Min, data.zMin);
651
- const t1End = project(data.t0Min, data.t1Max, data.zMin);
652
- const zEnd = project(data.t0Min, data.t1Min, data.zMax);
653
-
654
- drawArrow(ctx, origin.x, origin.y, t0End.x, t0End.y, "#30444f", 9);
655
- drawArrow(ctx, origin.x, origin.y, t1End.x, t1End.y, "#30444f", 9);
656
- drawArrow(ctx, origin.x, origin.y, zEnd.x, zEnd.y, "#b23f2c", 10);
657
-
658
- draw3DLabel(ctx, t0End.x + 7, t0End.y + 14, "theta0 (+)");
659
- draw3DLabel(ctx, t1End.x + 7, t1End.y + 14, "theta1 (+)");
660
- draw3DLabel(ctx, zEnd.x + 8, zEnd.y - 10, "cost J(theta) increases");
661
- draw3DLabel(ctx, origin.x + 8, origin.y + 16, "cost decreases toward valley");
662
- }
663
-
664
- function draw3DLabel(ctx, x, y, text) {
665
- ctx.fillStyle = "#23333d";
666
- ctx.font = "12px 'Courier New', monospace";
667
- ctx.fillText(text, x, y);
668
- }
669
-
670
- function drawAxisLegend3D(ctx, width, height) {
671
- const boxW = Math.min(270, Math.max(200, Math.floor(width * 0.34)));
672
- const boxH = Math.min(120, Math.max(92, Math.floor(height * 0.24)));
673
- const boxX = 16;
674
- const boxY = height - boxH - 14;
675
- const ox = boxX + Math.floor(boxW * 0.36);
676
- const oy = boxY + Math.floor(boxH * 0.78);
677
-
678
- ctx.fillStyle = "rgba(245, 244, 240, 0.92)";
679
- ctx.strokeStyle = "rgba(35, 51, 61, 0.4)";
680
- ctx.lineWidth = 1;
681
- ctx.beginPath();
682
- ctx.rect(boxX, boxY, boxW, boxH);
683
- ctx.fill();
684
- ctx.stroke();
685
-
686
- const xLen = Math.floor(boxW * 0.42);
687
- const yLen = Math.floor(boxW * 0.24);
688
- const zLen = Math.floor(boxH * 0.64);
689
-
690
- drawArrow(ctx, ox, oy, ox + xLen, oy, "#1f5d7a", 9);
691
- drawArrow(ctx, ox, oy, ox - yLen, oy - yLen, "#2f7a3a", 9);
692
- drawArrow(ctx, ox, oy, ox, oy - zLen, "#b23f2c", 9);
693
-
694
- ctx.fillStyle = "#1f5d7a";
695
- ctx.font = "12px 'Courier New', monospace";
696
- ctx.fillText("theta0 (+)", ox + xLen + 4, oy + 4);
697
-
698
- ctx.fillStyle = "#2f7a3a";
699
- ctx.fillText("theta1 (+)", Math.max(6, ox - yLen - 62), oy - yLen - 6);
700
-
701
- ctx.fillStyle = "#b23f2c";
702
- ctx.fillText("cost J(theta) (+)", ox + 8, oy - zLen - 8);
703
- }
704
-
705
- function project3D(x1, x2, y, camera) {
706
- if (camera.lockCostUp) {
707
- const cosY = Math.cos(camera.yaw);
708
- const sinY = Math.sin(camera.yaw);
709
- const u = x1 * cosY - x2 * sinY;
710
- const ground = x1 * sinY + x2 * cosY;
711
- const v = ground * camera.groundTilt + y * camera.costTilt;
712
-
713
- return {
714
- x: camera.cx + u * camera.scale,
715
- y: camera.cy - v * camera.scale,
716
- };
717
- }
718
-
719
- const cosY = Math.cos(camera.yaw);
720
- const sinY = Math.sin(camera.yaw);
721
- const cosX = Math.cos(camera.pitch);
722
- const sinX = Math.sin(camera.pitch);
723
-
724
- const u = x1 * cosY - x2 * sinY;
725
- const v0 = x1 * sinY + x2 * cosY;
726
- const v = v0 * cosX - y * sinX;
727
- const w = v0 * sinX + y * cosX;
728
-
729
- const perspective = camera.dist / (camera.dist + w + 0.001);
730
- return {
731
- x: camera.cx + u * camera.scale * perspective,
732
- y: camera.cy - v * camera.scale * perspective,
733
- };
734
- }
735
-
736
- function heatColor(t) {
737
- const clamped = Math.max(0, Math.min(1, t));
738
- const r = Math.round(34 + clamped * 210);
739
- const g = Math.round(48 + (1 - clamped) * 140);
740
- const b = Math.round(95 + (1 - clamped) * 80);
741
- return `rgb(${r}, ${g}, ${b})`;
742
- }
743
-
744
- function trajectoryColor(t) {
745
- const clamped = Math.max(0, Math.min(1, t));
746
- const r = Math.round(22 + clamped * 205);
747
- const g = Math.round(88 + clamped * 28);
748
- const b = Math.round(120 - clamped * 68);
749
- return `rgb(${r}, ${g}, ${b})`;
750
- }
751
-
752
- function drawArrow(ctx, x1, y1, x2, y2, color, headSize) {
753
- const dx = x2 - x1;
754
- const dy = y2 - y1;
755
- const len = Math.hypot(dx, dy);
756
- if (len < 0.0001) return;
757
-
758
- const ux = dx / len;
759
- const uy = dy / len;
760
- const hx = x2 - ux * headSize;
761
- const hy = y2 - uy * headSize;
762
-
763
- ctx.strokeStyle = color;
764
- ctx.fillStyle = color;
765
- ctx.lineWidth = 2;
766
- ctx.beginPath();
767
- ctx.moveTo(x1, y1);
768
- ctx.lineTo(x2, y2);
769
- ctx.stroke();
770
-
771
- ctx.beginPath();
772
- ctx.moveTo(x2, y2);
773
- ctx.lineTo(hx - uy * (headSize * 0.5), hy + ux * (headSize * 0.5));
774
- ctx.lineTo(hx + uy * (headSize * 0.5), hy - ux * (headSize * 0.5));
775
- ctx.closePath();
776
- ctx.fill();
777
- }
778
-
779
  function startPlayback() {
780
  if (state.timer || !state.steps.length) return;
781
  playGdBtn.textContent = "Pause";
 
7
  const gdStepLabel = document.getElementById("gdStepLabel");
8
  const playGdBtn = document.getElementById("playGdBtn");
9
  const resetGdBtn = document.getElementById("resetGdBtn");
10
+
11
  const fitCanvas = document.getElementById("fitCanvas");
12
  const costCanvas = document.getElementById("costCanvas");
13
+ const landscapePlotWrap = document.getElementById("landscapePlotWrap");
14
+ const landscapePlot = document.getElementById("landscapePlot");
15
+ const landscapeSummary = document.getElementById("landscapeSummary");
16
+
17
+ const resetViewBtn = document.getElementById("resetViewBtn");
18
+ const plotSmallerBtn = document.getElementById("plotSmallerBtn");
19
+ const plotFitBtn = document.getElementById("plotFitBtn");
20
+ const plotLargerBtn = document.getElementById("plotLargerBtn");
21
+ const plotFullscreenBtn = document.getElementById("plotFullscreenBtn");
22
+
23
+ const toggleSurface = document.getElementById("toggleSurface");
24
+ const toggleContours = document.getElementById("toggleContours");
25
+ const toggleTrajectory = document.getElementById("toggleTrajectory");
26
+
27
  const costSummary = document.getElementById("costSummary");
28
  const stepSummary = document.getElementById("stepSummary");
29
  const gdLogBody = document.getElementById("gdLogBody");
30
 
31
  const fitCtx = fitCanvas.getContext("2d");
32
  const costCtx = costCanvas.getContext("2d");
33
+
34
+ const DEFAULT_CAMERA = {
35
+ eye: { x: 1.5, y: 1.5, z: 1.1 },
36
+ center: { x: 0, y: 0, z: 0 },
37
+ up: { x: 0, y: 0, z: 1 },
38
+ };
39
 
40
  const state = {
41
  data: [],
 
43
  currentStep: 0,
44
  timer: null,
45
  landscapeCache: null,
46
+ landscapeCamera: null,
47
+ landscapePlotReady: false,
48
+ landscapeResizeObserver: null,
49
  };
50
 
51
  function init() {
52
  resizeCanvases();
53
+ fitLandscapeSize();
54
+ initLandscapeInteractions();
55
 
56
  runGdBtn.addEventListener("click", runGradientDescent);
 
57
  window.addEventListener("resize", handleResize);
58
 
59
  gdStep.addEventListener("input", () => {
 
79
  runGradientDescent();
80
  }
81
 
82
+ function initLandscapeInteractions() {
83
+ resetViewBtn.addEventListener("click", resetLandscapeView);
84
+ plotFitBtn.addEventListener("click", () => fitLandscapeSize(true));
85
+ plotLargerBtn.addEventListener("click", () => scaleLandscapeSize(1.14));
86
+ plotSmallerBtn.addEventListener("click", () => scaleLandscapeSize(1 / 1.14));
87
+ plotFullscreenBtn.addEventListener("click", toggleLandscapeFullscreen);
88
+
89
+ for (const cb of [toggleSurface, toggleContours, toggleTrajectory]) {
90
+ cb.addEventListener("change", renderCostLandscape);
91
+ }
92
+
93
+ document.addEventListener("fullscreenchange", () => {
94
+ plotFullscreenBtn.textContent = document.fullscreenElement === landscapePlotWrap ? "Exit Fullscreen" : "Fullscreen";
95
+ resizeLandscapePlot();
96
+ });
97
+
98
+ if (window.ResizeObserver) {
99
+ state.landscapeResizeObserver = new ResizeObserver(() => {
100
+ resizeLandscapePlot();
101
+ });
102
+ state.landscapeResizeObserver.observe(landscapePlotWrap);
103
+ }
104
+ }
105
+
106
  function handleResize() {
107
  resizeCanvases();
108
+
109
+ if (document.fullscreenElement !== landscapePlotWrap) {
110
+ constrainLandscapeSizeToContainer();
111
+ }
112
+
113
  if (state.steps.length) renderAll();
114
  }
115
 
116
  function resizeCanvases() {
117
  resizeCanvasToContainer(fitCanvas, 0.46, 240, 480);
118
  resizeCanvasToContainer(costCanvas, 0.28, 200, 320);
 
119
  }
120
 
121
  function resizeCanvasToContainer(canvas, ratio, minHeight, maxHeight) {
 
132
  }
133
  }
134
 
135
+ function getLandscapeFitSize() {
136
+ const host = landscapePlotWrap.parentElement;
137
+ const hostWidth = host ? host.clientWidth : window.innerWidth;
138
+ const maxW = Math.max(320, Math.min(1000, hostWidth - 4));
139
+
140
+ if (window.innerWidth < 680) {
141
+ return { width: maxW, height: 380 };
142
+ }
143
+
144
+ if (window.innerWidth < 1024) {
145
+ return { width: Math.min(maxW, 920), height: 560 };
146
+ }
147
+
148
+ return { width: Math.min(maxW, 980), height: 640 };
149
+ }
150
+
151
+ function fitLandscapeSize(forceResize = false) {
152
+ const size = getLandscapeFitSize();
153
+ setLandscapeSize(size.width, size.height, forceResize);
154
+ }
155
+
156
+ function setLandscapeSize(rawW, rawH, forceResize = false) {
157
+ const host = landscapePlotWrap.parentElement;
158
+ const hostMaxW = host ? host.clientWidth - 4 : rawW;
159
+ const isFullscreen = document.fullscreenElement === landscapePlotWrap;
160
+
161
+ const minW = 320;
162
+ const minH = 280;
163
+ const maxW = isFullscreen ? Math.min(1800, window.innerWidth - 24) : Math.max(minW, hostMaxW);
164
+ const maxH = isFullscreen ? Math.min(1200, window.innerHeight - 24) : Math.min(900, window.innerHeight - 150);
165
+
166
+ const width = Math.round(Math.max(minW, Math.min(maxW, rawW)));
167
+ const height = Math.round(Math.max(minH, Math.min(maxH, rawH)));
168
+
169
+ if (forceResize || landscapePlotWrap.clientWidth !== width || landscapePlotWrap.clientHeight !== height) {
170
+ landscapePlotWrap.style.width = `${width}px`;
171
+ landscapePlotWrap.style.height = `${height}px`;
172
+ resizeLandscapePlot();
173
+ }
174
+ }
175
+
176
+ function constrainLandscapeSizeToContainer() {
177
+ setLandscapeSize(landscapePlotWrap.clientWidth, landscapePlotWrap.clientHeight, true);
178
+ }
179
+
180
+ function scaleLandscapeSize(factor) {
181
+ setLandscapeSize(landscapePlotWrap.clientWidth * factor, landscapePlotWrap.clientHeight * factor, true);
182
+ }
183
+
184
+ function resizeLandscapePlot() {
185
+ if (!state.landscapePlotReady || typeof Plotly === "undefined") return;
186
+ requestAnimationFrame(() => {
187
+ Plotly.Plots.resize(landscapePlot);
188
+ });
189
+ }
190
+
191
+ function toggleLandscapeFullscreen() {
192
+ if (document.fullscreenElement === landscapePlotWrap) {
193
+ document.exitFullscreen();
194
+ return;
195
+ }
196
+
197
+ if (landscapePlotWrap.requestFullscreen) {
198
+ landscapePlotWrap.requestFullscreen();
199
+ }
200
+ }
201
+
202
+ function resetLandscapeView() {
203
+ state.landscapeCamera = cloneCamera(DEFAULT_CAMERA);
204
+ if (!state.landscapePlotReady || typeof Plotly === "undefined") return;
205
+ Plotly.relayout(landscapePlot, { "scene.camera": state.landscapeCamera });
206
+ }
207
+
208
  function runGradientDescent() {
209
  stopPlayback();
210
 
 
365
 
366
  function renderCostLandscape() {
367
  const data = getLandscapeData();
368
+ renderLandscapeSummary();
369
+ renderLandscapePlotly(data);
370
  }
371
 
372
  function getLandscapeData() {
 
388
  const t1Min = t1MinRaw - t1Pad;
389
  const t1Max = t1MaxRaw + t1Pad;
390
 
391
+ const gridN = 64;
392
+ const theta0Values = [];
393
+ const theta1Values = [];
394
+
395
+ for (let i = 0; i < gridN; i += 1) {
396
+ theta0Values.push(t0Min + (i / (gridN - 1)) * (t0Max - t0Min));
397
+ theta1Values.push(t1Min + (i / (gridN - 1)) * (t1Max - t1Min));
398
+ }
399
+
400
  const grid = [];
401
  let zMin = Infinity;
402
  let zMax = -Infinity;
403
 
404
  for (let gy = 0; gy < gridN; gy += 1) {
405
+ const t1 = theta1Values[gy];
406
  const row = [];
407
  for (let gx = 0; gx < gridN; gx += 1) {
408
+ const t0 = theta0Values[gx];
409
  const cost = costAndGradient(state.data, [t0, t1]).cost;
410
  row.push(cost);
411
  zMin = Math.min(zMin, cost);
 
415
  }
416
 
417
  const path = state.steps.map((s) => ({
418
+ step: s.step,
419
  t0: s.theta[0],
420
  t1: s.theta[1],
421
  z: s.cost,
 
423
  g1: s.grad[1],
424
  }));
425
 
426
+ const contourLines = buildContourLines(theta0Values, theta1Values, grid, zMin, zMax, 9);
427
+
428
  state.landscapeCache = {
429
  grid,
430
  gridN,
431
+ theta0Values,
432
+ theta1Values,
433
  t0Min,
434
  t0Max,
435
  t1Min,
 
437
  zMin,
438
  zMax,
439
  path,
440
+ contourLines,
441
  };
442
 
443
  return state.landscapeCache;
444
  }
445
 
446
+ function buildContourLines(theta0Values, theta1Values, grid, zMin, zMax, levelsCount) {
447
+ const xs = [];
448
+ const ys = [];
449
+ const zs = [];
450
+ const zBase = zMin;
451
+
452
+ for (let levelIdx = 1; levelIdx <= levelsCount; levelIdx += 1) {
453
+ const level = zMin + (levelIdx / (levelsCount + 1)) * (zMax - zMin);
454
+
455
+ for (let y = 0; y < theta1Values.length - 1; y += 1) {
456
+ for (let x = 0; x < theta0Values.length - 1; x += 1) {
457
+ const p00 = { x: theta0Values[x], y: theta1Values[y], z: grid[y][x] };
458
+ const p10 = { x: theta0Values[x + 1], y: theta1Values[y], z: grid[y][x + 1] };
459
+ const p11 = { x: theta0Values[x + 1], y: theta1Values[y + 1], z: grid[y + 1][x + 1] };
460
+ const p01 = { x: theta0Values[x], y: theta1Values[y + 1], z: grid[y + 1][x] };
461
+
462
+ const intersections = [];
463
+
464
+ addContourIntersection(intersections, p00, p10, level);
465
+ addContourIntersection(intersections, p10, p11, level);
466
+ addContourIntersection(intersections, p11, p01, level);
467
+ addContourIntersection(intersections, p01, p00, level);
468
+
469
+ if (intersections.length === 2) {
470
+ xs.push(intersections[0].x, intersections[1].x, null);
471
+ ys.push(intersections[0].y, intersections[1].y, null);
472
+ zs.push(zBase, zBase, null);
473
+ } else if (intersections.length === 4) {
474
+ xs.push(intersections[0].x, intersections[1].x, null, intersections[2].x, intersections[3].x, null);
475
+ ys.push(intersections[0].y, intersections[1].y, null, intersections[2].y, intersections[3].y, null);
476
+ zs.push(zBase, zBase, null, zBase, zBase, null);
477
+ }
478
+ }
479
+ }
480
+ }
481
 
482
+ return { x: xs, y: ys, z: zs };
483
+ }
484
 
485
+ function addContourIntersection(bucket, a, b, level) {
486
+ const aSide = a.z - level;
487
+ const bSide = b.z - level;
488
 
489
+ if (aSide === 0 && bSide === 0) return;
490
+ if (aSide * bSide > 0) return;
491
 
492
+ const denom = b.z - a.z;
493
+ const t = Math.abs(denom) < 1e-12 ? 0.5 : (level - a.z) / denom;
 
 
 
 
 
 
494
 
495
+ if (t < 0 || t > 1) return;
496
 
497
+ bucket.push({
498
+ x: a.x + t * (b.x - a.x),
499
+ y: a.y + t * (b.y - a.y),
 
 
 
 
 
500
  });
501
+ }
502
 
503
+ function renderLandscapeSummary() {
504
+ if (!state.steps.length) {
505
+ landscapeSummary.innerHTML = "";
506
+ return;
507
  }
508
 
509
+ const first = state.steps[0];
510
+ const final = state.steps[state.steps.length - 1];
511
+ const current = state.steps[state.currentStep];
512
+ const reduction = ((first.cost - final.cost) / Math.max(1e-9, first.cost)) * 100;
513
+
514
+ landscapeSummary.innerHTML = `
515
+ <span class="landscape-pill">Start cost: ${first.cost.toFixed(2)}</span>
516
+ <span class="landscape-pill">Final cost: ${final.cost.toFixed(2)}</span>
517
+ <span class="landscape-pill">Reduction: ${reduction.toFixed(1)}%</span>
518
+ <span class="landscape-pill">Iterations: ${state.steps.length - 1}</span>
519
+ <span class="landscape-pill">Current step: ${state.currentStep}</span>
520
+ <span class="landscape-pill">Current cost: ${current.cost.toFixed(2)}</span>
521
+ `;
 
 
522
  }
523
 
524
+ function renderLandscapePlotly(data) {
525
+ if (typeof Plotly === "undefined") return;
 
526
 
527
+ const trajectoryPath = data.path;
528
+ const current = data.path[state.currentStep];
529
+ const start = data.path[0];
530
+ const final = data.path[data.path.length - 1];
531
+
532
+ const baseHover =
533
+ "Iteration: %{customdata[0]}<br>theta0: %{x:.4f}<br>theta1: %{y:.4f}<br>Cost J(theta): %{z:.4f}<extra></extra>";
534
+
535
+ const traces = [
536
+ {
537
+ type: "surface",
538
+ name: "Cost surface",
539
+ x: data.theta0Values,
540
+ y: data.theta1Values,
541
+ z: data.grid,
542
+ opacity: 0.82,
543
+ colorscale: "Viridis",
544
+ showscale: true,
545
+ colorbar: {
546
+ title: "Cost J",
547
+ thickness: 16,
548
+ },
549
+ contours: {
550
+ z: {
551
+ show: false,
552
+ },
553
+ },
554
+ visible: toggleSurface.checked,
555
+ hovertemplate: "theta0: %{x:.3f}<br>theta1: %{y:.3f}<br>Cost J(theta): %{z:.4f}<extra></extra>",
556
+ },
557
+ {
558
+ type: "scatter3d",
559
+ mode: "lines",
560
+ name: "Contours",
561
+ x: data.contourLines.x,
562
+ y: data.contourLines.y,
563
+ z: data.contourLines.z,
564
+ line: { color: "rgba(25,45,58,0.65)", width: 3 },
565
+ visible: toggleContours.checked,
566
+ hoverinfo: "skip",
567
+ },
568
+ {
569
+ type: "scatter3d",
570
+ mode: "lines+markers",
571
+ name: "Gradient descent trajectory",
572
+ x: trajectoryPath.map((p) => p.t0),
573
+ y: trajectoryPath.map((p) => p.t1),
574
+ z: trajectoryPath.map((p) => p.z),
575
+ customdata: trajectoryPath.map((p) => [p.step]),
576
+ line: { color: "#f97316", width: 7 },
577
+ marker: { size: 3.8, color: "#b42318", opacity: 0.95 },
578
+ visible: toggleTrajectory.checked,
579
+ hovertemplate: baseHover,
580
+ },
581
+ {
582
+ type: "scatter3d",
583
+ mode: "markers+text",
584
+ name: "Start",
585
+ x: [start.t0],
586
+ y: [start.t1],
587
+ z: [start.z],
588
+ customdata: [[start.step]],
589
+ marker: { size: 10, color: "#0c7b73", line: { color: "#0b2230", width: 1.2 } },
590
+ text: ["Start"],
591
+ textposition: "top center",
592
+ visible: toggleTrajectory.checked,
593
+ hovertemplate: baseHover,
594
+ },
595
+ {
596
+ type: "scatter3d",
597
+ mode: "markers+text",
598
+ name: "Final",
599
+ x: [final.t0],
600
+ y: [final.t1],
601
+ z: [final.z],
602
+ customdata: [[final.step]],
603
+ marker: { size: 10, color: "#dd5e2f", line: { color: "#0b2230", width: 1.2 } },
604
+ text: ["Final"],
605
+ textposition: "top center",
606
+ visible: toggleTrajectory.checked,
607
+ hovertemplate: baseHover,
608
+ },
609
+ {
610
+ type: "scatter3d",
611
+ mode: "markers+text",
612
+ name: "Current",
613
+ x: [current.t0],
614
+ y: [current.t1],
615
+ z: [current.z],
616
+ customdata: [[current.step]],
617
+ marker: { size: 9, color: "#111827", line: { color: "#f59e0b", width: 2 } },
618
+ text: [state.currentStep === data.path.length - 1 ? "Final" : `Step ${state.currentStep}`],
619
+ textposition: "bottom center",
620
+ visible: toggleTrajectory.checked,
621
+ hovertemplate: baseHover,
622
+ },
623
+ ];
624
 
625
+ const sceneCamera = state.landscapeCamera || cloneCamera(DEFAULT_CAMERA);
626
+
627
+ const layout = {
628
+ autosize: true,
629
+ margin: { l: 0, r: 0, t: 8, b: 0 },
630
+ paper_bgcolor: "#ffffff",
631
+ scene: {
632
+ xaxis: {
633
+ title: "theta0",
634
+ range: [data.t0Min, data.t0Max],
635
+ showspikes: false,
636
+ backgroundcolor: "rgba(247, 248, 252, 0.85)",
637
+ },
638
+ yaxis: {
639
+ title: "theta1",
640
+ range: [data.t1Min, data.t1Max],
641
+ showspikes: false,
642
+ backgroundcolor: "rgba(247, 248, 252, 0.85)",
643
+ },
644
+ zaxis: {
645
+ title: "Cost J(theta)",
646
+ range: [data.zMin, data.zMax],
647
+ showspikes: false,
648
+ backgroundcolor: "rgba(249, 250, 252, 0.9)",
649
+ },
650
+ aspectmode: "manual",
651
+ aspectratio: { x: 1.12, y: 1.12, z: 0.78 },
652
+ camera: sceneCamera,
653
+ dragmode: "turntable",
654
+ },
655
+ showlegend: false,
656
+ uirevision: "landscape-ui",
657
  };
658
 
659
+ const config = {
660
+ responsive: true,
661
+ displaylogo: false,
662
+ scrollZoom: true,
663
+ doubleClick: "reset",
664
+ modeBarButtonsToRemove: ["lasso2d", "select2d"],
 
665
  };
666
 
667
+ const plotPromise = state.landscapePlotReady
668
+ ? Plotly.react(landscapePlot, traces, layout, config)
669
+ : Plotly.newPlot(landscapePlot, traces, layout, config);
670
+
671
+ plotPromise.then(() => {
672
+ if (!state.landscapePlotReady) {
673
+ state.landscapePlotReady = true;
674
+ landscapePlot.on("plotly_relayout", (ev) => {
675
+ if (ev && ev["scene.camera"]) {
676
+ state.landscapeCamera = cloneCamera(ev["scene.camera"]);
677
+ }
678
+ });
 
 
 
 
679
  }
 
 
680
 
681
+ resizeLandscapePlot();
682
+ });
683
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
684
 
685
+ function cloneCamera(camera) {
686
  return {
687
+ eye: { ...camera.eye },
688
+ center: { ...camera.center },
689
+ up: { ...camera.up },
 
690
  };
691
  }
692
 
 
 
 
 
 
 
 
 
693
  function renderCostSummary() {
694
  const first = state.steps[0];
695
  const last = state.steps[state.steps.length - 1];
 
697
  costSummary.innerHTML = `
698
  <article class="matrix-card">
699
  <p class="matrix-title">Cost Function</p>
700
+ <p class="formula-line">J(theta) = (1/2m) sum((theta0 + theta1*x1 - y)^2)</p>
701
  </article>
702
  <article class="matrix-card">
703
  <p class="matrix-title">Result</p>
 
765
  ctx.fillText(yLabel, pad - 30, pad - 10);
766
  }
767
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
768
  function startPlayback() {
769
  if (state.timer || !state.steps.length) return;
770
  playGdBtn.textContent = "Pause";
linear-regression/styles.css CHANGED
@@ -368,14 +368,89 @@ input[type="checkbox"] {
368
  }
369
 
370
  #fitCanvas,
371
- #costCanvas,
372
- #landscapeCanvas {
373
  width: 100%;
374
  border: 1px solid var(--line);
375
  border-radius: 12px;
376
  background: #fff;
377
  }
378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  .log-wrap {
380
  overflow: auto;
381
  border: 1px solid var(--line);
@@ -434,4 +509,10 @@ input[type="checkbox"] {
434
  .gd-slider-row {
435
  grid-template-columns: 1fr;
436
  }
 
 
 
 
 
 
437
  }
 
368
  }
369
 
370
  #fitCanvas,
371
+ #costCanvas {
 
372
  width: 100%;
373
  border: 1px solid var(--line);
374
  border-radius: 12px;
375
  background: #fff;
376
  }
377
 
378
+ .landscape-summary {
379
+ display: grid;
380
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
381
+ gap: 10px;
382
+ margin: 6px 0 12px;
383
+ }
384
+
385
+ .landscape-pill {
386
+ border: 1px solid var(--line);
387
+ border-radius: 10px;
388
+ padding: 8px 10px;
389
+ background: #fff;
390
+ font-size: 0.9rem;
391
+ color: #24323a;
392
+ }
393
+
394
+ .landscape-controls {
395
+ display: flex;
396
+ flex-wrap: wrap;
397
+ gap: 8px;
398
+ margin-bottom: 10px;
399
+ }
400
+
401
+ .viz-btn {
402
+ padding: 8px 12px;
403
+ border-radius: 9px;
404
+ border: 1px solid #c7c0b2;
405
+ background: #fff;
406
+ color: #24323a;
407
+ }
408
+
409
+ .viz-btn:hover {
410
+ border-color: var(--accent-2);
411
+ }
412
+
413
+ .landscape-controls.toggles {
414
+ margin-bottom: 12px;
415
+ }
416
+
417
+ .viz-toggle {
418
+ display: inline-flex;
419
+ align-items: center;
420
+ gap: 8px;
421
+ min-width: 0;
422
+ font-weight: 600;
423
+ border: 1px solid var(--line);
424
+ border-radius: 999px;
425
+ padding: 6px 10px;
426
+ background: #fff;
427
+ }
428
+
429
+ .viz-toggle input {
430
+ margin: 0;
431
+ }
432
+
433
+ .landscape-plot-wrap {
434
+ width: min(100%, 980px);
435
+ height: 620px;
436
+ min-width: 320px;
437
+ min-height: 320px;
438
+ max-width: 1400px;
439
+ max-height: 900px;
440
+ resize: both;
441
+ overflow: auto;
442
+ border: 1px solid var(--line);
443
+ border-radius: 12px;
444
+ background: #fff;
445
+ position: relative;
446
+ }
447
+
448
+ .landscape-plot {
449
+ width: 100%;
450
+ height: 100%;
451
+ min-height: 100%;
452
+ }
453
+
454
  .log-wrap {
455
  overflow: auto;
456
  border: 1px solid var(--line);
 
509
  .gd-slider-row {
510
  grid-template-columns: 1fr;
511
  }
512
+
513
+ .landscape-plot-wrap {
514
+ width: 100%;
515
+ height: 420px;
516
+ min-height: 280px;
517
+ }
518
  }
logistic-regression/cost-visualization.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>DDW Interface - Cost Function Visualization</title>
7
  <link rel="stylesheet" href="styles.css" />
8
  </head>
9
  <body>
@@ -34,10 +34,11 @@
34
 
35
  <p class="hint" id="formulaText"></p>
36
 
37
- <label for="lr">Learning rate: <span id="lrValue">0.20</span></label>
38
- <input id="lr" type="range" min="0.001" max="1.000" step="0.001" value="0.200" />
39
  <label for="lrInput">Learning rate input</label>
40
- <input id="lrInput" type="number" min="0.001" max="1.000" step="0.001" value="0.200" />
 
41
 
42
  <label for="iters">Iterations: <span id="iterValue">80</span></label>
43
  <input id="iters" type="range" min="5" max="220" step="1" value="80" />
@@ -68,12 +69,67 @@
68
  </section>
69
 
70
  <section class="panel chart-panel">
71
- <h2>3D Cost Surface and Gradient Descent Trajectory</h2>
72
- <canvas id="costCanvas" width="900" height="520" aria-label="3D cost function and gradient descent"></canvas>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  <div class="stats" id="costStats"></div>
74
  </section>
75
  </main>
76
 
77
- <script src="cost.js"></script>
 
78
  </body>
79
  </html>
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>DDW Interface - Logistic Cost Landscape</title>
7
  <link rel="stylesheet" href="styles.css" />
8
  </head>
9
  <body>
 
34
 
35
  <p class="hint" id="formulaText"></p>
36
 
37
+ <label for="lr">Learning rate: <span id="lrValue">0.100</span></label>
38
+ <input id="lr" type="range" min="0.001" max="10.000" step="0.001" value="0.100" />
39
  <label for="lrInput">Learning rate input</label>
40
+ <input id="lrInput" type="number" min="0.001" max="10.000" step="0.001" value="0.100" />
41
+ <p class="hint">If you use z-normalization, you can generally use a larger learning rate than with unscaled features.</p>
42
 
43
  <label for="iters">Iterations: <span id="iterValue">80</span></label>
44
  <input id="iters" type="range" min="5" max="220" step="1" value="80" />
 
69
  </section>
70
 
71
  <section class="panel chart-panel">
72
+ <h2>Cost Landscape (Gradient Descent)</h2>
73
+ <div id="modelDefinition" class="model-definition"></div>
74
+
75
+ <p class="hint">
76
+ The height represents Binary Cross-Entropy (BCE) cost J(theta). Lower points correspond to parameter values that better fit class labels.
77
+ </p>
78
+ <p class="hint">
79
+ Each trajectory point is one gradient-descent update: horizontal movement = parameter changes, vertical movement = BCE change.
80
+ </p>
81
+
82
+ <div id="landscapeSummary" class="landscape-summary"></div>
83
+
84
+ <div class="landscape-controls">
85
+ <label for="landscapeView">Landscape View</label>
86
+ <select id="landscapeView">
87
+ <option value="surface3d">3D Surface + Trajectory</option>
88
+ <option value="contour2d">2D Contour + Trajectory</option>
89
+ <option value="surface3dContours">3D Surface + Contours</option>
90
+ </select>
91
+ <button id="resetViewBtn" type="button" class="viz-btn">Reset View</button>
92
+ <button id="plotSmallerBtn" type="button" class="viz-btn">-</button>
93
+ <button id="plotFitBtn" type="button" class="viz-btn">Fit</button>
94
+ <button id="plotLargerBtn" type="button" class="viz-btn">+</button>
95
+ <button id="plotFullscreenBtn" type="button" class="viz-btn">Fullscreen</button>
96
+ </div>
97
+
98
+ <div class="landscape-controls toggles">
99
+ <label class="viz-toggle"><input id="toggleSurface" type="checkbox" checked /> Surface</label>
100
+ <label class="viz-toggle"><input id="toggleContours" type="checkbox" checked /> Contours</label>
101
+ <label class="viz-toggle"><input id="toggleTrajectory" type="checkbox" checked /> Trajectory</label>
102
+ <label class="viz-toggle"><input id="toggleCurrent" type="checkbox" checked /> Current Point</label>
103
+ </div>
104
+
105
+ <div class="viz-info-row">
106
+ <div class="viz-chip" id="clipInfo">Surface clipping: none</div>
107
+ <div class="viz-chip" id="minimumInfo">Lowest point in shown region: --</div>
108
+ <div class="viz-chip warning" id="stabilityWarning">Stability: normal</div>
109
+ </div>
110
+
111
+ <div id="landscapePlotWrap" class="landscape-plot-wrap">
112
+ <div id="landscapePlot" class="landscape-plot"></div>
113
+ </div>
114
+
115
+ <p class="slice-label">Drag to rotate · Scroll to zoom · Hover to inspect · Resize from the corner</p>
116
+
117
+ <div class="iteration-block">
118
+ <label for="landscapeStep">Gradient Descent Step</label>
119
+ <input id="landscapeStep" type="range" min="0" max="0" value="0" />
120
+ <span id="landscapeStepLabel">0 / 0</span>
121
+ </div>
122
+
123
+ <div class="classifier-panel">
124
+ <h3>Classifier at This Step</h3>
125
+ <canvas id="classifierCanvas" width="940" height="310" aria-label="Current classifier view"></canvas>
126
+ </div>
127
+
128
  <div class="stats" id="costStats"></div>
129
  </section>
130
  </main>
131
 
132
+ <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
133
+ <script src="cost.js?v=20260813b"></script>
134
  </body>
135
  </html>
logistic-regression/cost.js CHANGED
@@ -1,5 +1,7 @@
1
- const canvas = document.getElementById("costCanvas");
2
- const ctx = canvas.getContext("2d");
 
 
3
 
4
  const controls = {
5
  caseSelect: document.getElementById("caseSelect"),
@@ -15,6 +17,17 @@ const controls = {
15
  restartBtn: document.getElementById("restartBtn"),
16
  regenBtn: document.getElementById("regenBtn"),
17
  clearBtn: document.getElementById("clearBtn"),
 
 
 
 
 
 
 
 
 
 
 
18
  };
19
 
20
  const labels = {
@@ -24,44 +37,87 @@ const labels = {
24
  p1: document.getElementById("p1Value"),
25
  p2: document.getElementById("p2Value"),
26
  speed: document.getElementById("speedValue"),
 
27
  };
28
 
 
 
29
  const stats = document.getElementById("costStats");
 
 
 
 
 
 
 
 
 
30
 
31
- const P_MIN = -6;
32
- const P_MAX = 6;
33
- const GRID_N = 26;
34
 
35
  const caseMeta = {
36
  case1: {
37
  p1Name: "b0",
38
  p2Name: "b1",
39
- logistic: "Case 1 logistic: p(y=1|x) = 1 / (1 + exp(-(b0 + b1x)))",
 
 
 
 
 
 
 
40
  },
41
  case2: {
42
  p1Name: "w1",
43
  p2Name: "w2",
44
- logistic: "Case 2 logistic: p(y=1|x1,x2) = 1 / (1 + exp(-(w1x1 + w2x2))) (no intercept)",
 
 
 
 
 
 
 
 
45
  },
46
  };
47
 
48
- let datasets = { case1: [], case2: [] };
49
- let gridCache = { case1: null, case2: null };
 
 
 
 
 
 
 
 
 
 
50
 
51
- let trajectory = [];
52
- let stepIndex = 0;
53
- let animationTimer = null;
54
 
55
- function sigmoid(z) {
56
- return 1 / (1 + Math.exp(-z));
57
  }
58
 
59
- function clampProb(p) {
60
- return Math.min(1 - 1e-9, Math.max(1e-9, p));
 
 
 
 
 
61
  }
62
 
63
- function randRange(min, max) {
64
- return min + Math.random() * (max - min);
 
 
65
  }
66
 
67
  function randNormal(mean = 0, std = 1) {
@@ -71,51 +127,76 @@ function randNormal(mean = 0, std = 1) {
71
  return mean + z0 * std;
72
  }
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  function generateSyntheticData() {
75
  const case1 = [];
76
  const case2 = [];
77
 
78
- const trueB0 = -0.2;
79
- const trueB1 = 2.35;
80
  for (let i = 0; i < 160; i += 1) {
81
- const center = i < 80 ? -3.0 : 3.0;
82
- const x = randNormal(center, 1.3);
83
  const p = sigmoid(trueB0 + trueB1 * x);
84
  const y = Math.random() < p ? 1 : 0;
85
  case1.push({ x, y });
86
  }
87
 
88
- const trueW1 = 2.15;
89
- const trueW2 = 1.85;
90
- for (let i = 0; i < 190; i += 1) {
91
- const center1 = i < 95 ? -2.4 : 2.4;
92
- const center2 = i < 95 ? -1.9 : 1.9;
93
- const x1 = randNormal(center1, 1.25);
94
- const x2 = randNormal(center2 + 0.45 * (x1 - center1), 1.1);
95
  const p = sigmoid(trueW1 * x1 + trueW2 * x2);
96
  const y = Math.random() < p ? 1 : 0;
97
  case2.push({ x1, x2, y });
98
  }
99
 
100
- datasets = { case1, case2 };
101
- gridCache = { case1: null, case2: null };
 
102
  }
103
 
104
  function costFor(caseKey, params) {
105
- const data = datasets[caseKey];
106
  let total = 0;
107
 
108
  if (caseKey === "case1") {
109
  const [b0, b1] = params;
110
  for (const row of data) {
111
- const p = clampProb(sigmoid(b0 + b1 * row.x));
112
- total += -(row.y * Math.log(p) + (1 - row.y) * Math.log(1 - p));
113
  }
114
  } else {
115
  const [w1, w2] = params;
116
  for (const row of data) {
117
- const p = clampProb(sigmoid(w1 * row.x1 + w2 * row.x2));
118
- total += -(row.y * Math.log(p) + (1 - row.y) * Math.log(1 - p));
119
  }
120
  }
121
 
@@ -123,7 +204,7 @@ function costFor(caseKey, params) {
123
  }
124
 
125
  function gradFor(caseKey, params) {
126
- const data = datasets[caseKey];
127
  let g1 = 0;
128
  let g2 = 0;
129
 
@@ -149,321 +230,836 @@ function gradFor(caseKey, params) {
149
  return [g1 / m, g2 / m];
150
  }
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  function buildTrajectory(caseKey, start, lr, iters) {
153
  const path = [];
154
  let params = [start[0], start[1]];
 
155
 
156
- path.push({ p1: params[0], p2: params[1], cost: costFor(caseKey, params), gradNorm: 0 });
157
 
158
  for (let i = 0; i < iters; i += 1) {
159
  const grad = gradFor(caseKey, params);
160
- const gradNorm = Math.hypot(grad[0], grad[1]);
161
- params = [params[0] - lr * grad[0], params[1] - lr * grad[1]];
162
- path.push({ p1: params[0], p2: params[1], cost: costFor(caseKey, params), gradNorm });
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  }
164
 
165
- return path;
166
  }
167
 
168
- function getGrid(caseKey) {
169
- if (gridCache[caseKey]) {
170
- return gridCache[caseKey];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  }
172
 
173
  const vals = [];
 
174
  let min = Infinity;
175
  let max = -Infinity;
 
 
176
 
177
  for (let j = 0; j < GRID_N; j += 1) {
178
  const row = [];
179
- const p2 = P_MIN + (j / (GRID_N - 1)) * (P_MAX - P_MIN);
 
180
  for (let i = 0; i < GRID_N; i += 1) {
181
- const p1 = P_MIN + (i / (GRID_N - 1)) * (P_MAX - P_MIN);
182
  const c = costFor(caseKey, [p1, p2]);
183
  row.push(c);
184
- if (c < min) min = c;
 
 
 
 
 
 
185
  if (c > max) max = c;
186
  }
 
187
  vals.push(row);
188
  }
189
 
190
- gridCache[caseKey] = { vals, min, max };
191
- return gridCache[caseKey];
192
- }
193
-
194
- function colorScale(t) {
195
- const clamped = Math.max(0, Math.min(1, t));
196
- const r = Math.round(35 + 220 * clamped);
197
- const g = Math.round(90 + 160 * (1 - Math.abs(clamped - 0.5) * 2));
198
- const b = Math.round(240 - 220 * clamped);
199
- return `rgb(${r},${g},${b})`;
200
- }
201
-
202
- function project3D(x, y, z) {
203
- const yaw = -0.75;
204
- const pitch = 0.75;
205
-
206
- const cy = Math.cos(yaw);
207
- const sy = Math.sin(yaw);
208
- const cp = Math.cos(pitch);
209
- const sp = Math.sin(pitch);
210
-
211
- const xr = x * cy - y * sy;
212
- const yr = x * sy + y * cy;
213
-
214
- const y2 = yr * cp - z * sp;
215
- const z2 = yr * sp + z * cp;
216
-
217
- const perspective = 1 / (1 + z2 * 0.09);
218
- const scale = 56;
219
 
220
  return {
221
- sx: canvas.width * 0.49 + xr * scale * perspective,
222
- sy: canvas.height * 0.62 - y2 * scale * perspective,
223
- depth: z2,
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  };
225
  }
226
 
227
- function mapParam(v) {
228
- return ((v - P_MIN) / (P_MAX - P_MIN)) * 8 - 4;
 
 
 
 
 
 
 
229
  }
230
 
231
- function mapCost(cost, minCost, maxCost) {
232
- const t = (cost - minCost) / Math.max(1e-9, maxCost - minCost);
233
- return t * 5.0;
234
- }
 
235
 
236
- function drawSurface(caseKey) {
237
- const grid = getGrid(caseKey);
238
- const { vals, min, max } = grid;
239
-
240
- ctx.clearRect(0, 0, canvas.width, canvas.height);
241
- ctx.fillStyle = "#ffffff";
242
- ctx.fillRect(0, 0, canvas.width, canvas.height);
243
-
244
- for (let j = 0; j < GRID_N - 1; j += 1) {
245
- for (let i = 0; i < GRID_N - 1; i += 1) {
246
- const p1a = P_MIN + (i / (GRID_N - 1)) * (P_MAX - P_MIN);
247
- const p2a = P_MIN + (j / (GRID_N - 1)) * (P_MAX - P_MIN);
248
- const p1b = P_MIN + ((i + 1) / (GRID_N - 1)) * (P_MAX - P_MIN);
249
- const p2b = P_MIN + ((j + 1) / (GRID_N - 1)) * (P_MAX - P_MIN);
250
-
251
- const z00 = mapCost(vals[j][i], min, max);
252
- const z10 = mapCost(vals[j][i + 1], min, max);
253
- const z11 = mapCost(vals[j + 1][i + 1], min, max);
254
- const z01 = mapCost(vals[j + 1][i], min, max);
255
-
256
- const q00 = project3D(mapParam(p1a), mapParam(p2a), z00);
257
- const q10 = project3D(mapParam(p1b), mapParam(p2a), z10);
258
- const q11 = project3D(mapParam(p1b), mapParam(p2b), z11);
259
- const q01 = project3D(mapParam(p1a), mapParam(p2b), z01);
260
-
261
- const t = ((vals[j][i] + vals[j][i + 1] + vals[j + 1][i + 1] + vals[j + 1][i]) / 4 - min) / Math.max(1e-9, max - min);
262
- ctx.fillStyle = colorScale(t);
263
- ctx.globalAlpha = 0.48;
264
- ctx.beginPath();
265
- ctx.moveTo(q00.sx, q00.sy);
266
- ctx.lineTo(q10.sx, q10.sy);
267
- ctx.lineTo(q11.sx, q11.sy);
268
- ctx.lineTo(q01.sx, q01.sy);
269
- ctx.closePath();
270
- ctx.fill();
271
- }
272
- }
273
- ctx.globalAlpha = 1;
274
 
275
- for (let j = 0; j < GRID_N; j += 1) {
276
- ctx.beginPath();
277
- for (let i = 0; i < GRID_N; i += 1) {
278
- const p1 = P_MIN + (i / (GRID_N - 1)) * (P_MAX - P_MIN);
279
- const p2 = P_MIN + (j / (GRID_N - 1)) * (P_MAX - P_MIN);
280
- const x = mapParam(p1);
281
- const y = mapParam(p2);
282
- const z = mapCost(vals[j][i], min, max);
283
- const pr = project3D(x, y, z);
284
- if (i === 0) ctx.moveTo(pr.sx, pr.sy);
285
- else ctx.lineTo(pr.sx, pr.sy);
286
- }
287
- ctx.strokeStyle = "rgba(13, 66, 83, 0.34)";
288
- ctx.lineWidth = 1;
289
- ctx.stroke();
290
- }
291
 
292
- for (let i = 0; i < GRID_N; i += 1) {
293
- ctx.beginPath();
294
- for (let j = 0; j < GRID_N; j += 1) {
295
- const p1 = P_MIN + (i / (GRID_N - 1)) * (P_MAX - P_MIN);
296
- const p2 = P_MIN + (j / (GRID_N - 1)) * (P_MAX - P_MIN);
297
- const x = mapParam(p1);
298
- const y = mapParam(p2);
299
- const z = mapCost(vals[j][i], min, max);
300
- const pr = project3D(x, y, z);
301
- if (j === 0) ctx.moveTo(pr.sx, pr.sy);
302
- else ctx.lineTo(pr.sx, pr.sy);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  }
304
- ctx.strokeStyle = "rgba(13, 66, 83, 0.26)";
305
- ctx.lineWidth = 1;
306
- ctx.stroke();
307
  }
308
 
309
- drawAxes3D(caseKey, min, max);
310
- drawHeatLegend(min, max);
311
  }
312
 
313
- function drawAxes3D(caseKey, minCost, maxCost) {
314
- const meta = caseMeta[caseKey];
 
 
 
 
 
 
315
 
316
- const o = project3D(-4.3, -4.3, 0);
317
- const xA = project3D(4.4, -4.3, 0);
318
- const yA = project3D(-4.3, 4.4, 0);
319
- const zA = project3D(-4.3, -4.3, 3.4);
320
 
321
- ctx.strokeStyle = "#13323d";
322
- ctx.lineWidth = 1.4;
 
 
323
 
324
- ctx.beginPath();
325
- ctx.moveTo(o.sx, o.sy);
326
- ctx.lineTo(xA.sx, xA.sy);
327
- ctx.stroke();
328
 
329
- ctx.beginPath();
330
- ctx.moveTo(o.sx, o.sy);
331
- ctx.lineTo(yA.sx, yA.sy);
332
- ctx.stroke();
333
 
334
- ctx.beginPath();
335
- ctx.moveTo(o.sx, o.sy);
336
- ctx.lineTo(zA.sx, zA.sy);
337
- ctx.stroke();
338
 
339
- ctx.fillStyle = "#13323d";
340
- ctx.font = "12px 'Avenir Next', sans-serif";
341
- ctx.fillText(meta.p1Name, xA.sx + 6, xA.sy + 2);
342
- ctx.fillText(meta.p2Name, yA.sx + 6, yA.sy + 2);
343
- ctx.fillText("J", zA.sx + 6, zA.sy + 2);
 
344
 
345
- ctx.fillText(`J min=${minCost.toFixed(3)}`, 18, 24);
346
- ctx.fillText(`J max=${maxCost.toFixed(3)}`, 18, 42);
 
347
  }
348
 
349
- function drawHeatLegend(minCost, maxCost) {
350
- const x = canvas.width - 54;
351
- const y = 80;
352
- const w = 18;
353
- const h = 180;
354
 
355
- for (let i = 0; i < h; i += 1) {
356
- const t = 1 - i / h;
357
- ctx.strokeStyle = colorScale(t);
358
- ctx.beginPath();
359
- ctx.moveTo(x, y + i);
360
- ctx.lineTo(x + w, y + i);
361
- ctx.stroke();
362
  }
 
 
363
 
364
- ctx.strokeStyle = "#14343f";
365
- ctx.strokeRect(x, y, w, h);
 
 
 
 
366
 
367
- ctx.fillStyle = "#14343f";
368
- ctx.font = "11px 'Avenir Next', sans-serif";
369
- ctx.fillText("Cost", x - 4, y - 8);
370
- ctx.fillText(maxCost.toFixed(2), x - 8, y - 6);
371
- ctx.fillText(minCost.toFixed(2), x - 6, y + h + 16);
 
 
 
372
  }
373
 
374
- function drawTrajectory3D(caseKey) {
375
- if (trajectory.length === 0) return;
376
 
377
- const grid = getGrid(caseKey);
378
- const current = Math.min(stepIndex, trajectory.length - 1);
379
-
380
- ctx.strokeStyle = "#d8534f";
381
- ctx.lineWidth = 2.6;
382
- ctx.beginPath();
383
-
384
- for (let i = 0; i <= current; i += 1) {
385
- const node = trajectory[i];
386
- const p = project3D(
387
- mapParam(node.p1),
388
- mapParam(node.p2),
389
- mapCost(node.cost, grid.min, grid.max)
390
- );
391
- if (i === 0) ctx.moveTo(p.sx, p.sy);
392
- else ctx.lineTo(p.sx, p.sy);
393
- }
394
- ctx.stroke();
395
-
396
- for (let i = 0; i <= current; i += 1) {
397
- const node = trajectory[i];
398
- const p = project3D(
399
- mapParam(node.p1),
400
- mapParam(node.p2),
401
- mapCost(node.cost, grid.min, grid.max)
402
- );
403
- const isNow = i === current;
404
- ctx.fillStyle = isNow ? "#102a32" : "#d8534f";
405
- ctx.beginPath();
406
- ctx.arc(p.sx, p.sy, isNow ? 5.4 : 3.2, 0, Math.PI * 2);
407
- ctx.fill();
408
  }
409
 
410
- const node = trajectory[current];
411
- const p = project3D(mapParam(node.p1), mapParam(node.p2), mapCost(node.cost, grid.min, grid.max));
412
- ctx.fillStyle = "#102a32";
413
- ctx.font = "12px 'Avenir Next', sans-serif";
414
- ctx.fillText(`step ${current}`, p.sx + 8, p.sy - 8);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  }
416
 
417
  function renderStats(caseKey) {
418
  const meta = caseMeta[caseKey];
419
- const dataCount = datasets[caseKey].length;
420
 
421
- if (trajectory.length === 0) {
422
  stats.innerHTML = `
423
  <span>Model case: ${meta.p1Name}, ${meta.p2Name}</span>
424
- <span>Synthetic samples: ${dataCount}</span>
425
  <span>No trajectory yet. Click Run Gradient Descent.</span>
426
- <span>Use Next Step or Automatic Run to show the path.</span>
 
 
427
  `;
428
  return;
429
  }
430
 
431
- const idx = Math.min(stepIndex, trajectory.length - 1);
432
- const curr = trajectory[idx];
433
- const start = trajectory[0];
434
- const last = trajectory[trajectory.length - 1];
435
 
436
  stats.innerHTML = `
437
  <span>Model case: ${meta.p1Name}, ${meta.p2Name}</span>
438
- <span>Synthetic samples: ${dataCount}</span>
439
- <span>Current step: ${idx} / ${trajectory.length - 1}</span>
440
- <span>Current params: ${meta.p1Name}=${curr.p1.toFixed(3)}, ${meta.p2Name}=${curr.p2.toFixed(3)}</span>
441
- <span>Current cost J: ${curr.cost.toFixed(5)}</span>
442
- <span>Start J: ${start.cost.toFixed(5)} -> Final J: ${last.cost.toFixed(5)}</span>
443
  `;
444
  }
445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  function render() {
447
  const caseKey = controls.caseSelect.value;
 
448
 
449
- labels.formula.textContent = caseMeta[caseKey].logistic;
450
  labels.lr.textContent = Number(controls.lr.value).toFixed(3);
451
  labels.iters.textContent = controls.iters.value;
452
  labels.p1.textContent = Number(controls.p1.value).toFixed(2);
453
  labels.p2.textContent = Number(controls.p2.value).toFixed(2);
454
  labels.speed.textContent = controls.speed.value;
455
 
456
- drawSurface(caseKey);
457
- drawTrajectory3D(caseKey);
 
 
 
 
 
458
  renderStats(caseKey);
 
459
  }
460
 
461
  function stopAnimation() {
462
- if (animationTimer) {
463
- clearInterval(animationTimer);
464
- animationTimer = null;
465
  controls.autoBtn.textContent = "Automatic Run";
 
466
  }
 
 
 
 
467
  }
468
 
469
  function runTrajectory() {
@@ -474,70 +1070,63 @@ function runTrajectory() {
474
  const lr = Number(controls.lr.value);
475
  const iters = Number(controls.iters.value);
476
 
477
- trajectory = buildTrajectory(caseKey, start, lr, iters);
478
- stepIndex = 0;
 
 
 
 
 
 
479
  render();
480
  }
481
 
482
  function nextStep() {
483
- if (trajectory.length === 0) {
484
  runTrajectory();
485
  return;
486
  }
487
- if (stepIndex < trajectory.length - 1) {
488
- stepIndex += 1;
 
489
  render();
490
  }
491
  }
492
 
493
- function automaticRun() {
494
- if (animationTimer) {
495
  stopAnimation();
496
  return;
497
  }
498
 
499
- if (trajectory.length === 0) {
500
- runTrajectory();
501
- }
502
 
503
  controls.autoBtn.textContent = "Stop Auto";
504
-
505
- animationTimer = setInterval(() => {
506
- if (stepIndex >= trajectory.length - 1) {
507
  stopAnimation();
508
  return;
509
  }
510
- stepIndex += 1;
 
511
  render();
512
  }, Number(controls.speed.value));
513
  }
514
 
515
  function restartPath() {
516
  stopAnimation();
517
- if (trajectory.length > 0) {
518
- stepIndex = 0;
519
  }
520
  render();
521
  }
522
 
523
  function clearPath() {
524
  stopAnimation();
525
- trajectory = [];
526
- stepIndex = 0;
527
- render();
528
- }
529
-
530
- function syncLrFromSlider() {
531
- controls.lrInput.value = Number(controls.lr.value).toFixed(3);
532
- render();
533
- }
534
-
535
- function syncLrFromInput() {
536
- let v = Number(controls.lrInput.value);
537
- if (!Number.isFinite(v)) v = 0.2;
538
- v = Math.max(0.001, Math.min(1.0, v));
539
- controls.lr.value = v;
540
- controls.lrInput.value = v.toFixed(3);
541
  render();
542
  }
543
 
@@ -545,37 +1134,120 @@ function applyCaseDefaults(caseKey) {
545
  if (caseKey === "case1") {
546
  controls.p1.value = -4.0;
547
  controls.p2.value = 4.0;
548
- controls.lr.value = 0.20;
549
  } else {
550
  controls.p1.value = -2.2;
551
  controls.p2.value = -2.0;
552
- controls.lr.value = 0.14;
553
  }
 
554
  controls.lrInput.value = Number(controls.lr.value).toFixed(3);
555
  controls.iters.value = 80;
556
  controls.speed.value = 90;
 
557
  clearPath();
558
  }
559
 
560
- for (const el of [controls.iters, controls.p1, controls.p2, controls.speed]) {
561
- el.addEventListener("input", render);
 
 
 
 
 
 
 
 
 
 
 
562
  }
563
 
564
- controls.lr.addEventListener("input", syncLrFromSlider);
565
- controls.lrInput.addEventListener("change", syncLrFromInput);
566
- controls.caseSelect.addEventListener("change", () => {
567
- applyCaseDefaults(controls.caseSelect.value);
568
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
- controls.runBtn.addEventListener("click", runTrajectory);
571
- controls.stepBtn.addEventListener("click", nextStep);
572
- controls.autoBtn.addEventListener("click", automaticRun);
573
- controls.restartBtn.addEventListener("click", restartPath);
574
- controls.regenBtn.addEventListener("click", () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
  generateSyntheticData();
576
- clearPath();
577
- });
578
- controls.clearBtn.addEventListener("click", clearPath);
 
 
 
579
 
580
- generateSyntheticData();
581
- applyCaseDefaults("case1");
 
1
+ const plotWrap = document.getElementById("landscapePlotWrap");
2
+ const plotEl = document.getElementById("landscapePlot");
3
+ const classifierCanvas = document.getElementById("classifierCanvas");
4
+ const classifierCtx = classifierCanvas.getContext("2d");
5
 
6
  const controls = {
7
  caseSelect: document.getElementById("caseSelect"),
 
17
  restartBtn: document.getElementById("restartBtn"),
18
  regenBtn: document.getElementById("regenBtn"),
19
  clearBtn: document.getElementById("clearBtn"),
20
+ landscapeView: document.getElementById("landscapeView"),
21
+ resetViewBtn: document.getElementById("resetViewBtn"),
22
+ plotSmallerBtn: document.getElementById("plotSmallerBtn"),
23
+ plotFitBtn: document.getElementById("plotFitBtn"),
24
+ plotLargerBtn: document.getElementById("plotLargerBtn"),
25
+ plotFullscreenBtn: document.getElementById("plotFullscreenBtn"),
26
+ toggleSurface: document.getElementById("toggleSurface"),
27
+ toggleContours: document.getElementById("toggleContours"),
28
+ toggleTrajectory: document.getElementById("toggleTrajectory"),
29
+ toggleCurrent: document.getElementById("toggleCurrent"),
30
+ stepSlider: document.getElementById("landscapeStep"),
31
  };
32
 
33
  const labels = {
 
37
  p1: document.getElementById("p1Value"),
38
  p2: document.getElementById("p2Value"),
39
  speed: document.getElementById("speedValue"),
40
+ stepLabel: document.getElementById("landscapeStepLabel"),
41
  };
42
 
43
+ const modelDefinition = document.getElementById("modelDefinition");
44
+ const landscapeSummary = document.getElementById("landscapeSummary");
45
  const stats = document.getElementById("costStats");
46
+ const clipInfo = document.getElementById("clipInfo");
47
+ const minimumInfo = document.getElementById("minimumInfo");
48
+ const stabilityWarning = document.getElementById("stabilityWarning");
49
+
50
+ const DEFAULT_CAMERA = {
51
+ eye: { x: 1.5, y: 1.5, z: 1.1 },
52
+ center: { x: 0, y: 0, z: 0 },
53
+ up: { x: 0, y: 0, z: 1 },
54
+ };
55
 
56
+ const GRID_N = 60;
57
+ const MIN_SPAN = 2.0;
 
58
 
59
  const caseMeta = {
60
  case1: {
61
  p1Name: "b0",
62
  p2Name: "b1",
63
+ p1Label: "b0 (intercept)",
64
+ p2Label: "b1 (weight for x)",
65
+ model: "z = b0 + b1*x",
66
+ explain: [
67
+ "Cost landscape: J(b0, b1)",
68
+ "b0 = intercept, b1 = coefficient of x",
69
+ ],
70
+ formula: "Case 1 logistic: p(y=1|x) = 1 / (1 + exp(-(b0 + b1x)))",
71
  },
72
  case2: {
73
  p1Name: "w1",
74
  p2Name: "w2",
75
+ p1Label: "w1 (weight for x1)",
76
+ p2Label: "w2 (weight for x2)",
77
+ model: "z = w1*x1 + w2*x2",
78
+ explain: [
79
+ "Cost landscape: J(w1, w2)",
80
+ "w1 = coefficient of x1, w2 = coefficient of x2",
81
+ "No intercept is used in this model.",
82
+ ],
83
+ formula: "Case 2 logistic: p(y=1|x1,x2) = 1 / (1 + exp(-(w1x1 + w2x2)))",
84
  },
85
  };
86
 
87
+ const state = {
88
+ datasets: { case1: [], case2: [] },
89
+ dataVersion: 0,
90
+ trajectory: [],
91
+ stepIndex: 0,
92
+ timer: null,
93
+ unstableWarning: "",
94
+ gridCache: { case1: null, case2: null },
95
+ plotReady: false,
96
+ camera3d: null,
97
+ resizeObserver: null,
98
+ };
99
 
100
+ function hasPlotly() {
101
+ return typeof window !== "undefined" && typeof window.Plotly !== "undefined";
102
+ }
103
 
104
+ function showPlotFallback(message) {
105
+ plotEl.innerHTML = `<div class="plot-fallback">${message}</div>`;
106
  }
107
 
108
+ function sigmoid(z) {
109
+ if (z >= 0) {
110
+ const e = Math.exp(-z);
111
+ return 1 / (1 + e);
112
+ }
113
+ const e = Math.exp(z);
114
+ return e / (1 + e);
115
  }
116
 
117
+ function softplus(z) {
118
+ if (z > 35) return z;
119
+ if (z < -35) return Math.exp(z);
120
+ return Math.log1p(Math.exp(z));
121
  }
122
 
123
  function randNormal(mean = 0, std = 1) {
 
127
  return mean + z0 * std;
128
  }
129
 
130
+ function percentile(sortedValues, q) {
131
+ if (sortedValues.length === 0) return NaN;
132
+ const pos = (sortedValues.length - 1) * q;
133
+ const lo = Math.floor(pos);
134
+ const hi = Math.min(sortedValues.length - 1, lo + 1);
135
+ const t = pos - lo;
136
+ return sortedValues[lo] * (1 - t) + sortedValues[hi] * t;
137
+ }
138
+
139
+ function cloneCamera(camera) {
140
+ return {
141
+ eye: { ...camera.eye },
142
+ center: { ...camera.center },
143
+ up: { ...camera.up },
144
+ };
145
+ }
146
+
147
+ function formatCost(v) {
148
+ if (!Number.isFinite(v)) return "--";
149
+ if (Math.abs(v) < 1) return v.toFixed(4);
150
+ if (Math.abs(v) < 100) return v.toFixed(3);
151
+ return v.toFixed(2);
152
+ }
153
+
154
  function generateSyntheticData() {
155
  const case1 = [];
156
  const case2 = [];
157
 
158
+ const trueB0 = -0.15;
159
+ const trueB1 = 1.05;
160
  for (let i = 0; i < 160; i += 1) {
161
+ const center = i < 80 ? -1.5 : 1.5;
162
+ const x = randNormal(center, 1.9);
163
  const p = sigmoid(trueB0 + trueB1 * x);
164
  const y = Math.random() < p ? 1 : 0;
165
  case1.push({ x, y });
166
  }
167
 
168
+ const trueW1 = 1.1;
169
+ const trueW2 = 0.95;
170
+ for (let i = 0; i < 200; i += 1) {
171
+ const center1 = i < 100 ? -1.4 : 1.4;
172
+ const center2 = i < 100 ? -1.2 : 1.2;
173
+ const x1 = randNormal(center1, 1.7);
174
+ const x2 = randNormal(center2 + 0.25 * (x1 - center1), 1.55);
175
  const p = sigmoid(trueW1 * x1 + trueW2 * x2);
176
  const y = Math.random() < p ? 1 : 0;
177
  case2.push({ x1, x2, y });
178
  }
179
 
180
+ state.datasets = { case1, case2 };
181
+ state.dataVersion += 1;
182
+ state.gridCache = { case1: null, case2: null };
183
  }
184
 
185
  function costFor(caseKey, params) {
186
+ const data = state.datasets[caseKey];
187
  let total = 0;
188
 
189
  if (caseKey === "case1") {
190
  const [b0, b1] = params;
191
  for (const row of data) {
192
+ const z = b0 + b1 * row.x;
193
+ total += softplus(z) - row.y * z;
194
  }
195
  } else {
196
  const [w1, w2] = params;
197
  for (const row of data) {
198
+ const z = w1 * row.x1 + w2 * row.x2;
199
+ total += softplus(z) - row.y * z;
200
  }
201
  }
202
 
 
204
  }
205
 
206
  function gradFor(caseKey, params) {
207
+ const data = state.datasets[caseKey];
208
  let g1 = 0;
209
  let g2 = 0;
210
 
 
230
  return [g1 / m, g2 / m];
231
  }
232
 
233
+ function estimateAnchorPoint(caseKey) {
234
+ let params = [0, 0];
235
+ const lr = 0.1;
236
+
237
+ for (let i = 0; i < 220; i += 1) {
238
+ const grad = gradFor(caseKey, params);
239
+ const next = [params[0] - lr * grad[0], params[1] - lr * grad[1]];
240
+ if (!Number.isFinite(next[0]) || !Number.isFinite(next[1])) break;
241
+ params = next;
242
+ }
243
+
244
+ return params;
245
+ }
246
+
247
  function buildTrajectory(caseKey, start, lr, iters) {
248
  const path = [];
249
  let params = [start[0], start[1]];
250
+ let unstable = false;
251
 
252
+ path.push({ step: 0, p1: params[0], p2: params[1], cost: costFor(caseKey, params), dCost: 0 });
253
 
254
  for (let i = 0; i < iters; i += 1) {
255
  const grad = gradFor(caseKey, params);
256
+ const next = [params[0] - lr * grad[0], params[1] - lr * grad[1]];
257
+
258
+ if (!Number.isFinite(next[0]) || !Number.isFinite(next[1])) {
259
+ unstable = true;
260
+ break;
261
+ }
262
+
263
+ const nextCost = costFor(caseKey, next);
264
+ if (!Number.isFinite(nextCost) || nextCost > 1e6) {
265
+ unstable = true;
266
+ break;
267
+ }
268
+
269
+ const prevCost = path[path.length - 1].cost;
270
+ params = next;
271
+ path.push({ step: i + 1, p1: params[0], p2: params[1], cost: nextCost, dCost: nextCost - prevCost });
272
  }
273
 
274
+ return { path, unstable };
275
  }
276
 
277
+ function getTrajectoryBounds(caseKey) {
278
+ const anchor = estimateAnchorPoint(caseKey);
279
+ const p1s = [anchor[0], Number(controls.p1.value)];
280
+ const p2s = [anchor[1], Number(controls.p2.value)];
281
+
282
+ for (const s of state.trajectory) {
283
+ p1s.push(s.p1);
284
+ p2s.push(s.p2);
285
+ }
286
+
287
+ let p1Min = Math.min(...p1s);
288
+ let p1Max = Math.max(...p1s);
289
+ let p2Min = Math.min(...p2s);
290
+ let p2Max = Math.max(...p2s);
291
+
292
+ let span1 = p1Max - p1Min;
293
+ let span2 = p2Max - p2Min;
294
+
295
+ if (span1 < MIN_SPAN) {
296
+ const add = (MIN_SPAN - span1) / 2;
297
+ p1Min -= add;
298
+ p1Max += add;
299
+ span1 = MIN_SPAN;
300
+ }
301
+
302
+ if (span2 < MIN_SPAN) {
303
+ const add = (MIN_SPAN - span2) / 2;
304
+ p2Min -= add;
305
+ p2Max += add;
306
+ span2 = MIN_SPAN;
307
+ }
308
+
309
+ const pad1 = Math.max(0.4 * span1, 0.9);
310
+ const pad2 = Math.max(0.4 * span2, 0.9);
311
+
312
+ return {
313
+ p1Min: p1Min - pad1,
314
+ p1Max: p1Max + pad1,
315
+ p2Min: p2Min - pad2,
316
+ p2Max: p2Max + pad2,
317
+ };
318
+ }
319
+
320
+ function getGridCacheKey(caseKey) {
321
+ const first = state.trajectory[0];
322
+ const last = state.trajectory[state.trajectory.length - 1];
323
+ return [
324
+ state.dataVersion,
325
+ caseKey,
326
+ controls.p1.value,
327
+ controls.p2.value,
328
+ state.trajectory.length,
329
+ first ? `${first.p1.toFixed(4)}:${first.p2.toFixed(4)}` : "none",
330
+ last ? `${last.p1.toFixed(4)}:${last.p2.toFixed(4)}` : "none",
331
+ ].join("|");
332
+ }
333
+
334
+ function buildGrid(caseKey) {
335
+ const bounds = getTrajectoryBounds(caseKey);
336
+ const p1Values = [];
337
+ const p2Values = [];
338
+
339
+ for (let i = 0; i < GRID_N; i += 1) {
340
+ p1Values.push(bounds.p1Min + (i / (GRID_N - 1)) * (bounds.p1Max - bounds.p1Min));
341
+ p2Values.push(bounds.p2Min + (i / (GRID_N - 1)) * (bounds.p2Max - bounds.p2Min));
342
  }
343
 
344
  const vals = [];
345
+ const flat = [];
346
  let min = Infinity;
347
  let max = -Infinity;
348
+ let minI = 0;
349
+ let minJ = 0;
350
 
351
  for (let j = 0; j < GRID_N; j += 1) {
352
  const row = [];
353
+ const p2 = p2Values[j];
354
+
355
  for (let i = 0; i < GRID_N; i += 1) {
356
+ const p1 = p1Values[i];
357
  const c = costFor(caseKey, [p1, p2]);
358
  row.push(c);
359
+ flat.push(c);
360
+
361
+ if (c < min) {
362
+ min = c;
363
+ minI = i;
364
+ minJ = j;
365
+ }
366
  if (c > max) max = c;
367
  }
368
+
369
  vals.push(row);
370
  }
371
 
372
+ const sorted = flat.slice().sort((a, b) => a - b);
373
+ let clipMax = percentile(sorted, 0.975);
374
+ if (!Number.isFinite(clipMax)) clipMax = max;
375
+ clipMax = Math.min(max, Math.max(min + 1e-6, clipMax));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
  return {
378
+ key: "",
379
+ p1Values,
380
+ p2Values,
381
+ vals,
382
+ min,
383
+ max,
384
+ clipMax,
385
+ p1Min: bounds.p1Min,
386
+ p1Max: bounds.p1Max,
387
+ p2Min: bounds.p2Min,
388
+ p2Max: bounds.p2Max,
389
+ minPoint: {
390
+ p1: p1Values[minI],
391
+ p2: p2Values[minJ],
392
+ cost: min,
393
+ },
394
  };
395
  }
396
 
397
+ function getGrid(caseKey) {
398
+ const key = getGridCacheKey(caseKey);
399
+ const cached = state.gridCache[caseKey];
400
+ if (cached && cached.key === key) return cached;
401
+
402
+ const built = buildGrid(caseKey);
403
+ built.key = key;
404
+ state.gridCache[caseKey] = built;
405
+ return built;
406
  }
407
 
408
+ function collectEdgeHit(bucket, a, b, level) {
409
+ const da = a.z - level;
410
+ const db = b.z - level;
411
+ if (da === 0 && db === 0) return;
412
+ if (da * db > 0) return;
413
 
414
+ const den = b.z - a.z;
415
+ const t = Math.abs(den) < 1e-12 ? 0.5 : (level - a.z) / den;
416
+ if (t < 0 || t > 1) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
 
418
+ bucket.push({
419
+ x: a.x + t * (b.x - a.x),
420
+ y: a.y + t * (b.y - a.y),
421
+ });
422
+ }
 
 
 
 
 
 
 
 
 
 
 
423
 
424
+ function buildContourSegments3D(grid) {
425
+ const xs = [];
426
+ const ys = [];
427
+ const zs = [];
428
+ const levels = 9;
429
+ const zFloor = grid.min;
430
+
431
+ for (let levelIdx = 1; levelIdx <= levels; levelIdx += 1) {
432
+ const level = grid.min + (levelIdx / (levels + 1)) * (grid.clipMax - grid.min);
433
+
434
+ for (let j = 0; j < GRID_N - 1; j += 1) {
435
+ for (let i = 0; i < GRID_N - 1; i += 1) {
436
+ const p00 = { x: grid.p1Values[i], y: grid.p2Values[j], z: grid.vals[j][i] };
437
+ const p10 = { x: grid.p1Values[i + 1], y: grid.p2Values[j], z: grid.vals[j][i + 1] };
438
+ const p11 = { x: grid.p1Values[i + 1], y: grid.p2Values[j + 1], z: grid.vals[j + 1][i + 1] };
439
+ const p01 = { x: grid.p1Values[i], y: grid.p2Values[j + 1], z: grid.vals[j + 1][i] };
440
+
441
+ const hits = [];
442
+ collectEdgeHit(hits, p00, p10, level);
443
+ collectEdgeHit(hits, p10, p11, level);
444
+ collectEdgeHit(hits, p11, p01, level);
445
+ collectEdgeHit(hits, p01, p00, level);
446
+
447
+ if (hits.length === 2) {
448
+ xs.push(hits[0].x, hits[1].x, null);
449
+ ys.push(hits[0].y, hits[1].y, null);
450
+ zs.push(zFloor, zFloor, null);
451
+ } else if (hits.length === 4) {
452
+ xs.push(hits[0].x, hits[1].x, null, hits[2].x, hits[3].x, null);
453
+ ys.push(hits[0].y, hits[1].y, null, hits[2].y, hits[3].y, null);
454
+ zs.push(zFloor, zFloor, null, zFloor, zFloor, null);
455
+ }
456
+ }
457
  }
 
 
 
458
  }
459
 
460
+ return { x: xs, y: ys, z: zs };
 
461
  }
462
 
463
+ function buildHover(meta, point) {
464
+ return [
465
+ `Iteration: ${point.step}`,
466
+ `${meta.p1Name}: ${point.p1.toFixed(4)}`,
467
+ `${meta.p2Name}: ${point.p2.toFixed(4)}`,
468
+ `BCE cost: ${point.cost.toFixed(4)}`,
469
+ ].join("<br>");
470
+ }
471
 
472
+ function getFitSize() {
473
+ const host = plotWrap.parentElement;
474
+ const hostW = host ? host.clientWidth : window.innerWidth;
475
+ const maxW = Math.max(320, Math.min(1000, hostW - 4));
476
 
477
+ if (window.innerWidth < 680) return { width: maxW, height: 390 };
478
+ if (window.innerWidth < 1024) return { width: Math.min(maxW, 920), height: 560 };
479
+ return { width: Math.min(maxW, 980), height: 640 };
480
+ }
481
 
482
+ function setPlotSize(rawW, rawH, forceResize = false) {
483
+ const host = plotWrap.parentElement;
484
+ const hostMaxW = host ? host.clientWidth - 4 : rawW;
485
+ const inFullscreen = document.fullscreenElement === plotWrap;
486
 
487
+ const minW = 320;
488
+ const minH = 300;
489
+ const maxW = inFullscreen ? Math.min(1800, window.innerWidth - 24) : Math.max(minW, hostMaxW);
490
+ const maxH = inFullscreen ? Math.min(1200, window.innerHeight - 24) : Math.min(900, window.innerHeight - 140);
491
 
492
+ const w = Math.round(Math.max(minW, Math.min(maxW, rawW)));
493
+ const h = Math.round(Math.max(minH, Math.min(maxH, rawH)));
 
 
494
 
495
+ if (forceResize || plotWrap.clientWidth !== w || plotWrap.clientHeight !== h) {
496
+ plotWrap.style.width = `${w}px`;
497
+ plotWrap.style.height = `${h}px`;
498
+ resizePlot();
499
+ }
500
+ }
501
 
502
+ function fitPlotSize(forceResize = false) {
503
+ const size = getFitSize();
504
+ setPlotSize(size.width, size.height, forceResize);
505
  }
506
 
507
+ function resizePlot() {
508
+ if (!state.plotReady || !hasPlotly()) return;
509
+ requestAnimationFrame(() => window.Plotly.Plots.resize(plotEl));
510
+ }
 
511
 
512
+ function toggleFullscreen() {
513
+ if (document.fullscreenElement === plotWrap) {
514
+ document.exitFullscreen();
515
+ return;
 
 
 
516
  }
517
+ if (plotWrap.requestFullscreen) plotWrap.requestFullscreen();
518
+ }
519
 
520
+ function updateStepSlider() {
521
+ const max = Math.max(0, state.trajectory.length - 1);
522
+ controls.stepSlider.max = String(max);
523
+ controls.stepSlider.value = String(Math.min(state.stepIndex, max));
524
+ labels.stepLabel.textContent = `${Math.min(state.stepIndex, max)} / ${max}`;
525
+ }
526
 
527
+ function updateModelDefinition(caseKey) {
528
+ const meta = caseMeta[caseKey];
529
+ modelDefinition.innerHTML = `
530
+ <p><strong>Model:</strong> ${meta.model}</p>
531
+ <p>${meta.explain[0]}</p>
532
+ <p>${meta.explain[1]}</p>
533
+ ${meta.explain[2] ? `<p>${meta.explain[2]}</p>` : ""}
534
+ `;
535
  }
536
 
537
+ function updateSummary(caseKey) {
538
+ const meta = caseMeta[caseKey];
539
 
540
+ if (state.trajectory.length === 0) {
541
+ landscapeSummary.innerHTML = `
542
+ <span class="landscape-pill">Start BCE: --</span>
543
+ <span class="landscape-pill">Current BCE: --</span>
544
+ <span class="landscape-pill">Final BCE: --</span>
545
+ <span class="landscape-pill">Reduction: --</span>
546
+ <span class="landscape-pill">Iterations: 0 / 0</span>
547
+ `;
548
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  }
550
 
551
+ const idx = Math.min(state.stepIndex, state.trajectory.length - 1);
552
+ const start = state.trajectory[0];
553
+ const current = state.trajectory[idx];
554
+ const final = state.trajectory[state.trajectory.length - 1];
555
+ const reduction = ((start.cost - final.cost) / Math.max(1e-9, start.cost)) * 100;
556
+
557
+ landscapeSummary.innerHTML = `
558
+ <span class="landscape-pill">Start BCE: ${start.cost.toFixed(4)}</span>
559
+ <span class="landscape-pill">Current BCE: ${current.cost.toFixed(4)}</span>
560
+ <span class="landscape-pill">Final BCE: ${final.cost.toFixed(4)}</span>
561
+ <span class="landscape-pill">Reduction: ${reduction.toFixed(1)}%</span>
562
+ <span class="landscape-pill">Iterations: ${idx} / ${state.trajectory.length - 1}</span>
563
+ <span class="landscape-pill">${meta.p1Name}: ${current.p1.toFixed(3)}, ${meta.p2Name}: ${current.p2.toFixed(3)}</span>
564
+ `;
565
+ }
566
+
567
+ function updateInfoChips(caseKey, grid) {
568
+ const meta = caseMeta[caseKey];
569
+
570
+ if (grid.clipMax < grid.max - 1e-9) {
571
+ clipInfo.textContent = `Surface clipping (visual only): J <= ${formatCost(grid.clipMax)} (true max ${formatCost(grid.max)})`;
572
+ } else {
573
+ clipInfo.textContent = "Surface clipping: none";
574
+ }
575
+
576
+ minimumInfo.textContent = `Lowest shown point: ${meta.p1Name}=${grid.minPoint.p1.toFixed(3)}, ${meta.p2Name}=${grid.minPoint.p2.toFixed(3)}, J=${grid.minPoint.cost.toFixed(4)}`;
577
+
578
+ if (state.unstableWarning) {
579
+ stabilityWarning.textContent = state.unstableWarning;
580
+ stabilityWarning.classList.add("warning");
581
+ } else {
582
+ stabilityWarning.textContent = "Stability: normal";
583
+ stabilityWarning.classList.remove("warning");
584
+ }
585
  }
586
 
587
  function renderStats(caseKey) {
588
  const meta = caseMeta[caseKey];
589
+ const m = state.datasets[caseKey].length;
590
 
591
+ if (state.trajectory.length === 0) {
592
  stats.innerHTML = `
593
  <span>Model case: ${meta.p1Name}, ${meta.p2Name}</span>
594
+ <span>Samples: ${m}</span>
595
  <span>No trajectory yet. Click Run Gradient Descent.</span>
596
+ <span>Current step: 0</span>
597
+ <span>${meta.p1Label}: --</span>
598
+ <span>${meta.p2Label}: --</span>
599
  `;
600
  return;
601
  }
602
 
603
+ const idx = Math.min(state.stepIndex, state.trajectory.length - 1);
604
+ const current = state.trajectory[idx];
 
 
605
 
606
  stats.innerHTML = `
607
  <span>Model case: ${meta.p1Name}, ${meta.p2Name}</span>
608
+ <span>Samples: ${m}</span>
609
+ <span>Current step: ${idx}</span>
610
+ <span>BCE: ${current.cost.toFixed(4)}</span>
611
+ <span>${meta.p1Label}: ${current.p1.toFixed(4)}</span>
612
+ <span>${meta.p2Label}: ${current.p2.toFixed(4)}</span>
613
  `;
614
  }
615
 
616
+ function renderLandscape(caseKey, grid) {
617
+ if (!hasPlotly()) {
618
+ showPlotFallback("Interactive 3D view unavailable (Plotly not loaded). Other page controls remain active.");
619
+ state.plotReady = false;
620
+ return;
621
+ }
622
+
623
+ const meta = caseMeta[caseKey];
624
+ const view = controls.landscapeView.value;
625
+ const idx = Math.min(state.stepIndex, Math.max(0, state.trajectory.length - 1));
626
+
627
+ const showSurface = controls.toggleSurface.checked;
628
+ const showContours = controls.toggleContours.checked;
629
+ const showTrajectory = controls.toggleTrajectory.checked;
630
+ const showCurrent = controls.toggleCurrent.checked;
631
+
632
+ const fullPath = state.trajectory;
633
+ const current = fullPath[idx];
634
+ const start = fullPath[0];
635
+ const final = fullPath[fullPath.length - 1];
636
+
637
+ const traces = [];
638
+
639
+ if (view === "contour2d") {
640
+ traces.push({
641
+ type: "contour",
642
+ x: grid.p1Values,
643
+ y: grid.p2Values,
644
+ z: grid.vals,
645
+ ncontours: 16,
646
+ contours: {
647
+ coloring: showSurface ? "heatmap" : "lines",
648
+ },
649
+ colorscale: "Viridis",
650
+ showscale: showSurface,
651
+ visible: showSurface || showContours,
652
+ colorbar: { title: "BCE", thickness: 14 },
653
+ hovertemplate: `${meta.p1Name}: %{x:.4f}<br>${meta.p2Name}: %{y:.4f}<br>BCE: %{z:.4f}<extra></extra>`,
654
+ });
655
+
656
+ if (showTrajectory && fullPath.length > 0) {
657
+ traces.push({
658
+ type: "scatter",
659
+ mode: "lines+markers",
660
+ x: fullPath.map((p) => p.p1),
661
+ y: fullPath.map((p) => p.p2),
662
+ customdata: fullPath.map((p) => p.step),
663
+ line: { color: "#d8534f", width: 3 },
664
+ marker: { size: 4, color: "#9a3412" },
665
+ text: fullPath.map((p) => buildHover(meta, p)),
666
+ hovertemplate: "%{text}<extra></extra>",
667
+ });
668
+
669
+ traces.push({
670
+ type: "scatter",
671
+ mode: "markers+text",
672
+ x: [start.p1],
673
+ y: [start.p2],
674
+ customdata: [start.step],
675
+ marker: { size: 10, color: "#0a8f7b", line: { color: "#102a32", width: 1 } },
676
+ text: ["Start"],
677
+ textposition: "top center",
678
+ hovertemplate: `${buildHover(meta, start)}<extra></extra>`,
679
+ });
680
+
681
+ traces.push({
682
+ type: "scatter",
683
+ mode: "markers+text",
684
+ x: [final.p1],
685
+ y: [final.p2],
686
+ customdata: [final.step],
687
+ marker: { size: 10, color: "#dd5e2f", line: { color: "#102a32", width: 1 } },
688
+ text: ["Current solution"],
689
+ textposition: "top center",
690
+ hovertemplate: `${buildHover(meta, final)}<extra></extra>`,
691
+ });
692
+ }
693
+
694
+ if (showCurrent && fullPath.length > 0) {
695
+ traces.push({
696
+ type: "scatter",
697
+ mode: "markers+text",
698
+ x: [current.p1],
699
+ y: [current.p2],
700
+ customdata: [current.step],
701
+ marker: { size: 10, color: "#111827", line: { color: "#f2b84b", width: 2 } },
702
+ text: [`Step ${idx}`],
703
+ textposition: "bottom center",
704
+ hovertemplate: `${buildHover(meta, current)}<extra></extra>`,
705
+ });
706
+ }
707
+
708
+ const layout2d = {
709
+ autosize: true,
710
+ margin: { l: 58, r: 16, t: 8, b: 50 },
711
+ xaxis: { title: meta.p1Label },
712
+ yaxis: { title: meta.p2Label },
713
+ showlegend: false,
714
+ uirevision: "logistic-contour",
715
+ paper_bgcolor: "#fff",
716
+ plot_bgcolor: "#fff",
717
+ };
718
+
719
+ const config2d = {
720
+ responsive: true,
721
+ displaylogo: false,
722
+ scrollZoom: true,
723
+ modeBarButtonsToRemove: ["lasso2d", "select2d"],
724
+ };
725
+
726
+ const p2d = state.plotReady
727
+ ? window.Plotly.react(plotEl, traces, layout2d, config2d)
728
+ : window.Plotly.newPlot(plotEl, traces, layout2d, config2d);
729
+
730
+ p2d.then(() => {
731
+ if (!state.plotReady) {
732
+ state.plotReady = true;
733
+ bindPlotClick();
734
+ }
735
+ resizePlot();
736
+ });
737
+ return;
738
+ }
739
+
740
+ const clippedSurface = grid.vals.map((row) => row.map((v) => Math.min(v, grid.clipMax)));
741
+
742
+ traces.push({
743
+ type: "surface",
744
+ x: grid.p1Values,
745
+ y: grid.p2Values,
746
+ z: clippedSurface,
747
+ opacity: 0.84,
748
+ colorscale: "Viridis",
749
+ showscale: showSurface,
750
+ visible: showSurface,
751
+ colorbar: { title: "BCE", thickness: 14 },
752
+ hovertemplate: `${meta.p1Name}: %{x:.4f}<br>${meta.p2Name}: %{y:.4f}<br>BCE: %{z:.4f}<extra></extra>`,
753
+ });
754
+
755
+ const contour3d = buildContourSegments3D(grid);
756
+ traces.push({
757
+ type: "scatter3d",
758
+ mode: "lines",
759
+ x: contour3d.x,
760
+ y: contour3d.y,
761
+ z: contour3d.z,
762
+ line: { color: "rgba(16,54,76,0.7)", width: 3 },
763
+ visible: showContours || view === "surface3dContours",
764
+ hoverinfo: "skip",
765
+ });
766
+
767
+ if (showTrajectory && fullPath.length > 0) {
768
+ traces.push({
769
+ type: "scatter3d",
770
+ mode: "lines+markers",
771
+ x: fullPath.map((p) => p.p1),
772
+ y: fullPath.map((p) => p.p2),
773
+ z: fullPath.map((p) => Math.min(p.cost, grid.clipMax)),
774
+ customdata: fullPath.map((p) => p.step),
775
+ line: { color: "#d8534f", width: 6 },
776
+ marker: { size: 4, color: "#9a3412" },
777
+ text: fullPath.map((p) => buildHover(meta, p)),
778
+ hovertemplate: "%{text}<extra></extra>",
779
+ });
780
+
781
+ traces.push({
782
+ type: "scatter3d",
783
+ mode: "markers+text",
784
+ x: [start.p1],
785
+ y: [start.p2],
786
+ z: [Math.min(start.cost, grid.clipMax)],
787
+ customdata: [start.step],
788
+ marker: { size: 10, color: "#0a8f7b", line: { color: "#102a32", width: 1 } },
789
+ text: ["Start"],
790
+ textposition: "top center",
791
+ hovertemplate: `${buildHover(meta, start)}<extra></extra>`,
792
+ });
793
+
794
+ traces.push({
795
+ type: "scatter3d",
796
+ mode: "markers+text",
797
+ x: [final.p1],
798
+ y: [final.p2],
799
+ z: [Math.min(final.cost, grid.clipMax)],
800
+ customdata: [final.step],
801
+ marker: { size: 10, color: "#dd5e2f", line: { color: "#102a32", width: 1 } },
802
+ text: ["Current solution"],
803
+ textposition: "top center",
804
+ hovertemplate: `${buildHover(meta, final)}<extra></extra>`,
805
+ });
806
+ }
807
+
808
+ if (showCurrent && fullPath.length > 0) {
809
+ traces.push({
810
+ type: "scatter3d",
811
+ mode: "markers+text",
812
+ x: [current.p1],
813
+ y: [current.p2],
814
+ z: [Math.min(current.cost, grid.clipMax)],
815
+ customdata: [current.step],
816
+ marker: { size: 9, color: "#111827", line: { color: "#f2b84b", width: 2 } },
817
+ text: [`Step ${idx}`],
818
+ textposition: "bottom center",
819
+ hovertemplate: `${buildHover(meta, current)}<extra></extra>`,
820
+ });
821
+ }
822
+
823
+ const layout3d = {
824
+ autosize: true,
825
+ margin: { l: 0, r: 0, t: 8, b: 0 },
826
+ scene: {
827
+ xaxis: { title: meta.p1Label, range: [grid.p1Min, grid.p1Max], showspikes: false },
828
+ yaxis: { title: meta.p2Label, range: [grid.p2Min, grid.p2Max], showspikes: false },
829
+ zaxis: { title: "Binary Cross-Entropy J(theta)", range: [grid.min, grid.clipMax], showspikes: false },
830
+ aspectmode: "manual",
831
+ aspectratio: { x: 1.14, y: 1.12, z: 0.82 },
832
+ camera: state.camera3d || cloneCamera(DEFAULT_CAMERA),
833
+ dragmode: "turntable",
834
+ },
835
+ showlegend: false,
836
+ uirevision: "logistic-3d",
837
+ paper_bgcolor: "#fff",
838
+ };
839
+
840
+ const config3d = {
841
+ responsive: true,
842
+ displaylogo: false,
843
+ scrollZoom: true,
844
+ doubleClick: "reset",
845
+ modeBarButtonsToRemove: ["lasso2d", "select2d"],
846
+ };
847
+
848
+ const p3d = state.plotReady
849
+ ? window.Plotly.react(plotEl, traces, layout3d, config3d)
850
+ : window.Plotly.newPlot(plotEl, traces, layout3d, config3d);
851
+
852
+ p3d.then(() => {
853
+ if (!state.plotReady) {
854
+ state.plotReady = true;
855
+ bindPlotClick();
856
+ plotEl.on("plotly_relayout", (ev) => {
857
+ if (ev && ev["scene.camera"]) {
858
+ state.camera3d = cloneCamera(ev["scene.camera"]);
859
+ }
860
+ });
861
+ }
862
+ resizePlot();
863
+ });
864
+ }
865
+
866
+ function bindPlotClick() {
867
+ if (!plotEl || !plotEl.on) return;
868
+
869
+ plotEl.on("plotly_click", (ev) => {
870
+ if (!ev || !ev.points || ev.points.length === 0) return;
871
+ const pt = ev.points[0];
872
+ const raw = pt.customdata;
873
+
874
+ let step = null;
875
+ if (typeof raw === "number") step = raw;
876
+ if (Array.isArray(raw) && typeof raw[0] === "number") step = raw[0];
877
+
878
+ if (Number.isInteger(step) && step >= 0 && step < state.trajectory.length) {
879
+ state.stepIndex = step;
880
+ render();
881
+ }
882
+ });
883
+ }
884
+
885
+ function drawClassifier(caseKey) {
886
+ const parent = classifierCanvas.parentElement;
887
+ const width = Math.max(320, Math.floor(parent.clientWidth - 2));
888
+ const height = 310;
889
+ if (classifierCanvas.width !== width || classifierCanvas.height !== height) {
890
+ classifierCanvas.width = width;
891
+ classifierCanvas.height = height;
892
+ }
893
+
894
+ const ctx = classifierCtx;
895
+ ctx.clearRect(0, 0, width, height);
896
+ ctx.fillStyle = "#fff";
897
+ ctx.fillRect(0, 0, width, height);
898
+
899
+ const pad = 42;
900
+
901
+ if (state.trajectory.length === 0) {
902
+ ctx.fillStyle = "#2a3d46";
903
+ ctx.font = "14px 'Avenir Next', sans-serif";
904
+ ctx.fillText("Run Gradient Descent to view classifier changes by iteration.", 22, 42);
905
+ return;
906
+ }
907
+
908
+ const idx = Math.min(state.stepIndex, state.trajectory.length - 1);
909
+ const step = state.trajectory[idx];
910
+ const data = state.datasets[caseKey];
911
+
912
+ if (caseKey === "case1") {
913
+ const xs = data.map((d) => d.x);
914
+ const xMin = Math.min(...xs) - 0.6;
915
+ const xMax = Math.max(...xs) + 0.6;
916
+
917
+ const toX = (x) => pad + ((x - xMin) / (xMax - xMin || 1)) * (width - 2 * pad);
918
+ const toY = (p) => height - pad - p * (height - 2 * pad);
919
+
920
+ ctx.strokeStyle = "#7b8f98";
921
+ ctx.lineWidth = 1.1;
922
+ ctx.beginPath();
923
+ ctx.moveTo(pad, height - pad);
924
+ ctx.lineTo(width - pad, height - pad);
925
+ ctx.stroke();
926
+ ctx.beginPath();
927
+ ctx.moveTo(pad, height - pad);
928
+ ctx.lineTo(pad, pad);
929
+ ctx.stroke();
930
+
931
+ ctx.setLineDash([5, 4]);
932
+ ctx.strokeStyle = "#8aa0ad";
933
+ ctx.beginPath();
934
+ ctx.moveTo(pad, toY(0.5));
935
+ ctx.lineTo(width - pad, toY(0.5));
936
+ ctx.stroke();
937
+ ctx.setLineDash([]);
938
+
939
+ ctx.fillStyle = "#0a8f7b";
940
+ for (const row of data) {
941
+ const yJ = row.y === 1 ? 0.93 : 0.07;
942
+ ctx.beginPath();
943
+ ctx.arc(toX(row.x), toY(yJ), 3.4, 0, Math.PI * 2);
944
+ ctx.fill();
945
+ }
946
+
947
+ ctx.strokeStyle = "#d8534f";
948
+ ctx.lineWidth = 2.6;
949
+ ctx.beginPath();
950
+ for (let i = 0; i <= 120; i += 1) {
951
+ const x = xMin + (i / 120) * (xMax - xMin);
952
+ const p = sigmoid(step.p1 + step.p2 * x);
953
+ const sx = toX(x);
954
+ const sy = toY(p);
955
+ if (i === 0) ctx.moveTo(sx, sy);
956
+ else ctx.lineTo(sx, sy);
957
+ }
958
+ ctx.stroke();
959
+
960
+ if (Math.abs(step.p2) > 1e-9) {
961
+ const xb = -step.p1 / step.p2;
962
+ if (xb >= xMin && xb <= xMax) {
963
+ ctx.setLineDash([4, 4]);
964
+ ctx.strokeStyle = "#111827";
965
+ ctx.beginPath();
966
+ ctx.moveTo(toX(xb), pad);
967
+ ctx.lineTo(toX(xb), height - pad);
968
+ ctx.stroke();
969
+ ctx.setLineDash([]);
970
+ }
971
+ }
972
+
973
+ ctx.fillStyle = "#334b56";
974
+ ctx.font = "13px 'Avenir Next', sans-serif";
975
+ ctx.fillText("x", width - pad - 10, height - pad + 20);
976
+ ctx.fillText("Probability", pad - 28, pad - 10);
977
+ } else {
978
+ const x1s = data.map((d) => d.x1);
979
+ const x2s = data.map((d) => d.x2);
980
+ const x1Min = Math.min(...x1s) - 0.7;
981
+ const x1Max = Math.max(...x1s) + 0.7;
982
+ const x2Min = Math.min(...x2s) - 0.7;
983
+ const x2Max = Math.max(...x2s) + 0.7;
984
+
985
+ const toX = (x) => pad + ((x - x1Min) / (x1Max - x1Min || 1)) * (width - 2 * pad);
986
+ const toY = (y) => height - pad - ((y - x2Min) / (x2Max - x2Min || 1)) * (height - 2 * pad);
987
+
988
+ ctx.strokeStyle = "#7b8f98";
989
+ ctx.lineWidth = 1.1;
990
+ ctx.beginPath();
991
+ ctx.moveTo(pad, height - pad);
992
+ ctx.lineTo(width - pad, height - pad);
993
+ ctx.stroke();
994
+ ctx.beginPath();
995
+ ctx.moveTo(pad, height - pad);
996
+ ctx.lineTo(pad, pad);
997
+ ctx.stroke();
998
+
999
+ for (const row of data) {
1000
+ ctx.fillStyle = row.y === 1 ? "#0a8f7b" : "#d8534f";
1001
+ ctx.beginPath();
1002
+ ctx.arc(toX(row.x1), toY(row.x2), 3.2, 0, Math.PI * 2);
1003
+ ctx.fill();
1004
+ }
1005
+
1006
+ ctx.strokeStyle = "#111827";
1007
+ ctx.lineWidth = 2.4;
1008
+
1009
+ if (Math.abs(step.p2) > 1e-9) {
1010
+ const xa = x1Min;
1011
+ const xb = x1Max;
1012
+ const ya = -(step.p1 / step.p2) * xa;
1013
+ const yb = -(step.p1 / step.p2) * xb;
1014
+ ctx.beginPath();
1015
+ ctx.moveTo(toX(xa), toY(ya));
1016
+ ctx.lineTo(toX(xb), toY(yb));
1017
+ ctx.stroke();
1018
+ } else {
1019
+ ctx.beginPath();
1020
+ ctx.moveTo(toX(0), pad);
1021
+ ctx.lineTo(toX(0), height - pad);
1022
+ ctx.stroke();
1023
+ }
1024
+
1025
+ ctx.fillStyle = "#334b56";
1026
+ ctx.font = "13px 'Avenir Next', sans-serif";
1027
+ ctx.fillText("x1", width - pad - 12, height - pad + 20);
1028
+ ctx.fillText("x2", pad - 22, pad - 10);
1029
+ }
1030
+ }
1031
+
1032
  function render() {
1033
  const caseKey = controls.caseSelect.value;
1034
+ const meta = caseMeta[caseKey];
1035
 
1036
+ labels.formula.textContent = meta.formula;
1037
  labels.lr.textContent = Number(controls.lr.value).toFixed(3);
1038
  labels.iters.textContent = controls.iters.value;
1039
  labels.p1.textContent = Number(controls.p1.value).toFixed(2);
1040
  labels.p2.textContent = Number(controls.p2.value).toFixed(2);
1041
  labels.speed.textContent = controls.speed.value;
1042
 
1043
+ updateModelDefinition(caseKey);
1044
+ updateStepSlider();
1045
+
1046
+ const grid = getGrid(caseKey);
1047
+ updateInfoChips(caseKey, grid);
1048
+ updateSummary(caseKey);
1049
+ renderLandscape(caseKey, grid);
1050
  renderStats(caseKey);
1051
+ drawClassifier(caseKey);
1052
  }
1053
 
1054
  function stopAnimation() {
1055
+ if (!state.timer) {
 
 
1056
  controls.autoBtn.textContent = "Automatic Run";
1057
+ return;
1058
  }
1059
+
1060
+ clearInterval(state.timer);
1061
+ state.timer = null;
1062
+ controls.autoBtn.textContent = "Automatic Run";
1063
  }
1064
 
1065
  function runTrajectory() {
 
1070
  const lr = Number(controls.lr.value);
1071
  const iters = Number(controls.iters.value);
1072
 
1073
+ const { path, unstable } = buildTrajectory(caseKey, start, lr, iters);
1074
+ state.trajectory = path;
1075
+ state.stepIndex = 0;
1076
+ state.unstableWarning = unstable
1077
+ ? "The learning rate may be too large; gradient descent became unstable."
1078
+ : "";
1079
+
1080
+ state.gridCache[caseKey] = null;
1081
  render();
1082
  }
1083
 
1084
  function nextStep() {
1085
+ if (state.trajectory.length === 0) {
1086
  runTrajectory();
1087
  return;
1088
  }
1089
+
1090
+ if (state.stepIndex < state.trajectory.length - 1) {
1091
+ state.stepIndex += 1;
1092
  render();
1093
  }
1094
  }
1095
 
1096
+ function autoRun() {
1097
+ if (state.timer) {
1098
  stopAnimation();
1099
  return;
1100
  }
1101
 
1102
+ if (state.trajectory.length === 0) runTrajectory();
 
 
1103
 
1104
  controls.autoBtn.textContent = "Stop Auto";
1105
+ state.timer = setInterval(() => {
1106
+ if (state.stepIndex >= state.trajectory.length - 1) {
 
1107
  stopAnimation();
1108
  return;
1109
  }
1110
+
1111
+ state.stepIndex += 1;
1112
  render();
1113
  }, Number(controls.speed.value));
1114
  }
1115
 
1116
  function restartPath() {
1117
  stopAnimation();
1118
+ if (state.trajectory.length > 0) {
1119
+ state.stepIndex = 0;
1120
  }
1121
  render();
1122
  }
1123
 
1124
  function clearPath() {
1125
  stopAnimation();
1126
+ state.trajectory = [];
1127
+ state.stepIndex = 0;
1128
+ state.unstableWarning = "";
1129
+ state.gridCache = { case1: null, case2: null };
 
 
 
 
 
 
 
 
 
 
 
 
1130
  render();
1131
  }
1132
 
 
1134
  if (caseKey === "case1") {
1135
  controls.p1.value = -4.0;
1136
  controls.p2.value = 4.0;
1137
+ controls.lr.value = 0.1;
1138
  } else {
1139
  controls.p1.value = -2.2;
1140
  controls.p2.value = -2.0;
1141
+ controls.lr.value = 0.1;
1142
  }
1143
+
1144
  controls.lrInput.value = Number(controls.lr.value).toFixed(3);
1145
  controls.iters.value = 80;
1146
  controls.speed.value = 90;
1147
+
1148
  clearPath();
1149
  }
1150
 
1151
+ function resetView() {
1152
+ if (!hasPlotly() || !state.plotReady) return;
1153
+
1154
+ if (controls.landscapeView.value === "contour2d") {
1155
+ window.Plotly.relayout(plotEl, {
1156
+ "xaxis.autorange": true,
1157
+ "yaxis.autorange": true,
1158
+ });
1159
+ return;
1160
+ }
1161
+
1162
+ state.camera3d = cloneCamera(DEFAULT_CAMERA);
1163
+ window.Plotly.relayout(plotEl, { "scene.camera": state.camera3d });
1164
  }
1165
 
1166
+ function bindEvents() {
1167
+ controls.lr.addEventListener("input", () => {
1168
+ controls.lrInput.value = Number(controls.lr.value).toFixed(3);
1169
+ render();
1170
+ });
1171
+
1172
+ controls.lrInput.addEventListener("change", () => {
1173
+ let v = Number(controls.lrInput.value);
1174
+ if (!Number.isFinite(v)) v = 0.1;
1175
+ v = Math.max(0.001, Math.min(10.0, v));
1176
+ controls.lr.value = v;
1177
+ controls.lrInput.value = v.toFixed(3);
1178
+ render();
1179
+ });
1180
+
1181
+ for (const el of [controls.iters, controls.p1, controls.p2, controls.speed]) {
1182
+ el.addEventListener("input", render);
1183
+ }
1184
 
1185
+ controls.caseSelect.addEventListener("change", () => {
1186
+ applyCaseDefaults(controls.caseSelect.value);
1187
+ });
1188
+
1189
+ controls.runBtn.addEventListener("click", runTrajectory);
1190
+ controls.stepBtn.addEventListener("click", nextStep);
1191
+ controls.autoBtn.addEventListener("click", autoRun);
1192
+ controls.restartBtn.addEventListener("click", restartPath);
1193
+ controls.clearBtn.addEventListener("click", clearPath);
1194
+
1195
+ controls.regenBtn.addEventListener("click", () => {
1196
+ generateSyntheticData();
1197
+ clearPath();
1198
+ });
1199
+
1200
+ controls.landscapeView.addEventListener("change", render);
1201
+ controls.toggleSurface.addEventListener("change", render);
1202
+ controls.toggleContours.addEventListener("change", render);
1203
+ controls.toggleTrajectory.addEventListener("change", render);
1204
+ controls.toggleCurrent.addEventListener("change", render);
1205
+
1206
+ controls.stepSlider.addEventListener("input", () => {
1207
+ const next = Number(controls.stepSlider.value);
1208
+ if (!Number.isFinite(next)) return;
1209
+ state.stepIndex = Math.max(0, Math.min(next, Math.max(0, state.trajectory.length - 1)));
1210
+ render();
1211
+ });
1212
+
1213
+ controls.resetViewBtn.addEventListener("click", resetView);
1214
+ controls.plotFitBtn.addEventListener("click", () => fitPlotSize(true));
1215
+ controls.plotLargerBtn.addEventListener("click", () => setPlotSize(plotWrap.clientWidth * 1.14, plotWrap.clientHeight * 1.14, true));
1216
+ controls.plotSmallerBtn.addEventListener("click", () => setPlotSize(plotWrap.clientWidth / 1.14, plotWrap.clientHeight / 1.14, true));
1217
+ controls.plotFullscreenBtn.addEventListener("click", toggleFullscreen);
1218
+
1219
+ document.addEventListener("fullscreenchange", () => {
1220
+ controls.plotFullscreenBtn.textContent = document.fullscreenElement === plotWrap ? "Exit Fullscreen" : "Fullscreen";
1221
+ resizePlot();
1222
+ });
1223
+
1224
+ window.addEventListener("resize", () => {
1225
+ if (document.fullscreenElement !== plotWrap) {
1226
+ setPlotSize(plotWrap.clientWidth, plotWrap.clientHeight, true);
1227
+ }
1228
+ drawClassifier(controls.caseSelect.value);
1229
+ resizePlot();
1230
+ });
1231
+
1232
+ if (window.ResizeObserver) {
1233
+ state.resizeObserver = new window.ResizeObserver(() => {
1234
+ resizePlot();
1235
+ drawClassifier(controls.caseSelect.value);
1236
+ });
1237
+ state.resizeObserver.observe(plotWrap);
1238
+ state.resizeObserver.observe(classifierCanvas.parentElement);
1239
+ }
1240
+ }
1241
+
1242
+ function init() {
1243
+ fitPlotSize(true);
1244
+ bindEvents();
1245
  generateSyntheticData();
1246
+ applyCaseDefaults("case1");
1247
+
1248
+ if (!hasPlotly()) {
1249
+ showPlotFallback("Interactive 3D view unavailable (Plotly script did not load).");
1250
+ }
1251
+ }
1252
 
1253
+ init();
 
logistic-regression/styles.css CHANGED
@@ -156,14 +156,138 @@ select {
156
  margin: 0;
157
  }
158
 
159
- .chart-panel canvas {
160
- width: 100%;
161
- height: auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  border: 1px solid var(--line);
163
  border-radius: 10px;
164
  background: #fff;
165
  }
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  .stats {
168
  margin-top: 0.8rem;
169
  display: grid;
@@ -186,6 +310,58 @@ select {
186
  font-size: 0.9rem;
187
  }
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  .page-copy {
190
  max-width: 760px;
191
  margin: 1.2rem auto;
@@ -196,6 +372,12 @@ select {
196
  .layout {
197
  grid-template-columns: 1fr;
198
  }
 
 
 
 
 
 
199
  }
200
 
201
  .home-page {
 
156
  margin: 0;
157
  }
158
 
159
+ .model-definition {
160
+ border: 1px solid var(--line);
161
+ border-radius: 10px;
162
+ padding: 0.7rem 0.8rem;
163
+ background: #fcfeff;
164
+ font-size: 0.9rem;
165
+ color: #23343b;
166
+ }
167
+
168
+ .model-definition p {
169
+ margin: 0.18rem 0;
170
+ }
171
+
172
+ .landscape-summary {
173
+ margin: 0.7rem 0 0.8rem;
174
+ display: grid;
175
+ grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
176
+ gap: 0.5rem;
177
+ }
178
+
179
+ .landscape-pill {
180
+ border: 1px solid var(--line);
181
+ border-radius: 8px;
182
+ padding: 0.42rem 0.55rem;
183
+ background: #fcfeff;
184
+ font-size: 0.88rem;
185
+ }
186
+
187
+ .landscape-controls {
188
+ display: flex;
189
+ flex-wrap: wrap;
190
+ align-items: center;
191
+ gap: 0.5rem;
192
+ margin-bottom: 0.7rem;
193
+ }
194
+
195
+ .landscape-controls label {
196
+ min-width: 0;
197
+ margin: 0;
198
+ width: auto;
199
+ font-weight: 700;
200
+ }
201
+
202
+ .landscape-controls select {
203
+ min-width: 220px;
204
+ width: auto;
205
+ }
206
+
207
+ .viz-btn {
208
+ border: 1px solid var(--line);
209
+ background: #fff;
210
+ color: var(--ink);
211
+ border-radius: 9px;
212
+ padding: 0.45rem 0.7rem;
213
+ font-weight: 600;
214
+ width: auto;
215
+ }
216
+
217
+ .viz-btn:hover {
218
+ border-color: var(--accent);
219
+ }
220
+
221
+ .landscape-controls.toggles {
222
+ margin-bottom: 0.8rem;
223
+ }
224
+
225
+ .viz-toggle {
226
+ display: inline-flex;
227
+ align-items: center;
228
+ gap: 0.45rem;
229
+ border: 1px solid var(--line);
230
+ border-radius: 999px;
231
+ background: #fff;
232
+ padding: 0.36rem 0.62rem;
233
+ font-size: 0.88rem;
234
+ font-weight: 600;
235
+ }
236
+
237
+ .viz-toggle input {
238
+ margin: 0;
239
+ }
240
+
241
+ .landscape-plot-wrap {
242
+ width: min(100%, 980px);
243
+ height: 620px;
244
+ min-width: 320px;
245
+ min-height: 320px;
246
+ max-width: 1400px;
247
+ max-height: 900px;
248
+ resize: both;
249
+ overflow: auto;
250
  border: 1px solid var(--line);
251
  border-radius: 10px;
252
  background: #fff;
253
  }
254
 
255
+ .landscape-plot {
256
+ width: 100%;
257
+ height: 100%;
258
+ min-height: 100%;
259
+ }
260
+
261
+ .plot-fallback {
262
+ height: 100%;
263
+ min-height: 240px;
264
+ display: grid;
265
+ place-items: center;
266
+ text-align: center;
267
+ color: #2a3c45;
268
+ padding: 1rem;
269
+ font-size: 0.92rem;
270
+ }
271
+
272
+ .viz-info-row {
273
+ margin: 0.6rem 0 0.8rem;
274
+ display: grid;
275
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
276
+ gap: 0.55rem;
277
+ }
278
+
279
+ .viz-chip {
280
+ border: 1px solid var(--line);
281
+ border-radius: 8px;
282
+ padding: 0.45rem 0.6rem;
283
+ background: #fcfeff;
284
+ font-size: 0.88rem;
285
+ }
286
+
287
+ .viz-chip.warning {
288
+ border-color: #f2b84b;
289
+ }
290
+
291
  .stats {
292
  margin-top: 0.8rem;
293
  display: grid;
 
310
  font-size: 0.9rem;
311
  }
312
 
313
+ .slice-label {
314
+ margin: 0.65rem 0 0;
315
+ color: var(--muted);
316
+ font-size: 0.85rem;
317
+ }
318
+
319
+ .iteration-block {
320
+ margin-top: 0.9rem;
321
+ display: grid;
322
+ grid-template-columns: auto 1fr auto;
323
+ align-items: center;
324
+ gap: 0.55rem;
325
+ }
326
+
327
+ .iteration-block label {
328
+ margin: 0;
329
+ min-width: 0;
330
+ font-size: 0.9rem;
331
+ }
332
+
333
+ .iteration-block input[type="range"] {
334
+ width: 100%;
335
+ accent-color: var(--accent);
336
+ }
337
+
338
+ .iteration-block span {
339
+ font-size: 0.86rem;
340
+ color: #324b55;
341
+ font-weight: 600;
342
+ }
343
+
344
+ .classifier-panel {
345
+ margin-top: 0.9rem;
346
+ border: 1px solid var(--line);
347
+ border-radius: 10px;
348
+ padding: 0.65rem;
349
+ background: #fff;
350
+ }
351
+
352
+ .classifier-panel h3 {
353
+ margin: 0 0 0.5rem;
354
+ font-size: 0.98rem;
355
+ }
356
+
357
+ #classifierCanvas {
358
+ width: 100%;
359
+ height: auto;
360
+ border: 1px solid var(--line);
361
+ border-radius: 10px;
362
+ background: #fff;
363
+ }
364
+
365
  .page-copy {
366
  max-width: 760px;
367
  margin: 1.2rem auto;
 
372
  .layout {
373
  grid-template-columns: 1fr;
374
  }
375
+
376
+ .landscape-plot-wrap {
377
+ width: 100%;
378
+ height: 420px;
379
+ min-height: 280px;
380
+ }
381
  }
382
 
383
  .home-page {