abril4416 commited on
Commit
c745dff
·
1 Parent(s): 522ffd0

Add Gradio hub with linear and logistic regression interfaces

Browse files
app.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ import gradio as gr
4
+ from fastapi import FastAPI
5
+ from fastapi.staticfiles import StaticFiles
6
+
7
+ BASE_DIR = Path(__file__).resolve().parent
8
+
9
+ linear_dir = BASE_DIR / "linear-regression"
10
+ logistic_dir = BASE_DIR / "logistic-regression"
11
+
12
+ with gr.Blocks(title="DDW Machine Learning") as demo:
13
+ gr.Markdown(
14
+ """
15
+ # DDW Machine Learning
16
+
17
+ Choose one interface:
18
+ - **Linear Regression** (`week10/interface`)
19
+ - **Logistic Regression** (`week11/interface`)
20
+
21
+ Each interface opens with its own home page and keeps all original sub-page navigation.
22
+ """
23
+ )
24
+
25
+ with gr.Row():
26
+ gr.HTML(
27
+ """
28
+ <div style="border:1px solid #ddd;border-radius:12px;padding:16px;">
29
+ <h3>Linear Regression</h3>
30
+ <p>Week10 interface with NumPy Lab, Gradient Descent Studio, and Step Trainer.</p>
31
+ <a href="/linear-regression/index.html" target="_self">Open Linear Regression Interface</a>
32
+ </div>
33
+ """
34
+ )
35
+ gr.HTML(
36
+ """
37
+ <div style="border:1px solid #ddd;border-radius:12px;padding:16px;">
38
+ <h3>Logistic Regression</h3>
39
+ <p>Week11 interface with sigmoid, confusion matrix, and cost-function pages.</p>
40
+ <a href="/logistic-regression/index.html" target="_self">Open Logistic Regression Interface</a>
41
+ </div>
42
+ """
43
+ )
44
+
45
+ app = FastAPI()
46
+
47
+ app.mount("/linear-regression", StaticFiles(directory=str(linear_dir), html=True), name="linear-regression")
48
+ app.mount("/logistic-regression", StaticFiles(directory=str(logistic_dir), html=True), name="logistic-regression")
49
+ app = gr.mount_gradio_app(app, demo, path="/")
50
+
51
+
52
+ if __name__ == "__main__":
53
+ import uvicorn
54
+
55
+ uvicorn.run(app, host="0.0.0.0", port=7860)
linear-regression/app.js ADDED
@@ -0,0 +1,1326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const operations = [
2
+ {
3
+ id: "add",
4
+ label: "Add two matrices (element-wise)",
5
+ description: "A + B. Shapes of A and B should be equal or broadcast-compatible.",
6
+ inputs: 2,
7
+ code: "output = A + B",
8
+ match: ["add", "sum", "plus", "element-wise add"],
9
+ run: (A, B) => elementWise(A, B, (a, b) => a + b, "add"),
10
+ },
11
+ {
12
+ id: "subtract",
13
+ label: "Subtract two matrices (element-wise)",
14
+ description: "A - B. Shapes of A and B should be equal or broadcast-compatible.",
15
+ inputs: 2,
16
+ code: "output = A - B",
17
+ match: ["subtract", "minus", "difference"],
18
+ run: (A, B) => elementWise(A, B, (a, b) => a - b, "subtract"),
19
+ },
20
+ {
21
+ id: "multiply",
22
+ label: "Multiply two matrices (element-wise)",
23
+ description: "A * B. Shapes of A and B should be equal or broadcast-compatible.",
24
+ inputs: 2,
25
+ code: "output = A * B",
26
+ match: ["multiply", "times", "element-wise multiply"],
27
+ run: (A, B) => elementWise(A, B, (a, b) => a * b, "multiply"),
28
+ },
29
+ {
30
+ id: "matmul",
31
+ label: "Matrix multiplication (np.matmul)",
32
+ description: "np.matmul(A, B). Works for 2D and stacked arrays (up to 3D here).",
33
+ inputs: 2,
34
+ code: "output = np.matmul(A, B)",
35
+ match: ["matmul", "matrix multiplication", "dot product", "@"],
36
+ run: (A, B) => matmul(A, B),
37
+ },
38
+ {
39
+ id: "transpose",
40
+ label: "Transpose matrix (np.transpose)",
41
+ description: "Swap axes. For 2D, rows and columns are flipped.",
42
+ inputs: 1,
43
+ code: "output = np.transpose(A)",
44
+ match: ["transpose", "swap axes", "flip rows columns"],
45
+ run: (A) => transpose(A),
46
+ },
47
+ {
48
+ id: "reshape",
49
+ label: "Reshape matrix (np.reshape)",
50
+ description: "Reshape A into the target shape with same total number of elements.",
51
+ inputs: 1,
52
+ code: "output = np.reshape(A, new_shape)",
53
+ match: ["reshape", "change shape"],
54
+ requiresTarget: true,
55
+ run: (A, _unused, targetShape) => reshape(A, targetShape),
56
+ },
57
+ {
58
+ id: "concat",
59
+ label: "Concatenate two arrays (np.concatenate)",
60
+ description: "Join A and B along axis 0. Non-concat dimensions must match.",
61
+ inputs: 2,
62
+ code: "output = np.concatenate([A, B], axis=0)",
63
+ match: ["concatenate", "concat", "join arrays"],
64
+ run: (A, B) => concatAxis0(A, B),
65
+ },
66
+ {
67
+ id: "stack",
68
+ label: "Stack two arrays (np.stack)",
69
+ description: "Stack A and B along a new axis 0. A and B shapes must be identical.",
70
+ inputs: 2,
71
+ code: "output = np.stack([A, B], axis=0)",
72
+ match: ["stack", "new axis"],
73
+ run: (A, B) => stackAxis0(A, B),
74
+ },
75
+ {
76
+ id: "sum",
77
+ label: "Sum matrix values (np.sum)",
78
+ description: "Sum values on an optional axis with optional keepdims.",
79
+ inputs: 1,
80
+ code: "output = np.sum(A)",
81
+ match: ["sum all", "total", "np.sum", "sum"],
82
+ supportsReductionOptions: true,
83
+ run: (A, _unused, _unused2, options) =>
84
+ reduceArray(A, {
85
+ mode: "sum",
86
+ axis: options.axis,
87
+ keepdims: options.keepdims,
88
+ }),
89
+ },
90
+ {
91
+ id: "mean",
92
+ label: "Mean matrix values (np.mean)",
93
+ description: "Compute mean on an optional axis with optional keepdims.",
94
+ inputs: 1,
95
+ code: "output = np.mean(A)",
96
+ match: ["mean", "average", "np.mean"],
97
+ supportsReductionOptions: true,
98
+ run: (A, _unused, _unused2, options) =>
99
+ reduceArray(A, {
100
+ mode: "mean",
101
+ axis: options.axis,
102
+ keepdims: options.keepdims,
103
+ }),
104
+ },
105
+ {
106
+ id: "ones",
107
+ label: "Create ones matrix (np.ones)",
108
+ description: "Generate an array filled with ones for the given shape.",
109
+ inputs: 1,
110
+ code: "output = np.ones(shape)",
111
+ match: ["ones", "all ones", "np.ones"],
112
+ generatorOnly: true,
113
+ run: (A) => clone(A),
114
+ },
115
+ {
116
+ id: "zeros",
117
+ label: "Create zeros matrix (np.zeros)",
118
+ description: "Generate an array filled with zeros for the given shape.",
119
+ inputs: 1,
120
+ code: "output = np.zeros(shape)",
121
+ match: ["zeros", "all zeros", "np.zeros"],
122
+ generatorOnly: true,
123
+ run: (A) => clone(A),
124
+ },
125
+ ];
126
+
127
+ const operationSelect = document.getElementById("operationSelect");
128
+ const nlInput = document.getElementById("nlInput");
129
+ const matchBtn = document.getElementById("matchBtn");
130
+ const runBtn = document.getElementById("runBtn");
131
+ const operationInfo = document.getElementById("operationInfo");
132
+ const shapeInputs = document.getElementById("shapeInputs");
133
+ const operationOptions = document.getElementById("operationOptions");
134
+ const codeOutput = document.getElementById("codeOutput");
135
+ const inputViz = document.getElementById("inputViz");
136
+ const outputViz = document.getElementById("outputViz");
137
+ const detailViz = document.getElementById("detailViz");
138
+ let lastRunMeta = null;
139
+
140
+ function init() {
141
+ operations.forEach((op) => {
142
+ const option = document.createElement("option");
143
+ option.value = op.id;
144
+ option.textContent = op.label;
145
+ operationSelect.appendChild(option);
146
+ });
147
+
148
+ operationSelect.value = "add";
149
+ renderShapeInputs();
150
+ bindEvents();
151
+ }
152
+
153
+ function bindEvents() {
154
+ operationSelect.addEventListener("change", renderShapeInputs);
155
+
156
+ matchBtn.addEventListener("click", () => {
157
+ const query = nlInput.value.trim().toLowerCase();
158
+ if (!query) return;
159
+
160
+ let best = operations[0];
161
+ let bestScore = 0;
162
+
163
+ for (const op of operations) {
164
+ let score = 0;
165
+ for (const key of op.match) {
166
+ if (query.includes(key)) score += key.length;
167
+ }
168
+ if (score > bestScore) {
169
+ best = op;
170
+ bestScore = score;
171
+ }
172
+ }
173
+
174
+ operationSelect.value = best.id;
175
+ renderShapeInputs();
176
+ });
177
+
178
+ runBtn.addEventListener("click", () => {
179
+ try {
180
+ lastRunMeta = null;
181
+ const op = currentOperation();
182
+ const parsed = parseAllShapes(op);
183
+ const inputArrays = buildInputArrays(op, parsed);
184
+ const options = parsed.options || {};
185
+ const output = op.run(
186
+ inputArrays[0],
187
+ inputArrays[1],
188
+ parsed.targetShape || null,
189
+ options
190
+ );
191
+
192
+ renderCode(op, parsed);
193
+ renderInputs(inputArrays, op);
194
+ renderOutput(output);
195
+ renderComputationDetails(op, inputArrays, output, parsed, lastRunMeta);
196
+ } catch (err) {
197
+ codeOutput.textContent = "Error: " + err.message;
198
+ inputViz.innerHTML = "";
199
+ outputViz.innerHTML = `<p class=\"error\">${escapeHtml(err.message)}</p>`;
200
+ detailViz.innerHTML = "";
201
+ }
202
+ });
203
+ }
204
+
205
+ function currentOperation() {
206
+ return operations.find((op) => op.id === operationSelect.value);
207
+ }
208
+
209
+ function renderShapeInputs() {
210
+ const op = currentOperation();
211
+
212
+ operationInfo.innerHTML = `<strong>${op.label}</strong><br/>${escapeHtml(op.description)}`;
213
+
214
+ const cards = [];
215
+ for (let i = 0; i < op.inputs; i += 1) {
216
+ cards.push(shapeCard(`inputShape${i + 1}`, `Input ${i + 1} shape`, "2,3"));
217
+ }
218
+
219
+ if (op.requiresTarget) {
220
+ cards.push(shapeCard("targetShape", "Target shape", "3,2"));
221
+ }
222
+
223
+ shapeInputs.innerHTML = cards.join("");
224
+ operationOptions.innerHTML = op.supportsReductionOptions
225
+ ? reductionOptionsCard()
226
+ : "";
227
+
228
+ const s1 = document.getElementById("inputShape1");
229
+ const s2 = document.getElementById("inputShape2");
230
+
231
+ if (op.id === "matmul") {
232
+ s1.value = "2,3";
233
+ if (s2) s2.value = "3,2";
234
+ } else if (op.id === "concat" || op.id === "stack") {
235
+ s1.value = "2,2";
236
+ if (s2) s2.value = "2,2";
237
+ } else if (op.id === "transpose") {
238
+ s1.value = "2,3";
239
+ } else if (op.id === "reshape") {
240
+ s1.value = "2,3";
241
+ document.getElementById("targetShape").value = "3,2";
242
+ } else if (op.id === "ones" || op.id === "zeros") {
243
+ s1.value = "3,3";
244
+ } else if (op.id === "sum" || op.id === "mean") {
245
+ s1.value = "2,3";
246
+ }
247
+
248
+ if (op.supportsReductionOptions) {
249
+ const axisInput = document.getElementById("reduceAxis");
250
+ const keepdimsInput = document.getElementById("reduceKeepdims");
251
+
252
+ const setAxisHint = () => {
253
+ try {
254
+ const dims = parseShape(document.getElementById("inputShape1").value);
255
+ axisInput.placeholder = `axis (optional): 0 to ${dims.length - 1}`;
256
+ } catch (_err) {
257
+ axisInput.placeholder = "axis (optional): 0";
258
+ }
259
+ };
260
+
261
+ document.getElementById("inputShape1").addEventListener("input", setAxisHint);
262
+ setAxisHint();
263
+ keepdimsInput.checked = false;
264
+ }
265
+
266
+ detailViz.innerHTML = "";
267
+ }
268
+
269
+ function shapeCard(inputId, label, placeholder) {
270
+ return `
271
+ <div class="shape-card">
272
+ <h3>${label}</h3>
273
+ <input id="${inputId}" type="text" placeholder="${placeholder}" />
274
+ <small>Use comma-separated dimensions, max 3D, e.g. 2,3 or 2,2,3</small>
275
+ </div>
276
+ `;
277
+ }
278
+
279
+ function reductionOptionsCard() {
280
+ return `
281
+ <div class="shape-card options-card">
282
+ <h3>Reduction options</h3>
283
+ <div class="inline-fields">
284
+ <label class="mini-label" for="reduceAxis">axis</label>
285
+ <input id="reduceAxis" type="text" placeholder="axis (optional): 0" />
286
+ </div>
287
+ <div class="inline-fields">
288
+ <label class="mini-label" for="reduceKeepdims">keepdims</label>
289
+ <input id="reduceKeepdims" type="checkbox" />
290
+ </div>
291
+ <small>Leave axis empty to reduce all dimensions. keepdims keeps reduced axes as size 1.</small>
292
+ </div>
293
+ `;
294
+ }
295
+
296
+ function parseAllShapes(op) {
297
+ const shapes = [];
298
+
299
+ for (let i = 0; i < op.inputs; i += 1) {
300
+ const input = document.getElementById(`inputShape${i + 1}`);
301
+ shapes.push(parseShape(input.value));
302
+ }
303
+
304
+ const parsed = { shapes };
305
+
306
+ if (op.requiresTarget) {
307
+ parsed.targetShape = parseShape(document.getElementById("targetShape").value);
308
+ }
309
+
310
+ if (op.supportsReductionOptions) {
311
+ const axisRaw = document.getElementById("reduceAxis").value.trim();
312
+ const keepdims = document.getElementById("reduceKeepdims").checked;
313
+ let axis = null;
314
+
315
+ if (axisRaw !== "") {
316
+ axis = Number(axisRaw);
317
+ if (!Number.isInteger(axis)) {
318
+ throw new Error("axis must be an integer or left empty.");
319
+ }
320
+ if (axis < 0 || axis >= shapes[0].length) {
321
+ throw new Error(`axis out of range for input rank ${shapes[0].length}.`);
322
+ }
323
+ }
324
+
325
+ parsed.options = { axis, keepdims };
326
+ }
327
+
328
+ return parsed;
329
+ }
330
+
331
+ function parseShape(raw) {
332
+ if (!raw || !raw.trim()) {
333
+ throw new Error("Shape cannot be empty.");
334
+ }
335
+
336
+ let normalized = raw.trim();
337
+ if (normalized.startsWith("(") && normalized.endsWith(")")) {
338
+ normalized = normalized.slice(1, -1);
339
+ }
340
+ normalized = normalized.trim();
341
+
342
+ const dims = normalized
343
+ .split(",")
344
+ .map((x) => x.trim())
345
+ .filter((x) => x.length > 0)
346
+ .map((x) => Number(x));
347
+
348
+ if (dims.length < 1 || dims.length > 3) {
349
+ throw new Error("Each shape must have 1 to 3 dimensions.");
350
+ }
351
+
352
+ dims.forEach((d) => {
353
+ if (!Number.isFinite(d) || !Number.isInteger(d) || d < 1 || d > 6) {
354
+ throw new Error("Dimensions must be integers between 1 and 6.");
355
+ }
356
+ });
357
+
358
+ return dims;
359
+ }
360
+
361
+ function buildInputArrays(op, parsed) {
362
+ if (op.id === "ones") {
363
+ return [fillArray(parsed.shapes[0], 1)];
364
+ }
365
+ if (op.id === "zeros") {
366
+ return [fillArray(parsed.shapes[0], 0)];
367
+ }
368
+
369
+ const [shapeA, shapeB] = parsed.shapes;
370
+ const A = randomArray(shapeA);
371
+ if (op.inputs === 1) return [A];
372
+
373
+ let B;
374
+ if (["add", "subtract", "multiply"].includes(op.id)) {
375
+ B = randomArray(shapeB || shapeA);
376
+ } else if (op.id === "matmul") {
377
+ B = randomArray(shapeB);
378
+ } else if (op.id === "concat" || op.id === "stack") {
379
+ B = randomArray(shapeB);
380
+ } else {
381
+ B = randomArray(shapeB || shapeA);
382
+ }
383
+
384
+ return [A, B];
385
+ }
386
+
387
+ function randomArray(shape) {
388
+ return createByShape(shape, () => Math.floor(Math.random() * 9) + 1);
389
+ }
390
+
391
+ function fillArray(shape, val) {
392
+ return createByShape(shape, () => val);
393
+ }
394
+
395
+ function createByShape(shape, valueFn, level = 0) {
396
+ const len = shape[level];
397
+ const arr = new Array(len);
398
+
399
+ for (let i = 0; i < len; i += 1) {
400
+ arr[i] =
401
+ level === shape.length - 1
402
+ ? valueFn()
403
+ : createByShape(shape, valueFn, level + 1);
404
+ }
405
+
406
+ return arr;
407
+ }
408
+
409
+ function shapeOf(arr) {
410
+ if (!Array.isArray(arr)) return [];
411
+ return [arr.length, ...shapeOf(arr[0])];
412
+ }
413
+
414
+ function formatShape(shape) {
415
+ return `(${shape.join(", ")})`;
416
+ }
417
+
418
+ function broadcastShapes(shapeA, shapeB) {
419
+ const maxRank = Math.max(shapeA.length, shapeB.length);
420
+ const out = new Array(maxRank);
421
+
422
+ for (let i = 0; i < maxRank; i += 1) {
423
+ const a = shapeA[shapeA.length - 1 - i] ?? 1;
424
+ const b = shapeB[shapeB.length - 1 - i] ?? 1;
425
+
426
+ if (a !== b && a !== 1 && b !== 1) {
427
+ throw new Error(
428
+ `Broadcast mismatch at dimension ${maxRank - i - 1}: ${a} vs ${b}.`
429
+ );
430
+ }
431
+ out[maxRank - 1 - i] = Math.max(a, b);
432
+ }
433
+
434
+ return out;
435
+ }
436
+
437
+ function getAtIndices(arr, indices) {
438
+ let cur = arr;
439
+ for (let i = 0; i < indices.length; i += 1) {
440
+ cur = cur[indices[i]];
441
+ }
442
+ return cur;
443
+ }
444
+
445
+ function createByShapeIndexed(shape, valueFn, idx = []) {
446
+ if (shape.length === 0) return valueFn(idx);
447
+ const dim = shape[idx.length];
448
+ const out = new Array(dim);
449
+ for (let i = 0; i < dim; i += 1) {
450
+ const nextIdx = idx.concat(i);
451
+ if (nextIdx.length === shape.length) {
452
+ out[i] = valueFn(nextIdx);
453
+ } else {
454
+ out[i] = createByShapeIndexed(shape, valueFn, nextIdx);
455
+ }
456
+ }
457
+ return out;
458
+ }
459
+
460
+ function projectBroadcastIndices(outputIndices, sourceShape) {
461
+ const offset = outputIndices.length - sourceShape.length;
462
+ const mapped = [];
463
+ for (let i = 0; i < sourceShape.length; i += 1) {
464
+ const srcDim = sourceShape[i];
465
+ const outIndex = outputIndices[offset + i];
466
+ mapped.push(srcDim === 1 ? 0 : outIndex);
467
+ }
468
+ return mapped;
469
+ }
470
+
471
+ function elementWise(A, B, fn, opName) {
472
+ const shapeA = shapeOf(A);
473
+ const shapeB = shapeOf(B);
474
+ const outputShape = broadcastShapes(shapeA, shapeB);
475
+
476
+ const output = createByShapeIndexed(outputShape, (outIdx) => {
477
+ const idxA = projectBroadcastIndices(outIdx, shapeA);
478
+ const idxB = projectBroadcastIndices(outIdx, shapeB);
479
+ return fn(getAtIndices(A, idxA), getAtIndices(B, idxB));
480
+ });
481
+
482
+ const usedBroadcasting =
483
+ shapeA.length !== shapeB.length ||
484
+ shapeA.some((dim, i) => dim !== shapeB[i]) ||
485
+ shapeA.join(",") !== outputShape.join(",") ||
486
+ shapeB.join(",") !== outputShape.join(",");
487
+
488
+ lastRunMeta = {
489
+ kind: "elementwise",
490
+ opName,
491
+ shapeA,
492
+ shapeB,
493
+ outputShape,
494
+ usedBroadcasting,
495
+ };
496
+
497
+ return output;
498
+ }
499
+
500
+ function elementWiseDeep(A, B, fn) {
501
+ if (!Array.isArray(A) && !Array.isArray(B)) return fn(A, B);
502
+ return A.map((v, i) => elementWiseDeep(v, B[i], fn));
503
+ }
504
+
505
+ function transpose(A) {
506
+ const shape = shapeOf(A);
507
+ if (shape.length === 1) return clone(A);
508
+
509
+ if (shape.length === 2) {
510
+ const [rows, cols] = shape;
511
+ const out = [];
512
+ for (let c = 0; c < cols; c += 1) {
513
+ const row = [];
514
+ for (let r = 0; r < rows; r += 1) {
515
+ row.push(A[r][c]);
516
+ }
517
+ out.push(row);
518
+ }
519
+ return out;
520
+ }
521
+
522
+ if (shape.length === 3) {
523
+ const [d0, d1, d2] = shape;
524
+ const out = [];
525
+ for (let i = 0; i < d2; i += 1) {
526
+ const level2 = [];
527
+ for (let j = 0; j < d1; j += 1) {
528
+ const row = [];
529
+ for (let k = 0; k < d0; k += 1) {
530
+ row.push(A[k][j][i]);
531
+ }
532
+ level2.push(row);
533
+ }
534
+ out.push(level2);
535
+ }
536
+ return out;
537
+ }
538
+
539
+ throw new Error("Transpose supports up to 3D in this interface.");
540
+ }
541
+
542
+ function reshape(A, targetShape) {
543
+ const flat = flatten(A);
544
+ const totalA = flat.length;
545
+ const totalTarget = targetShape.reduce((x, y) => x * y, 1);
546
+
547
+ if (totalA !== totalTarget) {
548
+ throw new Error(
549
+ `reshape needs same number of elements. Got ${totalA} and ${totalTarget}.`
550
+ );
551
+ }
552
+
553
+ return unflatten(flat, targetShape);
554
+ }
555
+
556
+ function flatten(arr) {
557
+ if (!Array.isArray(arr)) return [arr];
558
+ return arr.flatMap((x) => flatten(x));
559
+ }
560
+
561
+ function unflatten(flat, shape) {
562
+ let idx = 0;
563
+ function build(level = 0) {
564
+ const len = shape[level];
565
+ const out = [];
566
+ for (let i = 0; i < len; i += 1) {
567
+ if (level === shape.length - 1) {
568
+ out.push(flat[idx]);
569
+ idx += 1;
570
+ } else {
571
+ out.push(build(level + 1));
572
+ }
573
+ }
574
+ return out;
575
+ }
576
+ return build();
577
+ }
578
+
579
+ function matmul(A, B) {
580
+ const sA = shapeOf(A);
581
+ const sB = shapeOf(B);
582
+ const leftVec = sA.length === 1;
583
+ const rightVec = sB.length === 1;
584
+
585
+ const leftBatch = sA.length === 3 ? sA[0] : 1;
586
+ const rightBatch = sB.length === 3 ? sB[0] : 1;
587
+ const outBatch = Math.max(leftBatch, rightBatch);
588
+
589
+ if (leftBatch !== rightBatch && leftBatch !== 1 && rightBatch !== 1) {
590
+ throw new Error(
591
+ `matmul batch broadcast mismatch: ${leftBatch} vs ${rightBatch}.`
592
+ );
593
+ }
594
+
595
+ const leftRows = leftVec ? 1 : sA[sA.length - 2];
596
+ const leftInner = sA[sA.length - 1];
597
+ const rightInner = rightVec ? sB[0] : sB[sB.length - 2];
598
+ const rightCols = rightVec ? 1 : sB[sB.length - 1];
599
+
600
+ if (leftInner !== rightInner) {
601
+ throw new Error(
602
+ `matmul shape mismatch on core dims: ${formatShape(sA)} @ ${formatShape(
603
+ sB
604
+ )} (inner ${leftInner} vs ${rightInner}).`
605
+ );
606
+ }
607
+
608
+ const getLeft = (batch, row, k) => {
609
+ if (leftVec) return A[k];
610
+ if (sA.length === 2) return A[row][k];
611
+ const batchIdx = leftBatch === 1 ? 0 : batch;
612
+ return A[batchIdx][row][k];
613
+ };
614
+
615
+ const getRight = (batch, k, col) => {
616
+ if (rightVec) return B[k];
617
+ if (sB.length === 2) return B[k][col];
618
+ const batchIdx = rightBatch === 1 ? 0 : batch;
619
+ return B[batchIdx][k][col];
620
+ };
621
+
622
+ const matrixForBatch = (batch) => {
623
+ const out = [];
624
+ for (let r = 0; r < leftRows; r += 1) {
625
+ const row = [];
626
+ for (let c = 0; c < rightCols; c += 1) {
627
+ let sum = 0;
628
+ for (let k = 0; k < leftInner; k += 1) {
629
+ sum += getLeft(batch, r, k) * getRight(batch, k, c);
630
+ }
631
+ row.push(sum);
632
+ }
633
+ out.push(row);
634
+ }
635
+ return out;
636
+ };
637
+
638
+ const hasBatchAxis = sA.length > 2 || sB.length > 2;
639
+ const matrices = hasBatchAxis
640
+ ? Array.from({ length: outBatch }, (_, batch) => matrixForBatch(batch))
641
+ : [matrixForBatch(0)];
642
+
643
+ let output;
644
+ if (leftVec && rightVec) {
645
+ output = hasBatchAxis ? matrices.map((m) => m[0][0]) : matrices[0][0][0];
646
+ } else if (leftVec) {
647
+ output = hasBatchAxis ? matrices.map((m) => m[0].slice()) : matrices[0][0].slice();
648
+ } else if (rightVec) {
649
+ output = hasBatchAxis
650
+ ? matrices.map((m) => m.map((row) => row[0]))
651
+ : matrices[0].map((row) => row[0]);
652
+ } else {
653
+ output = hasBatchAxis ? matrices : matrices[0];
654
+ }
655
+
656
+ const outputShape = shapeOf(output);
657
+ lastRunMeta = {
658
+ kind: "matmul",
659
+ shapeA: sA,
660
+ shapeB: sB,
661
+ outputShape,
662
+ leftBatch,
663
+ rightBatch,
664
+ outBatch,
665
+ usedBroadcasting:
666
+ leftBatch !== rightBatch || sA.length !== sB.length || leftVec || rightVec,
667
+ leftVectorPromoted: leftVec,
668
+ rightVectorPromoted: rightVec,
669
+ };
670
+
671
+ return output;
672
+ }
673
+
674
+ function matmul2D(A, B) {
675
+ const rowsA = A.length;
676
+ const colsA = A[0].length;
677
+ const rowsB = B.length;
678
+ const colsB = B[0].length;
679
+
680
+ if (colsA !== rowsB) {
681
+ throw new Error(
682
+ `matmul shape mismatch: (${rowsA},${colsA}) x (${rowsB},${colsB})`
683
+ );
684
+ }
685
+
686
+ const out = [];
687
+ for (let r = 0; r < rowsA; r += 1) {
688
+ const row = [];
689
+ for (let c = 0; c < colsB; c += 1) {
690
+ let sum = 0;
691
+ for (let k = 0; k < colsA; k += 1) {
692
+ sum += A[r][k] * B[k][c];
693
+ }
694
+ row.push(sum);
695
+ }
696
+ out.push(row);
697
+ }
698
+ return out;
699
+ }
700
+
701
+ function concatAxis0(A, B) {
702
+ const sA = shapeOf(A);
703
+ const sB = shapeOf(B);
704
+ if (sA.length !== sB.length) {
705
+ throw new Error("concatenate requires same rank.");
706
+ }
707
+ for (let i = 1; i < sA.length; i += 1) {
708
+ if (sA[i] !== sB[i]) {
709
+ throw new Error(
710
+ "concatenate axis=0 requires other dimensions to be identical."
711
+ );
712
+ }
713
+ }
714
+ return [...clone(A), ...clone(B)];
715
+ }
716
+
717
+ function stackAxis0(A, B) {
718
+ const sA = JSON.stringify(shapeOf(A));
719
+ const sB = JSON.stringify(shapeOf(B));
720
+ if (sA !== sB) {
721
+ throw new Error("stack requires A and B to have the same shape.");
722
+ }
723
+ return [clone(A), clone(B)];
724
+ }
725
+
726
+ function reduceArray(A, { mode, axis, keepdims }) {
727
+ const rank = shapeOf(A).length;
728
+
729
+ if (axis === null) {
730
+ const flat = flatten(A);
731
+ let scalar;
732
+ if (mode === "sum") {
733
+ scalar = flat.reduce((acc, x) => acc + x, 0);
734
+ } else {
735
+ scalar = Number(
736
+ (flat.reduce((acc, x) => acc + x, 0) / flat.length).toFixed(4)
737
+ );
738
+ }
739
+
740
+ if (!keepdims) return scalar;
741
+
742
+ let wrapped = scalar;
743
+ for (let i = 0; i < rank; i += 1) wrapped = [wrapped];
744
+ return wrapped;
745
+ }
746
+
747
+ const reduced = reduceAlongAxis(A, axis, mode);
748
+ if (!keepdims) return reduced;
749
+ return insertAxisDimension(reduced, axis);
750
+ }
751
+
752
+ function reduceAlongAxis(arr, axis, mode) {
753
+ if (axis === 0) {
754
+ if (arr.length === 0) throw new Error("Cannot reduce empty array.");
755
+ let accum = clone(arr[0]);
756
+ for (let i = 1; i < arr.length; i += 1) {
757
+ accum = elementWiseDeep(accum, arr[i], (a, b) => a + b);
758
+ }
759
+
760
+ if (mode === "sum") return accum;
761
+ return elementWiseDeep(accum, accum, (a) => Number((a / arr.length).toFixed(4)));
762
+ }
763
+
764
+ return arr.map((sub) => reduceAlongAxis(sub, axis - 1, mode));
765
+ }
766
+
767
+ function insertAxisDimension(value, axis) {
768
+ if (axis === 0) return [value];
769
+ if (!Array.isArray(value)) return [value];
770
+ return value.map((v) => insertAxisDimension(v, axis - 1));
771
+ }
772
+
773
+ function clone(value) {
774
+ return JSON.parse(JSON.stringify(value));
775
+ }
776
+
777
+ function renderCode(op, parsed) {
778
+ const lines = ["import numpy as np", ""];
779
+
780
+ if (op.id === "ones" || op.id === "zeros") {
781
+ lines.push(`shape = (${parsed.shapes[0].join(", ")})`);
782
+ } else {
783
+ lines.push(`A = np.random.randint(1, 10, size=(${parsed.shapes[0].join(", ")}))`);
784
+ if (op.inputs === 2) {
785
+ lines.push(
786
+ `B = np.random.randint(1, 10, size=(${parsed.shapes[1].join(", ")}))`
787
+ );
788
+ }
789
+ if (op.requiresTarget) {
790
+ lines.push(`new_shape = (${parsed.targetShape.join(", ")})`);
791
+ }
792
+ }
793
+
794
+ if (op.supportsReductionOptions) {
795
+ const axisPart = parsed.options.axis === null ? "None" : parsed.options.axis;
796
+ lines.push(
797
+ `output = np.${op.id}(A, axis=${axisPart}, keepdims=${parsed.options.keepdims})`
798
+ );
799
+ } else {
800
+ lines.push(op.code);
801
+ }
802
+
803
+ lines.push("print(output)");
804
+
805
+ codeOutput.textContent = lines.join("\n");
806
+ }
807
+
808
+ function renderInputs(arrays, op) {
809
+ inputViz.innerHTML = "";
810
+
811
+ if (op.id === "ones" || op.id === "zeros") {
812
+ inputViz.appendChild(
813
+ buildMatrixCard("Generated Array", arrays[0], "from requested shape")
814
+ );
815
+ return;
816
+ }
817
+
818
+ arrays.forEach((arr, i) => {
819
+ inputViz.appendChild(buildMatrixCard(`Input ${i + 1}`, arr));
820
+ });
821
+ }
822
+
823
+ function renderOutput(output) {
824
+ outputViz.innerHTML = "";
825
+ outputViz.appendChild(buildMatrixCard("Output", output));
826
+ }
827
+
828
+ function renderComputationDetails(op, inputArrays, output, parsed, meta) {
829
+ detailViz.innerHTML = "";
830
+
831
+ if (meta && (meta.kind === "elementwise" || meta.kind === "matmul")) {
832
+ detailViz.appendChild(buildBroadcastUsageCard(meta));
833
+ }
834
+
835
+ if (op.id === "matmul") {
836
+ detailViz.appendChild(
837
+ buildMatmulDetail(inputArrays[0], inputArrays[1], output, meta)
838
+ );
839
+ } else if (op.id === "reshape") {
840
+ detailViz.appendChild(buildReshapeDetail(inputArrays[0], parsed.targetShape));
841
+ } else if (op.supportsReductionOptions) {
842
+ detailViz.appendChild(buildReductionDetail(op, inputArrays[0], parsed.options, output));
843
+ } else {
844
+ const note = document.createElement("p");
845
+ note.className = "slice-label";
846
+ note.textContent = "No extra computation walkthrough for this operation yet.";
847
+ detailViz.appendChild(note);
848
+ }
849
+ }
850
+
851
+ function buildBroadcastUsageCard(meta) {
852
+ const card = document.createElement("article");
853
+ card.className = "matrix-card";
854
+
855
+ const title = document.createElement("p");
856
+ title.className = "matrix-title";
857
+ title.textContent = meta.usedBroadcasting
858
+ ? "Broadcasting detected"
859
+ : "No broadcasting needed";
860
+ card.appendChild(title);
861
+
862
+ const line1 = document.createElement("p");
863
+ line1.className = "formula-line";
864
+ line1.textContent = `Input shapes: A${formatShape(meta.shapeA)}, B${formatShape(
865
+ meta.shapeB
866
+ )}`;
867
+ card.appendChild(line1);
868
+
869
+ const line2 = document.createElement("p");
870
+ line2.className = "formula-line";
871
+ line2.textContent = `Output shape: ${formatShape(meta.outputShape)}`;
872
+ card.appendChild(line2);
873
+
874
+ const explain = document.createElement("p");
875
+ explain.className = "slice-label";
876
+
877
+ if (!meta.usedBroadcasting) {
878
+ explain.textContent = "Inputs already align directly; operation runs without dimension expansion.";
879
+ } else if (meta.kind === "elementwise") {
880
+ explain.textContent =
881
+ "Element-wise broadcasting aligns dimensions from the right. Any dimension with size 1 is repeated to match the other input.";
882
+ } else {
883
+ explain.textContent =
884
+ "For np.matmul, only batch dimensions are broadcast; core matrix dimensions still follow (..., m, k) @ (..., k, n).";
885
+ }
886
+ card.appendChild(explain);
887
+
888
+ return card;
889
+ }
890
+
891
+ function buildMatmulDetail(A, B, output, meta) {
892
+ const wrapper = document.createElement("article");
893
+ wrapper.className = "matrix-card";
894
+
895
+ const title = document.createElement("p");
896
+ title.className = "matrix-title";
897
+ title.textContent = "np.matmul interactive computation breakdown";
898
+ wrapper.appendChild(title);
899
+
900
+ const sA = shapeOf(A);
901
+ const sB = shapeOf(B);
902
+
903
+ if (sA.length === 2 && sB.length === 2) {
904
+ wrapper.appendChild(buildMatmulInteractive2D(A, B, output));
905
+ return wrapper;
906
+ }
907
+
908
+ if (sA.length === 3 && sB.length === 3 && sA[0] === sB[0]) {
909
+ const help = document.createElement("p");
910
+ help.className = "slice-label";
911
+ help.textContent =
912
+ "Select a batch and output cell to highlight A row, B column, and formula.";
913
+ wrapper.appendChild(help);
914
+
915
+ const controls = document.createElement("div");
916
+ controls.className = "inline-fields";
917
+ controls.innerHTML = `
918
+ <label class="mini-label" for="batchSelect">batch</label>
919
+ <select id="batchSelect"></select>
920
+ `;
921
+ wrapper.appendChild(controls);
922
+
923
+ const batchSelect = controls.querySelector("#batchSelect");
924
+ for (let i = 0; i < sA[0]; i += 1) {
925
+ const opt = document.createElement("option");
926
+ opt.value = String(i);
927
+ opt.textContent = `batch ${i}`;
928
+ batchSelect.appendChild(opt);
929
+ }
930
+
931
+ const host = document.createElement("div");
932
+ wrapper.appendChild(host);
933
+
934
+ const renderBatch = () => {
935
+ const idx = Number(batchSelect.value);
936
+ host.innerHTML = "";
937
+ const label = document.createElement("p");
938
+ label.className = "slice-label";
939
+ label.textContent = `output[${idx}] = A[${idx}] @ B[${idx}]`;
940
+ host.appendChild(label);
941
+ host.appendChild(buildMatmulInteractive2D(A[idx], B[idx], output[idx]));
942
+ };
943
+
944
+ batchSelect.addEventListener("change", renderBatch);
945
+ renderBatch();
946
+ return wrapper;
947
+ }
948
+
949
+ const mixedNote = document.createElement("p");
950
+ mixedNote.className = "slice-label";
951
+ mixedNote.textContent =
952
+ "Mixed-rank/broadcasted matmul: each output batch uses A_batch @ B_batch after NumPy batch broadcasting.";
953
+ wrapper.appendChild(mixedNote);
954
+
955
+ if (meta && meta.outBatch > 1) {
956
+ const mapLine = document.createElement("p");
957
+ mapLine.className = "formula-line";
958
+ mapLine.textContent = `Batch mapping: output batch i uses A[${meta.leftBatch === 1 ? "0" : "i"}] and B[${
959
+ meta.rightBatch === 1 ? "0" : "i"
960
+ }].`;
961
+ wrapper.appendChild(mapLine);
962
+ }
963
+
964
+ wrapper.appendChild(buildMatrixCard("Input A", A));
965
+ wrapper.appendChild(buildMatrixCard("Input B", B));
966
+ wrapper.appendChild(buildMatrixCard("Output", output));
967
+
968
+ return wrapper;
969
+ }
970
+
971
+ function buildMatmulInteractive2D(A, B, out) {
972
+ const box = document.createElement("div");
973
+ const helper = document.createElement("p");
974
+ helper.className = "slice-label";
975
+ helper.textContent =
976
+ "Click a value in the output matrix. The corresponding row/column will be highlighted.";
977
+ box.appendChild(helper);
978
+
979
+ const state = { row: 0, col: 0 };
980
+ const pickerHost = document.createElement("div");
981
+ const matrixHost = document.createElement("div");
982
+ matrixHost.className = "viz-grid";
983
+ const formulaLine = document.createElement("p");
984
+ formulaLine.className = "formula-line";
985
+
986
+ function renderPicker() {
987
+ pickerHost.innerHTML = "";
988
+ const table = document.createElement("table");
989
+ table.className = "matrix-table";
990
+
991
+ out.forEach((rowValues, r) => {
992
+ const tr = document.createElement("tr");
993
+ rowValues.forEach((value, c) => {
994
+ const td = document.createElement("td");
995
+ const btn = document.createElement("button");
996
+ btn.type = "button";
997
+ btn.className = "matrix-pick-btn";
998
+ if (r === state.row && c === state.col) btn.classList.add("active");
999
+ btn.textContent = String(value);
1000
+ btn.addEventListener("click", () => {
1001
+ state.row = r;
1002
+ state.col = c;
1003
+ renderAll();
1004
+ });
1005
+ td.appendChild(btn);
1006
+ tr.appendChild(td);
1007
+ });
1008
+ table.appendChild(tr);
1009
+ });
1010
+
1011
+ pickerHost.appendChild(table);
1012
+ }
1013
+
1014
+ function build2DCard(title, matrix, subtitle, highlight) {
1015
+ const card = document.createElement("article");
1016
+ card.className = "matrix-card";
1017
+
1018
+ const shape = shapeOf(matrix);
1019
+ const titleEl = document.createElement("p");
1020
+ titleEl.className = "matrix-title";
1021
+ titleEl.innerHTML = `${escapeHtml(title)} <span class="shape-badge">shape: (${shape.join(
1022
+ ", "
1023
+ )})</span>`;
1024
+ card.appendChild(titleEl);
1025
+
1026
+ const sub = document.createElement("p");
1027
+ sub.className = "slice-label";
1028
+ sub.textContent = subtitle;
1029
+ card.appendChild(sub);
1030
+
1031
+ card.appendChild(render2DTableWithHighlights(matrix, highlight));
1032
+ return card;
1033
+ }
1034
+
1035
+ function renderAll() {
1036
+ renderPicker();
1037
+ matrixHost.innerHTML = "";
1038
+ matrixHost.appendChild(
1039
+ build2DCard(
1040
+ "Input A",
1041
+ A,
1042
+ `highlighted row: ${state.row}`,
1043
+ { highlightRow: state.row }
1044
+ )
1045
+ );
1046
+ matrixHost.appendChild(
1047
+ build2DCard(
1048
+ "Input B",
1049
+ B,
1050
+ `highlighted column: ${state.col}`,
1051
+ { highlightCol: state.col }
1052
+ )
1053
+ );
1054
+ matrixHost.appendChild(
1055
+ build2DCard(
1056
+ "Output",
1057
+ out,
1058
+ `selected cell: [${state.row}, ${state.col}]`,
1059
+ { highlightCell: { row: state.row, col: state.col } }
1060
+ )
1061
+ );
1062
+
1063
+ const terms = [];
1064
+ const values = [];
1065
+ for (let k = 0; k < A[0].length; k += 1) {
1066
+ terms.push(`${A[state.row][k]}*${B[k][state.col]}`);
1067
+ values.push(A[state.row][k] * B[k][state.col]);
1068
+ }
1069
+ formulaLine.textContent = `output[${state.row}, ${state.col}] = ${terms.join(
1070
+ " + "
1071
+ )} = ${values.join(" + ")} = ${out[state.row][state.col]}`;
1072
+ }
1073
+
1074
+ box.appendChild(pickerHost);
1075
+ box.appendChild(formulaLine);
1076
+ box.appendChild(matrixHost);
1077
+ renderAll();
1078
+
1079
+ return box;
1080
+ }
1081
+
1082
+ function buildReshapeDetail(A, targetShape) {
1083
+ const wrapper = document.createElement("article");
1084
+ wrapper.className = "matrix-card";
1085
+
1086
+ const title = document.createElement("p");
1087
+ title.className = "matrix-title";
1088
+ title.textContent = "np.reshape step-by-step (dynamic)";
1089
+ wrapper.appendChild(title);
1090
+
1091
+ const flat = flatten(A);
1092
+ const total = flat.length;
1093
+
1094
+ const helper = document.createElement("p");
1095
+ helper.className = "slice-label";
1096
+ helper.textContent =
1097
+ "Use Play to animate reshaping; slider also works for manual inspection.";
1098
+ wrapper.appendChild(helper);
1099
+
1100
+ const controls = document.createElement("div");
1101
+ controls.className = "slider-row";
1102
+ controls.innerHTML = `
1103
+ <label for="reshapeStep">step</label>
1104
+ <input class="reshape-step" id="reshapeStep" type="range" min="0" max="${total}" value="0" />
1105
+ <span id="reshapeStepLabel">0 / ${total}</span>
1106
+ <button type="button" id="reshapePlayBtn">Play</button>
1107
+ <button type="button" id="reshapeResetBtn">Reset</button>
1108
+ `;
1109
+ wrapper.appendChild(controls);
1110
+
1111
+ const flatLine = document.createElement("p");
1112
+ flatLine.className = "formula-line";
1113
+ flatLine.textContent = `flat(A) = [${flat.join(", ")}]`;
1114
+ wrapper.appendChild(flatLine);
1115
+
1116
+ const previewHost = document.createElement("div");
1117
+ wrapper.appendChild(previewHost);
1118
+
1119
+ const slider = controls.querySelector("#reshapeStep");
1120
+ const stepLabel = controls.querySelector("#reshapeStepLabel");
1121
+ const playBtn = controls.querySelector("#reshapePlayBtn");
1122
+ const resetBtn = controls.querySelector("#reshapeResetBtn");
1123
+ let timer = null;
1124
+ let playing = false;
1125
+
1126
+ const renderStep = () => {
1127
+ const step = Number(slider.value);
1128
+ stepLabel.textContent = `${step} / ${total}`;
1129
+ const partialFlat = new Array(total).fill("·");
1130
+ for (let i = 0; i < step; i += 1) partialFlat[i] = flat[i];
1131
+ const partial = unflatten(partialFlat, targetShape);
1132
+
1133
+ previewHost.innerHTML = "";
1134
+ previewHost.appendChild(
1135
+ buildMatrixCard(
1136
+ "Current reshaped output",
1137
+ partial,
1138
+ `first ${step} element(s) assigned in row-major order`
1139
+ )
1140
+ );
1141
+ };
1142
+
1143
+ const stopAnimation = () => {
1144
+ if (timer) {
1145
+ clearInterval(timer);
1146
+ timer = null;
1147
+ }
1148
+ playing = false;
1149
+ playBtn.textContent = "Play";
1150
+ };
1151
+
1152
+ const startAnimation = () => {
1153
+ stopAnimation();
1154
+ playing = true;
1155
+ playBtn.textContent = "Pause";
1156
+ timer = setInterval(() => {
1157
+ if (!document.body.contains(wrapper)) {
1158
+ stopAnimation();
1159
+ return;
1160
+ }
1161
+ const current = Number(slider.value);
1162
+ if (current >= total) {
1163
+ stopAnimation();
1164
+ return;
1165
+ }
1166
+ slider.value = String(current + 1);
1167
+ renderStep();
1168
+ }, 500);
1169
+ };
1170
+
1171
+ playBtn.addEventListener("click", () => {
1172
+ if (playing) {
1173
+ stopAnimation();
1174
+ } else {
1175
+ startAnimation();
1176
+ }
1177
+ });
1178
+
1179
+ resetBtn.addEventListener("click", () => {
1180
+ stopAnimation();
1181
+ slider.value = "0";
1182
+ renderStep();
1183
+ });
1184
+
1185
+ slider.addEventListener("input", () => {
1186
+ if (playing) stopAnimation();
1187
+ renderStep();
1188
+ });
1189
+
1190
+ renderStep();
1191
+ startAnimation();
1192
+
1193
+ return wrapper;
1194
+ }
1195
+
1196
+ function buildReductionDetail(op, A, options, output) {
1197
+ const wrapper = document.createElement("article");
1198
+ wrapper.className = "matrix-card";
1199
+
1200
+ const title = document.createElement("p");
1201
+ title.className = "matrix-title";
1202
+ title.textContent = `np.${op.id} options summary`;
1203
+ wrapper.appendChild(title);
1204
+
1205
+ const summary = document.createElement("p");
1206
+ summary.className = "formula-line";
1207
+ summary.textContent = `axis=${options.axis === null ? "None" : options.axis}, keepdims=${options.keepdims}`;
1208
+ wrapper.appendChild(summary);
1209
+
1210
+ const explain = document.createElement("p");
1211
+ explain.className = "slice-label";
1212
+ explain.textContent =
1213
+ options.axis === null
1214
+ ? "Reducing over all dimensions."
1215
+ : `Reducing along axis ${options.axis}; values on that axis are aggregated.`;
1216
+ wrapper.appendChild(explain);
1217
+
1218
+ wrapper.appendChild(buildMatrixCard("Input A", A));
1219
+ wrapper.appendChild(buildMatrixCard("Reduced output", output));
1220
+
1221
+ return wrapper;
1222
+ }
1223
+
1224
+ function buildMatrixCard(title, arr, subtitle = "") {
1225
+ const card = document.createElement("article");
1226
+ card.className = "matrix-card";
1227
+
1228
+ const shape = shapeOf(arr);
1229
+ const titleEl = document.createElement("p");
1230
+ titleEl.className = "matrix-title";
1231
+ titleEl.innerHTML = `${escapeHtml(title)} <span class="shape-badge">shape: (${shape.join(
1232
+ ", "
1233
+ ) || "scalar"})</span>`;
1234
+
1235
+ card.appendChild(titleEl);
1236
+
1237
+ if (subtitle) {
1238
+ const sub = document.createElement("p");
1239
+ sub.className = "slice-label";
1240
+ sub.textContent = subtitle;
1241
+ card.appendChild(sub);
1242
+ }
1243
+
1244
+ if (!Array.isArray(arr)) {
1245
+ const scalar = document.createElement("p");
1246
+ scalar.textContent = String(arr);
1247
+ scalar.style.fontFamily = "Courier New, monospace";
1248
+ scalar.style.fontWeight = "700";
1249
+ card.appendChild(scalar);
1250
+ return card;
1251
+ }
1252
+
1253
+ const rank = shape.length;
1254
+
1255
+ if (rank === 1) {
1256
+ card.appendChild(render2DTable([arr]));
1257
+ } else if (rank === 2) {
1258
+ card.appendChild(render2DTable(arr));
1259
+ } else if (rank === 3) {
1260
+ arr.forEach((slice, idx) => {
1261
+ const lbl = document.createElement("p");
1262
+ lbl.className = "slice-label";
1263
+ lbl.textContent = `slice ${idx} (axis 0)`;
1264
+ card.appendChild(lbl);
1265
+ card.appendChild(render2DTable(slice));
1266
+ });
1267
+ }
1268
+
1269
+ return card;
1270
+ }
1271
+
1272
+ function render2DTable(matrix2d) {
1273
+ const table = document.createElement("table");
1274
+ table.className = "matrix-table";
1275
+
1276
+ matrix2d.forEach((row) => {
1277
+ const tr = document.createElement("tr");
1278
+ row.forEach((value) => {
1279
+ const td = document.createElement("td");
1280
+ td.textContent = String(value);
1281
+ tr.appendChild(td);
1282
+ });
1283
+ table.appendChild(tr);
1284
+ });
1285
+
1286
+ return table;
1287
+ }
1288
+
1289
+ function render2DTableWithHighlights(matrix2d, highlight = {}) {
1290
+ const table = document.createElement("table");
1291
+ table.className = "matrix-table";
1292
+
1293
+ matrix2d.forEach((row, r) => {
1294
+ const tr = document.createElement("tr");
1295
+ row.forEach((value, c) => {
1296
+ const td = document.createElement("td");
1297
+ td.textContent = String(value);
1298
+
1299
+ if (highlight.highlightRow === r) td.classList.add("hl-row");
1300
+ if (highlight.highlightCol === c) td.classList.add("hl-col");
1301
+ if (
1302
+ highlight.highlightCell &&
1303
+ highlight.highlightCell.row === r &&
1304
+ highlight.highlightCell.col === c
1305
+ ) {
1306
+ td.classList.add("hl-cell");
1307
+ }
1308
+
1309
+ tr.appendChild(td);
1310
+ });
1311
+ table.appendChild(tr);
1312
+ });
1313
+
1314
+ return table;
1315
+ }
1316
+
1317
+ function escapeHtml(text) {
1318
+ return String(text)
1319
+ .replaceAll("&", "&amp;")
1320
+ .replaceAll("<", "&lt;")
1321
+ .replaceAll(">", "&gt;")
1322
+ .replaceAll('"', "&quot;")
1323
+ .replaceAll("'", "&#39;");
1324
+ }
1325
+
1326
+ init();
linear-regression/gradient-descent.html ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Gradient Descent Studio</title>
7
+ <link rel="stylesheet" href="styles.css" />
8
+ </head>
9
+ <body>
10
+ <div class="bg-grid"></div>
11
+ <main class="container">
12
+ <nav class="top-nav">
13
+ <a href="index.html">Back Home</a>
14
+ </nav>
15
+
16
+ <header class="hero compact">
17
+ <p class="eyebrow">Page 2</p>
18
+ <h1>Gradient Descent Studio</h1>
19
+ <p>
20
+ Explore gradient descent for one-feature linear regression. Track
21
+ cost, parameters, and gradients at every step.
22
+ </p>
23
+ </header>
24
+
25
+ <section class="panel controls">
26
+ <div class="row two-col-row">
27
+ <div class="control-group">
28
+ <label for="sampleCount">Samples</label>
29
+ <input id="sampleCount" type="number" min="10" max="120" value="50" />
30
+ </div>
31
+ <div class="control-group">
32
+ <label for="learningRate">Learning Rate (alpha)</label>
33
+ <input id="learningRate" type="number" min="0.001" max="0.5" step="0.001" value="0.02" />
34
+ </div>
35
+ <div class="control-group">
36
+ <label for="iterationCount">Iterations</label>
37
+ <input id="iterationCount" type="number" min="5" max="300" value="120" />
38
+ </div>
39
+ <div class="control-group">
40
+ <label for="noiseLevel">Noise</label>
41
+ <input id="noiseLevel" type="number" min="0" max="5" step="0.1" value="0.35" />
42
+ </div>
43
+ </div>
44
+
45
+ <div class="row">
46
+ <button id="runGdBtn" type="button">Run Gradient Descent</button>
47
+ </div>
48
+ </section>
49
+
50
+ <section class="panel output">
51
+ <h2>Optimization Playback</h2>
52
+ <div class="slider-row gd-slider-row">
53
+ <label for="gdStep">step</label>
54
+ <input id="gdStep" type="range" min="0" max="0" value="0" />
55
+ <span id="gdStepLabel">0 / 0</span>
56
+ <button id="playGdBtn" type="button">Play</button>
57
+ <button id="resetGdBtn" type="button">Reset</button>
58
+ </div>
59
+ </section>
60
+
61
+ <section class="panel output">
62
+ <h2>Model Visualization</h2>
63
+ <canvas id="fitCanvas" width="980" height="450"></canvas>
64
+ </section>
65
+
66
+ <section class="panel output">
67
+ <h2>Cost Curve</h2>
68
+ <canvas id="costCanvas" width="980" height="260"></canvas>
69
+ </section>
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">
87
+ <h2>Cost Function and Result</h2>
88
+ <div id="costSummary" class="viz-grid"></div>
89
+ </section>
90
+
91
+ <section class="panel output">
92
+ <h2>Step Details</h2>
93
+ <div id="stepSummary" class="viz-grid"></div>
94
+ </section>
95
+
96
+ <section class="panel output">
97
+ <h2>Iteration Log (Cost / Parameters / Gradients)</h2>
98
+ <div class="log-wrap">
99
+ <table class="gd-log-table" id="gdLogTable">
100
+ <thead>
101
+ <tr>
102
+ <th>step</th>
103
+ <th>cost</th>
104
+ <th>theta</th>
105
+ <th>gradient</th>
106
+ </tr>
107
+ </thead>
108
+ <tbody id="gdLogBody"></tbody>
109
+ </table>
110
+ </div>
111
+ </section>
112
+ </main>
113
+
114
+ <script src="gradient.js?v=20260812g"></script>
115
+ </body>
116
+ </html>
linear-regression/gradient.js ADDED
@@ -0,0 +1,829 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const sampleCountInput = document.getElementById("sampleCount");
2
+ const learningRateInput = document.getElementById("learningRate");
3
+ const iterationCountInput = document.getElementById("iterationCount");
4
+ const noiseLevelInput = document.getElementById("noiseLevel");
5
+ const runGdBtn = document.getElementById("runGdBtn");
6
+ 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: [],
24
+ steps: [],
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", () => {
38
+ stopPlayback();
39
+ state.currentStep = Number(gdStep.value);
40
+ renderAll();
41
+ });
42
+
43
+ playGdBtn.addEventListener("click", () => {
44
+ if (!state.steps.length) return;
45
+ if (state.timer) stopPlayback();
46
+ else startPlayback();
47
+ });
48
+
49
+ resetGdBtn.addEventListener("click", () => {
50
+ stopPlayback();
51
+ if (!state.steps.length) return;
52
+ state.currentStep = 0;
53
+ gdStep.value = "0";
54
+ renderAll();
55
+ });
56
+
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) {
72
+ const parent = canvas.parentElement;
73
+ if (!parent) return;
74
+
75
+ const width = Math.max(320, Math.floor(parent.clientWidth - 2));
76
+ const targetHeight = Math.floor(width * ratio);
77
+ const height = Math.max(minHeight, Math.min(maxHeight, targetHeight));
78
+
79
+ if (canvas.width !== width || canvas.height !== height) {
80
+ canvas.width = width;
81
+ canvas.height = height;
82
+ }
83
+ }
84
+
85
+ function runGradientDescent() {
86
+ stopPlayback();
87
+
88
+ const config = readConfig();
89
+ state.data = generateData(config);
90
+ state.steps = computeGradientDescent(state.data, config).steps;
91
+ state.currentStep = 0;
92
+ state.landscapeCache = null;
93
+
94
+ gdStep.max = String(state.steps.length - 1);
95
+ gdStep.value = "0";
96
+
97
+ renderAll();
98
+ }
99
+
100
+ function readConfig() {
101
+ const sampleCount = clampInt(Number(sampleCountInput.value), 10, 120, 50);
102
+ const learningRate = clampNum(Number(learningRateInput.value), 0.001, 0.5, 0.02);
103
+ const iterations = clampInt(Number(iterationCountInput.value), 5, 300, 120);
104
+ const noise = clampNum(Number(noiseLevelInput.value), 0, 5, 0.35);
105
+
106
+ sampleCountInput.value = String(sampleCount);
107
+ learningRateInput.value = String(learningRate);
108
+ iterationCountInput.value = String(iterations);
109
+ noiseLevelInput.value = String(noise);
110
+
111
+ return { sampleCount, learningRate, iterations, noise };
112
+ }
113
+
114
+ function generateData(config) {
115
+ const rows = [];
116
+ for (let i = 0; i < config.sampleCount; i += 1) {
117
+ const x1 = rand(-5, 5);
118
+ const y = 8 + 4.2 * x1 + rand(-config.noise, config.noise);
119
+ rows.push({ x: [x1], y });
120
+ }
121
+ return rows;
122
+ }
123
+
124
+ function computeGradientDescent(data, config) {
125
+ let theta = [-12, 12];
126
+ const steps = [];
127
+
128
+ for (let step = 0; step <= config.iterations; step += 1) {
129
+ const { cost, grad } = costAndGradient(data, theta);
130
+
131
+ steps.push({ step, theta: theta.slice(), grad: grad.slice(), cost });
132
+
133
+ if (step === config.iterations) break;
134
+ theta = theta.map((t, j) => t - config.learningRate * grad[j]);
135
+ }
136
+
137
+ return { steps };
138
+ }
139
+
140
+ function costAndGradient(data, theta) {
141
+ const m = data.length;
142
+ let sumSq = 0;
143
+ const grad = [0, 0];
144
+
145
+ for (const row of data) {
146
+ const yHat = theta[0] + theta[1] * row.x[0];
147
+ const err = yHat - row.y;
148
+ sumSq += err * err;
149
+ grad[0] += err;
150
+ grad[1] += err * row.x[0];
151
+ }
152
+
153
+ grad[0] /= m;
154
+ grad[1] /= m;
155
+
156
+ return { cost: sumSq / (2 * m), grad };
157
+ }
158
+
159
+ function renderAll() {
160
+ if (!state.steps.length) return;
161
+
162
+ gdStepLabel.textContent = `${state.currentStep} / ${state.steps.length - 1}`;
163
+ renderFitPlot();
164
+ renderCostPlot();
165
+ renderCostLandscape();
166
+ renderCostSummary();
167
+ renderStepSummary();
168
+ renderLogTable();
169
+ }
170
+
171
+ function renderFitPlot() {
172
+ const ctx = fitCtx;
173
+ const { width, height } = fitCanvas;
174
+ const pad = 52;
175
+ const s = state.steps[state.currentStep];
176
+
177
+ ctx.clearRect(0, 0, width, height);
178
+
179
+ const xs = state.data.map((d) => d.x[0]);
180
+ const ys = state.data.map((d) => d.y);
181
+
182
+ const xMin = Math.min(...xs) - 1;
183
+ const xMax = Math.max(...xs) + 1;
184
+ const yMin = Math.min(...ys) - 2;
185
+ const yMax = Math.max(...ys) + 2;
186
+
187
+ const toX = (x) => pad + ((x - xMin) / (xMax - xMin)) * (width - 2 * pad);
188
+ const toY = (y) => height - pad - ((y - yMin) / (yMax - yMin)) * (height - 2 * pad);
189
+
190
+ drawAxes2D(ctx, width, height, pad, "x", "y");
191
+
192
+ ctx.fillStyle = "#0c7b73";
193
+ for (const row of state.data) {
194
+ ctx.beginPath();
195
+ ctx.arc(toX(row.x[0]), toY(row.y), 4, 0, Math.PI * 2);
196
+ ctx.fill();
197
+ }
198
+
199
+ const yL = s.theta[0] + s.theta[1] * xMin;
200
+ const yR = s.theta[0] + s.theta[1] * xMax;
201
+ ctx.strokeStyle = "#dd5e2f";
202
+ ctx.lineWidth = 3;
203
+ ctx.beginPath();
204
+ ctx.moveTo(toX(xMin), toY(yL));
205
+ ctx.lineTo(toX(xMax), toY(yR));
206
+ ctx.stroke();
207
+ }
208
+
209
+ function renderCostPlot() {
210
+ const ctx = costCtx;
211
+ const { width, height } = costCanvas;
212
+ const pad = 40;
213
+ const costs = state.steps.map((s) => s.cost);
214
+
215
+ ctx.clearRect(0, 0, width, height);
216
+
217
+ const minC = Math.min(...costs);
218
+ const maxC = Math.max(...costs);
219
+
220
+ const toX = (i) => pad + (i / (costs.length - 1 || 1)) * (width - 2 * pad);
221
+ const toY = (c) =>
222
+ height - pad - ((c - minC) / (maxC - minC || 1)) * (height - 2 * pad);
223
+
224
+ drawAxes2D(ctx, width, height, pad, "iteration", "cost");
225
+
226
+ ctx.strokeStyle = "#0c7b73";
227
+ ctx.lineWidth = 2.2;
228
+ ctx.beginPath();
229
+ costs.forEach((cost, i) => {
230
+ const x = toX(i);
231
+ const y = toY(cost);
232
+ if (i === 0) ctx.moveTo(x, y);
233
+ else ctx.lineTo(x, y);
234
+ });
235
+ ctx.stroke();
236
+
237
+ ctx.fillStyle = "#dd5e2f";
238
+ ctx.beginPath();
239
+ ctx.arc(toX(state.currentStep), toY(costs[state.currentStep]), 5, 0, Math.PI * 2);
240
+ ctx.fill();
241
+ }
242
+
243
+ function renderCostLandscape() {
244
+ const data = getLandscapeData();
245
+ if (landscapeMode.value === "3d") renderLandscape3D(data);
246
+ else renderLandscape2D(data);
247
+ }
248
+
249
+ function getLandscapeData() {
250
+ if (state.landscapeCache) return state.landscapeCache;
251
+
252
+ const pathTheta0 = state.steps.map((s) => s.theta[0]);
253
+ const pathTheta1 = state.steps.map((s) => s.theta[1]);
254
+
255
+ const t0MinRaw = Math.min(...pathTheta0);
256
+ const t0MaxRaw = Math.max(...pathTheta0);
257
+ const t1MinRaw = Math.min(...pathTheta1);
258
+ const t1MaxRaw = Math.max(...pathTheta1);
259
+
260
+ const t0Pad = (t0MaxRaw - t0MinRaw || 1) * 0.35;
261
+ const t1Pad = (t1MaxRaw - t1MinRaw || 1) * 0.35;
262
+
263
+ const t0Min = t0MinRaw - t0Pad;
264
+ const t0Max = t0MaxRaw + t0Pad;
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);
281
+ zMax = Math.max(zMax, cost);
282
+ }
283
+ grid.push(row);
284
+ }
285
+
286
+ const path = state.steps.map((s) => ({
287
+ t0: s.theta[0],
288
+ t1: s.theta[1],
289
+ z: s.cost,
290
+ g0: s.grad[0],
291
+ g1: s.grad[1],
292
+ }));
293
+
294
+ state.landscapeCache = {
295
+ grid,
296
+ gridN,
297
+ t0Min,
298
+ t0Max,
299
+ t1Min,
300
+ t1Max,
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];
576
+
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>
584
+ <p class="formula-line">y_hat = ${fmt(last.theta[0])} + ${fmt(last.theta[1])}*x1</p>
585
+ <p class="slice-label">parameters: 2 (theta0, theta1)</p>
586
+ <p class="slice-label">initial cost: ${first.cost.toFixed(6)}</p>
587
+ <p class="slice-label">final cost: ${last.cost.toFixed(6)}</p>
588
+ </article>
589
+ `;
590
+ }
591
+
592
+ function renderStepSummary() {
593
+ const s = state.steps[state.currentStep];
594
+ stepSummary.innerHTML = `
595
+ <article class="matrix-card">
596
+ <p class="matrix-title">Current Step ${s.step}</p>
597
+ <p class="formula-line">cost = ${s.cost.toFixed(6)}</p>
598
+ <p class="formula-line">theta = [${s.theta.map(fmt).join(", ")}]</p>
599
+ <p class="formula-line">gradient = [${s.grad.map(fmt).join(", ")}]</p>
600
+ </article>
601
+ `;
602
+ }
603
+
604
+ function renderLogTable() {
605
+ gdLogBody.innerHTML = "";
606
+
607
+ for (const s of state.steps) {
608
+ const tr = document.createElement("tr");
609
+ if (s.step === state.currentStep) tr.classList.add("active-log-row");
610
+
611
+ const cells = [
612
+ String(s.step),
613
+ s.cost.toFixed(6),
614
+ `[${s.theta.map(fmt).join(", ")}]`,
615
+ `[${s.grad.map(fmt).join(", ")}]`,
616
+ ];
617
+
618
+ cells.forEach((txt) => {
619
+ const td = document.createElement("td");
620
+ td.textContent = txt;
621
+ tr.appendChild(td);
622
+ });
623
+
624
+ gdLogBody.appendChild(tr);
625
+ }
626
+ }
627
+
628
+ function drawAxes2D(ctx, width, height, pad, xLabel, yLabel) {
629
+ ctx.strokeStyle = "#8f9ba1";
630
+ ctx.lineWidth = 1.2;
631
+
632
+ ctx.beginPath();
633
+ ctx.moveTo(pad, height - pad);
634
+ ctx.lineTo(width - pad, height - pad);
635
+ ctx.stroke();
636
+
637
+ ctx.beginPath();
638
+ ctx.moveTo(pad, height - pad);
639
+ ctx.lineTo(pad, pad);
640
+ ctx.stroke();
641
+
642
+ ctx.fillStyle = "#4c5961";
643
+ ctx.font = "13px 'Courier New', monospace";
644
+ ctx.fillText(xLabel, width - pad - 50, height - pad + 22);
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";
782
+
783
+ state.timer = setInterval(() => {
784
+ if (!document.body.contains(gdStep)) {
785
+ stopPlayback();
786
+ return;
787
+ }
788
+
789
+ if (state.currentStep >= state.steps.length - 1) {
790
+ stopPlayback();
791
+ return;
792
+ }
793
+
794
+ state.currentStep += 1;
795
+ gdStep.value = String(state.currentStep);
796
+ renderAll();
797
+ }, 220);
798
+ }
799
+
800
+ function stopPlayback() {
801
+ if (!state.timer) {
802
+ playGdBtn.textContent = "Play";
803
+ return;
804
+ }
805
+
806
+ clearInterval(state.timer);
807
+ state.timer = null;
808
+ playGdBtn.textContent = "Play";
809
+ }
810
+
811
+ function fmt(n) {
812
+ return Number(n).toFixed(4);
813
+ }
814
+
815
+ function rand(min, max) {
816
+ return min + Math.random() * (max - min);
817
+ }
818
+
819
+ function clampInt(v, min, max, fallback) {
820
+ if (!Number.isFinite(v)) return fallback;
821
+ return Math.max(min, Math.min(max, Math.round(v)));
822
+ }
823
+
824
+ function clampNum(v, min, max, fallback) {
825
+ if (!Number.isFinite(v)) return fallback;
826
+ return Math.max(min, Math.min(max, v));
827
+ }
828
+
829
+ init();
linear-regression/index.html ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Data Driven World Interface</title>
7
+ <link rel="stylesheet" href="styles.css" />
8
+ </head>
9
+ <body>
10
+ <div class="bg-grid"></div>
11
+ <main class="container">
12
+ <header class="hero">
13
+ <p class="eyebrow">Course Interface</p>
14
+ <h1>Data Driven World</h1>
15
+ <p>
16
+ Explore NumPy matrix operations through an interactive visual lab.
17
+ Choose operations in natural language, set matrix shapes, and inspect
18
+ the generated NumPy code with input/output visualizations.
19
+ </p>
20
+ </header>
21
+
22
+ <section class="cards">
23
+ <a class="card" href="numpy-lab.html">
24
+ <h2>NumPy Matrix Lab</h2>
25
+ <p>
26
+ Operations, shape-aware inputs, NumPy snippets, and visualized arrays
27
+ up to 3 dimensions.
28
+ </p>
29
+ <span>Open Lab</span>
30
+ </a>
31
+ <a class="card" href="gradient-descent.html">
32
+ <h2>Gradient Descent Studio</h2>
33
+ <p>
34
+ Dynamic optimization walkthrough for one-feature linear regression
35
+ with step-level gradients, costs, and descent-direction landscapes.
36
+ </p>
37
+ <span>Open Studio</span>
38
+ </a>
39
+ <a class="card" href="linear-regression-steps.html">
40
+ <h2>Linear Regression Step Trainer</h2>
41
+ <p>
42
+ Solve hand-calculation style gradient and one-step parameter updates
43
+ on two easy samples with optional z-normalization.
44
+ </p>
45
+ <span>Open Trainer</span>
46
+ </a>
47
+ </section>
48
+ </main>
49
+ </body>
50
+ </html>
linear-regression/linear-regression-steps.html ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Linear Regression Step Trainer</title>
7
+ <link rel="stylesheet" href="styles.css" />
8
+ </head>
9
+ <body>
10
+ <div class="bg-grid"></div>
11
+ <main class="container">
12
+ <nav class="top-nav">
13
+ <a href="index.html">Back Home</a>
14
+ </nav>
15
+
16
+ <header class="hero compact">
17
+ <p class="eyebrow">Page 3</p>
18
+ <h1>Linear Regression Step Trainer</h1>
19
+ <p>
20
+ Practice one-step gradient and parameter updates with two data samples.
21
+ Choose a model setup, optionally enable z-normalization, then inspect
22
+ every calculation in detail.
23
+ </p>
24
+ </header>
25
+
26
+ <section class="panel controls">
27
+ <div class="row">
28
+ <label for="setupSelect">Parameter Setup</label>
29
+ <select id="setupSelect">
30
+ <option value="two_weights_no_intercept">Two weights, no intercept: y_hat = w1x1 + w2x2</option>
31
+ <option value="two_weights_one_intercept">Two weights + one intercept: y_hat = b + w1x1 + w2x2</option>
32
+ <option value="one_weight_one_intercept">One weight + one intercept: y_hat = b + wx</option>
33
+ </select>
34
+ </div>
35
+
36
+ <div class="row">
37
+ <label for="alphaInput">Learning Rate (alpha)</label>
38
+ <input id="alphaInput" type="number" min="0.01" max="1" step="0.01" value="0.1" />
39
+ </div>
40
+
41
+ <div class="row">
42
+ <label for="zNormToggle">Use z-normalization</label>
43
+ <input id="zNormToggle" type="checkbox" />
44
+ </div>
45
+
46
+ <div class="row">
47
+ <button id="newQuestionBtn" type="button">Randomize New Samples</button>
48
+ <button id="solveBtn" type="button">Show Step-by-Step Solution</button>
49
+ </div>
50
+ </section>
51
+
52
+ <section class="panel output">
53
+ <h2>Question Data (Ground-Truth y Included)</h2>
54
+ <div id="questionPrompt" class="viz-grid"></div>
55
+ </section>
56
+
57
+ <section class="panel output">
58
+ <h2>Detailed Process, Result, and Explanation</h2>
59
+ <div id="solutionOutput" class="viz-grid"></div>
60
+ </section>
61
+ </main>
62
+
63
+ <script src="linear-regression-steps.js?v=20260812"></script>
64
+ </body>
65
+ </html>
linear-regression/linear-regression-steps.js ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const setupSelect = document.getElementById("setupSelect");
2
+ const alphaInput = document.getElementById("alphaInput");
3
+ const zNormToggle = document.getElementById("zNormToggle");
4
+ const newQuestionBtn = document.getElementById("newQuestionBtn");
5
+ const solveBtn = document.getElementById("solveBtn");
6
+ const questionPrompt = document.getElementById("questionPrompt");
7
+ const solutionOutput = document.getElementById("solutionOutput");
8
+
9
+ const setupConfig = {
10
+ two_weights_no_intercept: {
11
+ label: "Two weights, no intercept",
12
+ featureNames: ["x1", "x2"],
13
+ paramNames: ["w1", "w2"],
14
+ hasIntercept: false,
15
+ formula: "y_hat = w1*x1 + w2*x2",
16
+ },
17
+ two_weights_one_intercept: {
18
+ label: "Two weights + one intercept",
19
+ featureNames: ["x1", "x2"],
20
+ paramNames: ["b", "w1", "w2"],
21
+ hasIntercept: true,
22
+ formula: "y_hat = b + w1*x1 + w2*x2",
23
+ },
24
+ one_weight_one_intercept: {
25
+ label: "One weight + one intercept",
26
+ featureNames: ["x"],
27
+ paramNames: ["b", "w"],
28
+ hasIntercept: true,
29
+ formula: "y_hat = b + w*x",
30
+ },
31
+ };
32
+
33
+ const state = {
34
+ setupKey: "two_weights_no_intercept",
35
+ question: null,
36
+ };
37
+
38
+ function init() {
39
+ bindEvents();
40
+ generateQuestion();
41
+ }
42
+
43
+ function bindEvents() {
44
+ setupSelect.addEventListener("change", () => {
45
+ state.setupKey = setupSelect.value;
46
+ generateQuestion();
47
+ solutionOutput.innerHTML = "";
48
+ });
49
+
50
+ zNormToggle.addEventListener("change", renderQuestionPrompt);
51
+
52
+ newQuestionBtn.addEventListener("click", () => {
53
+ generateQuestion();
54
+ solutionOutput.innerHTML = "";
55
+ });
56
+
57
+ solveBtn.addEventListener("click", () => {
58
+ const alpha = clampNum(Number(alphaInput.value), 0.01, 1, 0.1);
59
+ alphaInput.value = fmt(alpha);
60
+ const useZNorm = zNormToggle.checked;
61
+ renderSolution(alpha, useZNorm);
62
+ });
63
+ }
64
+
65
+ function generateQuestion() {
66
+ const cfg = setupConfig[state.setupKey];
67
+ const rows = [];
68
+
69
+ if (state.setupKey === "one_weight_one_intercept") {
70
+ const trueB = randInt(-1, 3);
71
+ const trueW = randInt(1, 4);
72
+ const x1 = randInt(1, 4);
73
+ let x2 = randInt(2, 6);
74
+ while (x2 === x1) x2 = randInt(2, 6);
75
+
76
+ rows.push({ x: [x1], y: trueB + trueW * x1 });
77
+ rows.push({ x: [x2], y: trueB + trueW * x2 });
78
+
79
+ state.question = {
80
+ trueParams: { b: trueB, w: trueW },
81
+ initParams: { b: 0, w: 0 },
82
+ rows,
83
+ cfg,
84
+ };
85
+ } else if (state.setupKey === "two_weights_no_intercept") {
86
+ const trueW1 = randInt(1, 3);
87
+ const trueW2 = randInt(1, 3);
88
+
89
+ const x11 = randInt(1, 4);
90
+ let x12 = randInt(2, 6);
91
+ while (x12 === x11) x12 = randInt(2, 6);
92
+
93
+ const x21 = randInt(1, 4);
94
+ let x22 = randInt(2, 6);
95
+ while (x22 === x21) x22 = randInt(2, 6);
96
+
97
+ rows.push({ x: [x11, x21], y: trueW1 * x11 + trueW2 * x21 });
98
+ rows.push({ x: [x12, x22], y: trueW1 * x12 + trueW2 * x22 });
99
+
100
+ state.question = {
101
+ trueParams: { w1: trueW1, w2: trueW2 },
102
+ initParams: { w1: 0, w2: 0 },
103
+ rows,
104
+ cfg,
105
+ };
106
+ } else {
107
+ const trueB = randInt(-1, 3);
108
+ const trueW1 = randInt(1, 3);
109
+ const trueW2 = randInt(1, 3);
110
+
111
+ const x11 = randInt(1, 4);
112
+ let x12 = randInt(2, 6);
113
+ while (x12 === x11) x12 = randInt(2, 6);
114
+
115
+ const x21 = randInt(1, 4);
116
+ let x22 = randInt(2, 6);
117
+ while (x22 === x21) x22 = randInt(2, 6);
118
+
119
+ rows.push({ x: [x11, x21], y: trueB + trueW1 * x11 + trueW2 * x21 });
120
+ rows.push({ x: [x12, x22], y: trueB + trueW1 * x12 + trueW2 * x22 });
121
+
122
+ state.question = {
123
+ trueParams: { b: trueB, w1: trueW1, w2: trueW2 },
124
+ initParams: { b: 0, w1: 0, w2: 0 },
125
+ rows,
126
+ cfg,
127
+ };
128
+ }
129
+
130
+ renderQuestionPrompt();
131
+ }
132
+
133
+ function renderQuestionPrompt() {
134
+ if (!state.question) return;
135
+
136
+ const useZNorm = zNormToggle.checked;
137
+ const { cfg, rows } = state.question;
138
+
139
+ questionPrompt.innerHTML = `
140
+ <div class="matrix-card">
141
+ <p class="matrix-title">Setup</p>
142
+ <p class="formula-line">Model: ${escapeHtml(cfg.label)}</p>
143
+ <p class="formula-line">Formula: ${escapeHtml(cfg.formula)}</p>
144
+ <p class="formula-line">Initial parameters: ${renderParamInline(state.question.initParams)}</p>
145
+ <p class="formula-line">Normalization for update step: ${useZNorm ? "z-normalization enabled" : "raw features"}</p>
146
+ <p class="formula-line">Task: Compute gradients and one update step for all parameters.</p>
147
+ </div>
148
+ <div class="matrix-card">
149
+ <p class="matrix-title">Two Data Samples</p>
150
+ ${renderDataTable(rows, cfg.featureNames, null)}
151
+ <p class="slice-label">Ground-truth y is given in the table.</p>
152
+ </div>
153
+ `;
154
+ }
155
+
156
+ function renderSolution(alpha, useZNorm) {
157
+ if (!state.question) return;
158
+
159
+ const { cfg, rows, initParams } = state.question;
160
+ const m = rows.length;
161
+ const featureStats = computeFeatureStats(rows);
162
+ const rowsUsed = rows.map((row) => ({
163
+ y: row.y,
164
+ x: row.x.map((value, idx) => (useZNorm ? zNorm(value, featureStats[idx]) : value)),
165
+ xRaw: row.x.slice(),
166
+ }));
167
+
168
+ const orderedParams = cfg.paramNames.slice();
169
+ const grads = {};
170
+ const update = {};
171
+ const predRows = [];
172
+
173
+ for (const name of orderedParams) grads[name] = 0;
174
+
175
+ for (let i = 0; i < m; i += 1) {
176
+ const row = rowsUsed[i];
177
+ const yHat = predict(cfg, initParams, row.x);
178
+ const err = yHat - row.y;
179
+
180
+ predRows.push({ index: i + 1, x: row.x, xRaw: row.xRaw, y: row.y, yHat, err });
181
+
182
+ if (cfg.hasIntercept) grads.b += err;
183
+
184
+ if (state.setupKey === "one_weight_one_intercept") {
185
+ grads.w += err * row.x[0];
186
+ } else {
187
+ grads.w1 += err * row.x[0];
188
+ grads.w2 += err * row.x[1];
189
+ }
190
+ }
191
+
192
+ for (const name of orderedParams) {
193
+ grads[name] /= m;
194
+ update[name] = initParams[name] - alpha * grads[name];
195
+ }
196
+
197
+ const cost = predRows.reduce((acc, r) => acc + r.err * r.err, 0) / (2 * m);
198
+
199
+ const normBlock = useZNorm
200
+ ? `
201
+ <div class="matrix-card">
202
+ <p class="matrix-title">Step 0: z-Normalization</p>
203
+ ${renderNormSummary(cfg.featureNames, featureStats)}
204
+ ${renderDataTable(rows, cfg.featureNames, rowsUsed.map((r) => r.x))}
205
+ </div>
206
+ `
207
+ : "";
208
+
209
+ solutionOutput.innerHTML = `
210
+ ${normBlock}
211
+ <div class="matrix-card">
212
+ <p class="matrix-title">Step 1: Predictions and Errors</p>
213
+ ${renderPredictionSteps(cfg, predRows, initParams, useZNorm)}
214
+ <p class="formula-line">Cost: J = (1/(2m)) * sum((y_hat - y)^2) = ${fmt(cost)}</p>
215
+ </div>
216
+
217
+ <div class="matrix-card">
218
+ <p class="matrix-title">Step 2: Gradient Calculation</p>
219
+ ${renderGradientSteps(cfg, predRows, grads, m)}
220
+ </div>
221
+
222
+ <div class="matrix-card">
223
+ <p class="matrix-title">Step 3: Parameter Update (One Gradient Step)</p>
224
+ ${renderUpdateSteps(cfg, initParams, grads, update, alpha)}
225
+ </div>
226
+
227
+ <div class="matrix-card">
228
+ <p class="matrix-title">Explanation</p>
229
+ <p class="formula-line">A negative gradient means increasing that parameter will reduce cost, so the update adds value in that direction.</p>
230
+ <p class="formula-line">A positive gradient means decreasing that parameter will reduce cost.</p>
231
+ <p class="formula-line">${useZNorm ? "Using z-normalized features keeps scales consistent, so gradient magnitudes across features are easier to compare." : "Raw features are used directly, so larger-scale features can produce larger gradient terms."}</p>
232
+ <p class="formula-line">Use <strong>Randomize New Samples</strong> to practice again with fresh numbers.</p>
233
+ </div>
234
+ `;
235
+ }
236
+
237
+ function renderDataTable(rows, featureNames, zRows) {
238
+ const headers = featureNames.map((f) => `<th>${f}</th>`).join("");
239
+ const zHeaders = zRows ? featureNames.map((f) => `<th>z(${f})</th>`).join("") : "";
240
+
241
+ const body = rows
242
+ .map((row, i) => {
243
+ const xs = row.x.map((v) => `<td>${fmt(v)}</td>`).join("");
244
+ const zs = zRows
245
+ ? zRows[i].map((v) => `<td>${fmt(v)}</td>`).join("")
246
+ : "";
247
+ return `<tr><td>${i + 1}</td>${xs}${zs}<td>${fmt(row.y)}</td></tr>`;
248
+ })
249
+ .join("");
250
+
251
+ return `
252
+ <table class="matrix-table">
253
+ <thead>
254
+ <tr>
255
+ <th>sample</th>
256
+ ${headers}
257
+ ${zHeaders}
258
+ <th>y (ground truth)</th>
259
+ </tr>
260
+ </thead>
261
+ <tbody>${body}</tbody>
262
+ </table>
263
+ `;
264
+ }
265
+
266
+ function renderNormSummary(featureNames, stats) {
267
+ return featureNames
268
+ .map((name, idx) => {
269
+ const s = stats[idx];
270
+ return `<p class="formula-line">${name}: mean=${fmt(s.mean)}, std=${fmt(s.std)} so z = (x - ${fmt(s.mean)}) / ${fmt(s.std)}</p>`;
271
+ })
272
+ .join("");
273
+ }
274
+
275
+ function renderPredictionSteps(cfg, predRows, initParams, useZNorm) {
276
+ const pLine = renderParamInline(initParams);
277
+ const head = `<p class="formula-line">Start with ${pLine}. ${useZNorm ? "Use normalized x values." : "Use raw x values."}</p>`;
278
+
279
+ const lines = predRows
280
+ .map((row) => {
281
+ if (state.setupKey === "one_weight_one_intercept") {
282
+ return `<p class="formula-line">sample ${row.index}: y_hat = b + w*x = ${fmt(initParams.b)} + ${fmt(initParams.w)}*${fmt(row.x[0])} = ${fmt(row.yHat)}, error = y_hat - y = ${fmt(row.yHat)} - ${fmt(row.y)} = ${fmt(row.err)}</p>`;
283
+ }
284
+
285
+ if (cfg.hasIntercept) {
286
+ return `<p class="formula-line">sample ${row.index}: y_hat = b + w1*x1 + w2*x2 = ${fmt(initParams.b)} + ${fmt(initParams.w1)}*${fmt(row.x[0])} + ${fmt(initParams.w2)}*${fmt(row.x[1])} = ${fmt(row.yHat)}, error = ${fmt(row.err)}</p>`;
287
+ }
288
+
289
+ return `<p class="formula-line">sample ${row.index}: y_hat = w1*x1 + w2*x2 = ${fmt(initParams.w1)}*${fmt(row.x[0])} + ${fmt(initParams.w2)}*${fmt(row.x[1])} = ${fmt(row.yHat)}, error = ${fmt(row.err)}</p>`;
290
+ })
291
+ .join("");
292
+
293
+ return head + lines;
294
+ }
295
+
296
+ function renderGradientSteps(cfg, predRows, grads, m) {
297
+ const errTerms = predRows.map((r) => fmt(r.err)).join(" + ");
298
+ let out = "";
299
+
300
+ if (cfg.hasIntercept) {
301
+ out += `<p class="formula-line">grad_b = (1/m) * sum(error) = (1/${m}) * (${errTerms}) = ${fmt(grads.b)}</p>`;
302
+ }
303
+
304
+ if (state.setupKey === "one_weight_one_intercept") {
305
+ const terms = predRows.map((r) => `${fmt(r.err)}*${fmt(r.x[0])}`).join(" + ");
306
+ out += `<p class="formula-line">grad_w = (1/m) * sum(error*x) = (1/${m}) * (${terms}) = ${fmt(grads.w)}</p>`;
307
+ return out;
308
+ }
309
+
310
+ const terms1 = predRows.map((r) => `${fmt(r.err)}*${fmt(r.x[0])}`).join(" + ");
311
+ const terms2 = predRows.map((r) => `${fmt(r.err)}*${fmt(r.x[1])}`).join(" + ");
312
+ out += `<p class="formula-line">grad_w1 = (1/m) * sum(error*x1) = (1/${m}) * (${terms1}) = ${fmt(grads.w1)}</p>`;
313
+ out += `<p class="formula-line">grad_w2 = (1/m) * sum(error*x2) = (1/${m}) * (${terms2}) = ${fmt(grads.w2)}</p>`;
314
+
315
+ return out;
316
+ }
317
+
318
+ function renderUpdateSteps(cfg, initParams, grads, update, alpha) {
319
+ return cfg.paramNames
320
+ .map((name) => {
321
+ return `<p class="formula-line">${name}_new = ${name} - alpha*grad_${name} = ${fmt(initParams[name])} - ${fmt(alpha)}*${fmt(grads[name])} = ${fmt(update[name])}</p>`;
322
+ })
323
+ .join("");
324
+ }
325
+
326
+ function predict(cfg, params, xRow) {
327
+ let yHat = cfg.hasIntercept ? params.b : 0;
328
+
329
+ if (state.setupKey === "one_weight_one_intercept") {
330
+ yHat += params.w * xRow[0];
331
+ return yHat;
332
+ }
333
+
334
+ yHat += params.w1 * xRow[0] + params.w2 * xRow[1];
335
+ return yHat;
336
+ }
337
+
338
+ function computeFeatureStats(rows) {
339
+ const d = rows[0].x.length;
340
+ const stats = [];
341
+
342
+ for (let j = 0; j < d; j += 1) {
343
+ const values = rows.map((r) => r.x[j]);
344
+ const mean = values.reduce((acc, v) => acc + v, 0) / values.length;
345
+ const variance =
346
+ values.reduce((acc, v) => acc + (v - mean) * (v - mean), 0) / values.length;
347
+ const std = Math.sqrt(variance) || 1;
348
+ stats.push({ mean, std });
349
+ }
350
+
351
+ return stats;
352
+ }
353
+
354
+ function zNorm(value, stat) {
355
+ return (value - stat.mean) / stat.std;
356
+ }
357
+
358
+ function renderParamInline(params) {
359
+ return Object.entries(params)
360
+ .map(([k, v]) => `${k}=${fmt(v)}`)
361
+ .join(", ");
362
+ }
363
+
364
+ function clampNum(v, min, max, fallback) {
365
+ if (!Number.isFinite(v)) return fallback;
366
+ return Math.max(min, Math.min(max, v));
367
+ }
368
+
369
+ function randInt(min, max) {
370
+ return Math.floor(Math.random() * (max - min + 1)) + min;
371
+ }
372
+
373
+ function fmt(v) {
374
+ if (!Number.isFinite(v)) return String(v);
375
+ return Number(v.toFixed(4)).toString();
376
+ }
377
+
378
+ function escapeHtml(text) {
379
+ return String(text)
380
+ .replaceAll("&", "&amp;")
381
+ .replaceAll("<", "&lt;")
382
+ .replaceAll(">", "&gt;")
383
+ .replaceAll('"', "&quot;")
384
+ .replaceAll("'", "&#39;");
385
+ }
386
+
387
+ init();
linear-regression/numpy-lab.html ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>NumPy Matrix Lab</title>
7
+ <link rel="stylesheet" href="styles.css" />
8
+ </head>
9
+ <body>
10
+ <div class="bg-grid"></div>
11
+ <main class="container">
12
+ <nav class="top-nav">
13
+ <a href="index.html">Back Home</a>
14
+ </nav>
15
+
16
+ <header class="hero compact">
17
+ <p class="eyebrow">Page 1</p>
18
+ <h1>NumPy Matrix Operations</h1>
19
+ <p>
20
+ Pick a natural-language operation, define matrix shapes (max 3D), and
21
+ inspect input/output arrays plus executable NumPy code.
22
+ </p>
23
+ <p class="slice-label">
24
+ Broadcasting is enabled. Example element-wise valid pair: (2,3) with (1,3). Example matmul valid pair: (2,3) with (4,3,2).
25
+ </p>
26
+ </header>
27
+
28
+ <section class="panel controls">
29
+ <div class="row">
30
+ <label for="operationSelect">Operation</label>
31
+ <select id="operationSelect"></select>
32
+ </div>
33
+
34
+ <div class="row">
35
+ <label for="nlInput">Natural language query</label>
36
+ <input
37
+ id="nlInput"
38
+ type="text"
39
+ placeholder="Example: multiply matrix A and B"
40
+ />
41
+ <button id="matchBtn" type="button">Match Query</button>
42
+ </div>
43
+
44
+ <div id="operationInfo" class="operation-info"></div>
45
+ <div id="shapeInputs" class="shape-grid"></div>
46
+ <div id="operationOptions" class="shape-grid"></div>
47
+
48
+ <div class="row">
49
+ <button id="runBtn" type="button">Generate and Compute</button>
50
+ </div>
51
+ </section>
52
+
53
+ <section class="panel output">
54
+ <h2>Generated NumPy Code</h2>
55
+ <pre id="codeOutput" class="code"></pre>
56
+ </section>
57
+
58
+ <section class="panel output">
59
+ <h2>Input Matrices</h2>
60
+ <div id="inputViz" class="viz-grid"></div>
61
+ </section>
62
+
63
+ <section class="panel output">
64
+ <h2>Output Matrix</h2>
65
+ <div id="outputViz" class="viz-grid"></div>
66
+ </section>
67
+
68
+ <section class="panel output">
69
+ <h2>Computation Details</h2>
70
+ <div id="detailViz" class="viz-grid"></div>
71
+ </section>
72
+ </main>
73
+
74
+ <script src="app.js?v=20260812"></script>
75
+ </body>
76
+ </html>
linear-regression/styles.css ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #f4f1ea;
3
+ --card: #fffef9;
4
+ --ink: #18242d;
5
+ --muted: #57646d;
6
+ --accent: #dd5e2f;
7
+ --accent-2: #0c7b73;
8
+ --line: #d8d2c4;
9
+ --shadow: 0 12px 30px rgba(0, 0, 0, 0.08);
10
+ --radius: 14px;
11
+ }
12
+
13
+ * {
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ body {
18
+ margin: 0;
19
+ font-family: "Avenir Next", "Gill Sans", "Trebuchet MS", sans-serif;
20
+ color: var(--ink);
21
+ background: radial-gradient(circle at 20% 10%, #fff6df, #f4f1ea 45%),
22
+ linear-gradient(120deg, #f4f1ea, #ebf3ef);
23
+ min-height: 100vh;
24
+ }
25
+
26
+ .bg-grid {
27
+ position: fixed;
28
+ inset: 0;
29
+ background-image: linear-gradient(rgba(12, 123, 115, 0.07) 1px, transparent 1px),
30
+ linear-gradient(90deg, rgba(12, 123, 115, 0.07) 1px, transparent 1px);
31
+ background-size: 36px 36px;
32
+ pointer-events: none;
33
+ z-index: -1;
34
+ }
35
+
36
+ .container {
37
+ width: min(1050px, 92vw);
38
+ margin: 0 auto;
39
+ padding: 28px 0 50px;
40
+ }
41
+
42
+ .top-nav {
43
+ margin-bottom: 12px;
44
+ }
45
+
46
+ .top-nav a {
47
+ text-decoration: none;
48
+ color: var(--accent-2);
49
+ font-weight: 700;
50
+ }
51
+
52
+ .hero {
53
+ background: var(--card);
54
+ border: 1px solid var(--line);
55
+ border-radius: var(--radius);
56
+ padding: 28px;
57
+ box-shadow: var(--shadow);
58
+ }
59
+
60
+ .hero.compact {
61
+ padding: 22px 24px;
62
+ }
63
+
64
+ .eyebrow {
65
+ margin: 0;
66
+ text-transform: uppercase;
67
+ letter-spacing: 1.8px;
68
+ color: var(--accent);
69
+ font-weight: 800;
70
+ font-size: 12px;
71
+ }
72
+
73
+ h1 {
74
+ margin: 10px 0 8px;
75
+ font-size: clamp(1.8rem, 3vw, 2.8rem);
76
+ }
77
+
78
+ h2 {
79
+ margin-top: 0;
80
+ }
81
+
82
+ p {
83
+ margin: 0;
84
+ line-height: 1.6;
85
+ color: var(--muted);
86
+ }
87
+
88
+ .cards {
89
+ margin-top: 20px;
90
+ display: grid;
91
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
92
+ gap: 16px;
93
+ }
94
+
95
+ .card {
96
+ display: block;
97
+ background: var(--card);
98
+ border: 1px solid var(--line);
99
+ border-radius: var(--radius);
100
+ padding: 20px;
101
+ color: inherit;
102
+ text-decoration: none;
103
+ box-shadow: var(--shadow);
104
+ transition: transform 0.2s ease;
105
+ }
106
+
107
+ .card:hover {
108
+ transform: translateY(-4px);
109
+ }
110
+
111
+ .card span {
112
+ display: inline-block;
113
+ margin-top: 14px;
114
+ color: var(--accent-2);
115
+ font-weight: 700;
116
+ }
117
+
118
+ .panel {
119
+ margin-top: 16px;
120
+ background: var(--card);
121
+ border: 1px solid var(--line);
122
+ border-radius: var(--radius);
123
+ padding: 20px;
124
+ box-shadow: var(--shadow);
125
+ }
126
+
127
+ .row {
128
+ display: flex;
129
+ flex-wrap: wrap;
130
+ gap: 10px;
131
+ align-items: center;
132
+ margin-bottom: 14px;
133
+ }
134
+
135
+ label {
136
+ font-weight: 700;
137
+ min-width: 170px;
138
+ }
139
+
140
+ select,
141
+ input,
142
+ button {
143
+ border-radius: 10px;
144
+ border: 1px solid #c7c0b2;
145
+ padding: 10px 12px;
146
+ font-size: 0.96rem;
147
+ }
148
+
149
+ select,
150
+ input {
151
+ background: #fff;
152
+ flex: 1;
153
+ min-width: 220px;
154
+ }
155
+
156
+ button {
157
+ background: var(--accent);
158
+ color: #fff;
159
+ border: none;
160
+ font-weight: 700;
161
+ cursor: pointer;
162
+ transition: filter 0.15s ease;
163
+ }
164
+
165
+ button:hover {
166
+ filter: brightness(0.94);
167
+ }
168
+
169
+ .operation-info {
170
+ margin: 8px 0 14px;
171
+ color: var(--muted);
172
+ font-size: 0.95rem;
173
+ }
174
+
175
+ .shape-grid {
176
+ display: grid;
177
+ gap: 10px;
178
+ }
179
+
180
+ .shape-card {
181
+ padding: 12px;
182
+ border-radius: 12px;
183
+ border: 1px solid var(--line);
184
+ background: #fff;
185
+ }
186
+
187
+ .shape-card h3 {
188
+ margin: 0 0 8px;
189
+ font-size: 1rem;
190
+ }
191
+
192
+ .shape-card input {
193
+ width: 100%;
194
+ }
195
+
196
+ .options-card {
197
+ background: #fffaf1;
198
+ }
199
+
200
+ .inline-fields {
201
+ display: flex;
202
+ align-items: center;
203
+ gap: 10px;
204
+ margin-bottom: 8px;
205
+ }
206
+
207
+ .mini-label {
208
+ min-width: 70px;
209
+ font-weight: 700;
210
+ }
211
+
212
+ input[type="checkbox"] {
213
+ width: 18px;
214
+ height: 18px;
215
+ min-width: 18px;
216
+ flex: 0;
217
+ }
218
+
219
+ .code {
220
+ margin: 0;
221
+ padding: 14px;
222
+ border-radius: 12px;
223
+ background: #1c252e;
224
+ color: #f3f0e6;
225
+ overflow-x: auto;
226
+ min-height: 120px;
227
+ }
228
+
229
+ .viz-grid {
230
+ display: grid;
231
+ gap: 14px;
232
+ }
233
+
234
+ .matrix-card {
235
+ border: 1px solid var(--line);
236
+ border-radius: 12px;
237
+ padding: 12px;
238
+ background: #fff;
239
+ }
240
+
241
+ .matrix-title {
242
+ margin: 0 0 8px;
243
+ font-weight: 700;
244
+ }
245
+
246
+ .shape-badge {
247
+ display: inline-block;
248
+ margin-left: 8px;
249
+ color: #fff;
250
+ background: var(--accent-2);
251
+ border-radius: 999px;
252
+ font-size: 0.8rem;
253
+ padding: 2px 8px;
254
+ }
255
+
256
+ .matrix-table {
257
+ border-collapse: collapse;
258
+ width: max-content;
259
+ max-width: 100%;
260
+ margin-bottom: 10px;
261
+ }
262
+
263
+ .matrix-table td {
264
+ border: 1px solid #ddd3c0;
265
+ padding: 6px 9px;
266
+ text-align: right;
267
+ font-family: "Courier New", monospace;
268
+ font-size: 0.9rem;
269
+ background: #fdfbf5;
270
+ }
271
+
272
+ .matrix-table th {
273
+ border: 1px solid #ddd3c0;
274
+ padding: 6px 9px;
275
+ text-align: right;
276
+ font-family: "Courier New", monospace;
277
+ font-size: 0.86rem;
278
+ background: #f6efdf;
279
+ }
280
+
281
+ .matrix-pick-btn {
282
+ width: 100%;
283
+ border: 1px solid #ddd3c0;
284
+ border-radius: 6px;
285
+ padding: 5px 7px;
286
+ background: #fff;
287
+ color: var(--ink);
288
+ font-family: "Courier New", monospace;
289
+ font-weight: 700;
290
+ }
291
+
292
+ .matrix-pick-btn.active {
293
+ background: var(--accent-2);
294
+ color: #fff;
295
+ border-color: var(--accent-2);
296
+ }
297
+
298
+ .hl-row {
299
+ background: #fff1dc !important;
300
+ }
301
+
302
+ .hl-col {
303
+ background: #e7f7f4 !important;
304
+ }
305
+
306
+ .hl-cell {
307
+ background: #ddeafc !important;
308
+ font-weight: 800;
309
+ }
310
+
311
+ .slice-label {
312
+ font-size: 0.85rem;
313
+ color: var(--muted);
314
+ margin: 6px 0;
315
+ }
316
+
317
+ .formula-line {
318
+ margin: 4px 0;
319
+ font-family: "Courier New", monospace;
320
+ font-size: 0.9rem;
321
+ color: #29333a;
322
+ }
323
+
324
+ .slider-row {
325
+ display: grid;
326
+ grid-template-columns: auto 1fr auto auto auto;
327
+ gap: 10px;
328
+ align-items: center;
329
+ margin: 8px 0 10px;
330
+ }
331
+
332
+ .slider-row label {
333
+ min-width: 36px;
334
+ }
335
+
336
+ .error {
337
+ color: #a11634;
338
+ font-weight: 700;
339
+ }
340
+
341
+ .two-col-row {
342
+ align-items: flex-end;
343
+ display: grid;
344
+ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
345
+ gap: 12px;
346
+ }
347
+
348
+ .control-group {
349
+ display: grid;
350
+ gap: 6px;
351
+ }
352
+
353
+ .control-group label {
354
+ min-width: 0;
355
+ }
356
+
357
+ .gd-slider-row {
358
+ grid-template-columns: auto 1fr auto auto auto;
359
+ }
360
+
361
+ #fitCanvas,
362
+ #costCanvas,
363
+ #landscapeCanvas {
364
+ width: 100%;
365
+ border: 1px solid var(--line);
366
+ border-radius: 12px;
367
+ background: #fff;
368
+ }
369
+
370
+ .log-wrap {
371
+ overflow: auto;
372
+ border: 1px solid var(--line);
373
+ border-radius: 12px;
374
+ }
375
+
376
+ .gd-log-table {
377
+ border-collapse: collapse;
378
+ width: 100%;
379
+ min-width: 800px;
380
+ font-family: "Courier New", monospace;
381
+ font-size: 0.88rem;
382
+ }
383
+
384
+ .gd-log-table th,
385
+ .gd-log-table td {
386
+ border-bottom: 1px solid #e6dfd2;
387
+ padding: 8px 10px;
388
+ text-align: left;
389
+ white-space: nowrap;
390
+ }
391
+
392
+ .gd-log-table th {
393
+ position: sticky;
394
+ top: 0;
395
+ background: #faf6eb;
396
+ z-index: 1;
397
+ }
398
+
399
+ .active-log-row td {
400
+ background: #eef8f6;
401
+ }
402
+
403
+ @media (max-width: 680px) {
404
+ .container {
405
+ width: 94vw;
406
+ padding-top: 18px;
407
+ }
408
+
409
+ label {
410
+ min-width: 100%;
411
+ }
412
+
413
+ .row {
414
+ margin-bottom: 10px;
415
+ }
416
+
417
+ .slider-row {
418
+ grid-template-columns: 1fr;
419
+ }
420
+
421
+ .slider-row label {
422
+ min-width: 0;
423
+ }
424
+
425
+ .gd-slider-row {
426
+ grid-template-columns: 1fr;
427
+ }
428
+ }
logistic-regression/about.html ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>DDW Interface - Notes</title>
7
+ <link rel="stylesheet" href="styles.css" />
8
+ </head>
9
+ <body>
10
+ <header class="site-header">
11
+ <div>
12
+ <p class="kicker">Data Driven World</p>
13
+ <h1>Interface Notes</h1>
14
+ </div>
15
+ <nav>
16
+ <a href="index.html">Home</a>
17
+ <a href="simple-sigmoid.html">Simple Sigmoid</a>
18
+ <a href="confusion-matrix.html">Confusion Matrix (Page 3)</a>
19
+ <a href="sigmoid.html">Sigmoid Function</a>
20
+ <a href="cost-visualization.html">Cost Function</a>
21
+ <a class="active" href="about.html">Notes</a>
22
+ </nav>
23
+ </header>
24
+
25
+ <main class="panel page-copy">
26
+ <h2>How to use the sigmoid page</h2>
27
+ <p>
28
+ The sigmoid page visualizes logistic regression probability:
29
+ <code>p = 1 / (1 + exp(-(b0 + b1x)))</code>.
30
+ </p>
31
+ <p>
32
+ Adjust <code>b0</code>, <code>b1</code>, and threshold <code>t</code> to see how decision boundaries and
33
+ probabilities change. You can also click the chart to add sample points and inspect classification outcomes.
34
+ </p>
35
+ </main>
36
+ </body>
37
+ </html>
logistic-regression/app.js ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const canvas = document.getElementById("plot");
2
+ const ctx = canvas.getContext("2d");
3
+
4
+ const controls = {
5
+ b0: document.getElementById("b0"),
6
+ b1: document.getElementById("b1"),
7
+ t: document.getElementById("t"),
8
+ probeX: document.getElementById("probeX"),
9
+ showGrid: document.getElementById("showGrid"),
10
+ showShade: document.getElementById("showShade"),
11
+ showDerivative: document.getElementById("showDerivative"),
12
+ preset: document.getElementById("preset"),
13
+ resetBtn: document.getElementById("resetBtn"),
14
+ animateBtn: document.getElementById("animateBtn"),
15
+ };
16
+
17
+ const valueLabels = {
18
+ b0: document.getElementById("b0Value"),
19
+ b1: document.getElementById("b1Value"),
20
+ t: document.getElementById("tValue"),
21
+ probeX: document.getElementById("probeValue"),
22
+ };
23
+
24
+ const stats = document.getElementById("stats");
25
+ const points = [];
26
+
27
+ let animationTimer = null;
28
+ let animationDirection = 1;
29
+
30
+ const MARGIN = { left: 70, right: 24, top: 24, bottom: 58 };
31
+ const X_MIN = -10;
32
+ const X_MAX = 10;
33
+
34
+ function logistic(z) {
35
+ return 1 / (1 + Math.exp(-z));
36
+ }
37
+
38
+ function modelProbability(x, b0, b1) {
39
+ return logistic(b0 + b1 * x);
40
+ }
41
+
42
+ function xToCanvas(x) {
43
+ const w = canvas.width - MARGIN.left - MARGIN.right;
44
+ return MARGIN.left + ((x - X_MIN) / (X_MAX - X_MIN)) * w;
45
+ }
46
+
47
+ function yToCanvas(y) {
48
+ const h = canvas.height - MARGIN.top - MARGIN.bottom;
49
+ return canvas.height - MARGIN.bottom - y * h;
50
+ }
51
+
52
+ function canvasToX(pixelX) {
53
+ const w = canvas.width - MARGIN.left - MARGIN.right;
54
+ return X_MIN + ((pixelX - MARGIN.left) / w) * (X_MAX - X_MIN);
55
+ }
56
+
57
+ function format(num, digits = 3) {
58
+ return Number(num).toFixed(digits);
59
+ }
60
+
61
+ function drawAxes(showGrid) {
62
+ const plotLeft = MARGIN.left;
63
+ const plotRight = canvas.width - MARGIN.right;
64
+ const plotTop = MARGIN.top;
65
+ const plotBottom = canvas.height - MARGIN.bottom;
66
+
67
+ ctx.strokeStyle = "#c9d9df";
68
+ ctx.lineWidth = 1;
69
+
70
+ if (showGrid) {
71
+ for (let x = -10; x <= 10; x += 2) {
72
+ const px = xToCanvas(x);
73
+ ctx.beginPath();
74
+ ctx.moveTo(px, plotTop);
75
+ ctx.lineTo(px, plotBottom);
76
+ ctx.stroke();
77
+ }
78
+ for (let y = 0; y <= 1.001; y += 0.1) {
79
+ const py = yToCanvas(y);
80
+ ctx.beginPath();
81
+ ctx.moveTo(plotLeft, py);
82
+ ctx.lineTo(plotRight, py);
83
+ ctx.stroke();
84
+ }
85
+ }
86
+
87
+ ctx.strokeStyle = "#14343f";
88
+ ctx.lineWidth = 1.4;
89
+
90
+ ctx.beginPath();
91
+ ctx.moveTo(plotLeft, yToCanvas(0));
92
+ ctx.lineTo(plotRight, yToCanvas(0));
93
+ ctx.stroke();
94
+
95
+ ctx.beginPath();
96
+ ctx.moveTo(xToCanvas(0), plotTop);
97
+ ctx.lineTo(xToCanvas(0), plotBottom);
98
+ ctx.stroke();
99
+
100
+ ctx.fillStyle = "#14343f";
101
+ ctx.font = "13px 'Avenir Next', sans-serif";
102
+
103
+ for (let x = -10; x <= 10; x += 2) {
104
+ const px = xToCanvas(x);
105
+ ctx.fillText(String(x), px - 8, yToCanvas(0) + 19);
106
+ }
107
+
108
+ for (let y = 0; y <= 1.001; y += 0.2) {
109
+ const py = yToCanvas(y);
110
+ ctx.fillText(format(y, 1), plotLeft - 40, py + 4);
111
+ }
112
+
113
+ ctx.fillText("x", plotRight - 12, yToCanvas(0) + 40);
114
+ ctx.fillText("p", xToCanvas(0) + 12, plotTop + 12);
115
+ }
116
+
117
+ function drawThresholdLine(t) {
118
+ ctx.strokeStyle = "#d8534f";
119
+ ctx.lineWidth = 1.3;
120
+ ctx.setLineDash([7, 5]);
121
+ ctx.beginPath();
122
+ ctx.moveTo(MARGIN.left, yToCanvas(t));
123
+ ctx.lineTo(canvas.width - MARGIN.right, yToCanvas(t));
124
+ ctx.stroke();
125
+ ctx.setLineDash([]);
126
+ }
127
+
128
+ function drawRegionShade(b0, b1, t) {
129
+ if (Math.abs(b1) < 1e-9) {
130
+ return;
131
+ }
132
+ const logit = Math.log(t / (1 - t));
133
+ const boundaryX = (logit - b0) / b1;
134
+
135
+ const left = MARGIN.left;
136
+ const right = canvas.width - MARGIN.right;
137
+ const top = MARGIN.top;
138
+ const bottom = canvas.height - MARGIN.bottom;
139
+ const boundaryPx = xToCanvas(Math.max(X_MIN, Math.min(X_MAX, boundaryX)));
140
+
141
+ ctx.globalAlpha = 0.15;
142
+ ctx.fillStyle = "#0a8f7b";
143
+
144
+ if (b1 > 0) {
145
+ ctx.fillRect(boundaryPx, top, right - boundaryPx, bottom - top);
146
+ } else {
147
+ ctx.fillRect(left, top, boundaryPx - left, bottom - top);
148
+ }
149
+ ctx.globalAlpha = 1;
150
+ }
151
+
152
+ function drawCurve(b0, b1) {
153
+ ctx.strokeStyle = "#0a8f7b";
154
+ ctx.lineWidth = 3;
155
+ ctx.beginPath();
156
+
157
+ const steps = 600;
158
+ for (let i = 0; i <= steps; i += 1) {
159
+ const x = X_MIN + (i / steps) * (X_MAX - X_MIN);
160
+ const y = modelProbability(x, b0, b1);
161
+ const px = xToCanvas(x);
162
+ const py = yToCanvas(y);
163
+
164
+ if (i === 0) {
165
+ ctx.moveTo(px, py);
166
+ } else {
167
+ ctx.lineTo(px, py);
168
+ }
169
+ }
170
+
171
+ ctx.stroke();
172
+ }
173
+
174
+ function drawDerivative(b0, b1) {
175
+ ctx.strokeStyle = "#f2b84b";
176
+ ctx.lineWidth = 2;
177
+ ctx.setLineDash([6, 4]);
178
+ ctx.beginPath();
179
+
180
+ const steps = 600;
181
+ for (let i = 0; i <= steps; i += 1) {
182
+ const x = X_MIN + (i / steps) * (X_MAX - X_MIN);
183
+ const p = modelProbability(x, b0, b1);
184
+ const derivative = Math.abs(b1 * p * (1 - p));
185
+ const y = Math.min(1, derivative * 4);
186
+
187
+ const px = xToCanvas(x);
188
+ const py = yToCanvas(y);
189
+
190
+ if (i === 0) {
191
+ ctx.moveTo(px, py);
192
+ } else {
193
+ ctx.lineTo(px, py);
194
+ }
195
+ }
196
+
197
+ ctx.stroke();
198
+ ctx.setLineDash([]);
199
+ }
200
+
201
+ function drawProbeAndSamples(b0, b1, t, probeX) {
202
+ const probeP = modelProbability(probeX, b0, b1);
203
+
204
+ for (const sampleX of points) {
205
+ const p = modelProbability(sampleX, b0, b1);
206
+ const positive = p >= t;
207
+ ctx.fillStyle = positive ? "#0a8f7b" : "#d8534f";
208
+ ctx.beginPath();
209
+ ctx.arc(xToCanvas(sampleX), yToCanvas(p), 5, 0, Math.PI * 2);
210
+ ctx.fill();
211
+ }
212
+
213
+ ctx.strokeStyle = "#14343f";
214
+ ctx.setLineDash([4, 4]);
215
+ ctx.beginPath();
216
+ ctx.moveTo(xToCanvas(probeX), yToCanvas(0));
217
+ ctx.lineTo(xToCanvas(probeX), yToCanvas(probeP));
218
+ ctx.stroke();
219
+ ctx.setLineDash([]);
220
+
221
+ ctx.fillStyle = "#14343f";
222
+ ctx.beginPath();
223
+ ctx.arc(xToCanvas(probeX), yToCanvas(probeP), 6, 0, Math.PI * 2);
224
+ ctx.fill();
225
+
226
+ ctx.font = "12px 'Avenir Next', sans-serif";
227
+ ctx.fillText(`probe p=${format(probeP)}`, xToCanvas(probeX) + 8, yToCanvas(probeP) - 8);
228
+ }
229
+
230
+ function renderStats(b0, b1, t, probeX) {
231
+ const probeP = modelProbability(probeX, b0, b1);
232
+
233
+ let boundaryText = "No finite boundary (b1≈0)";
234
+ if (Math.abs(b1) > 1e-9) {
235
+ const logit = Math.log(t / (1 - t));
236
+ const boundaryX = (logit - b0) / b1;
237
+ boundaryText = `Decision boundary x*: ${format(boundaryX)}`;
238
+ }
239
+
240
+ const positives = points.filter((x) => modelProbability(x, b0, b1) >= t).length;
241
+ const negatives = points.length - positives;
242
+
243
+ stats.innerHTML = `
244
+ <span>Equation: p = 1/(1+exp(-(${format(b0)} + ${format(b1)}x)))</span>
245
+ <span>Threshold t = ${format(t, 2)}</span>
246
+ <span>Probe: x = ${format(probeX, 2)}, p = ${format(probeP)}</span>
247
+ <span>${boundaryText}</span>
248
+ <span>Sample points: ${points.length} total</span>
249
+ <span>Class counts by t: ${positives} positive, ${negatives} negative</span>
250
+ `;
251
+ }
252
+
253
+ function updateLabelValues() {
254
+ valueLabels.b0.textContent = format(controls.b0.value, 2);
255
+ valueLabels.b1.textContent = format(controls.b1.value, 2);
256
+ valueLabels.t.textContent = format(controls.t.value, 2);
257
+ valueLabels.probeX.textContent = format(controls.probeX.value, 2);
258
+ }
259
+
260
+ function render() {
261
+ const b0 = Number(controls.b0.value);
262
+ const b1 = Number(controls.b1.value);
263
+ const t = Number(controls.t.value);
264
+ const probeX = Number(controls.probeX.value);
265
+
266
+ updateLabelValues();
267
+
268
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
269
+ drawAxes(controls.showGrid.checked);
270
+
271
+ if (controls.showShade.checked) {
272
+ drawRegionShade(b0, b1, t);
273
+ }
274
+
275
+ drawThresholdLine(t);
276
+ drawCurve(b0, b1);
277
+
278
+ if (controls.showDerivative.checked) {
279
+ drawDerivative(b0, b1);
280
+ }
281
+
282
+ drawProbeAndSamples(b0, b1, t, probeX);
283
+ renderStats(b0, b1, t, probeX);
284
+ }
285
+
286
+ function reset() {
287
+ controls.b0.value = 0;
288
+ controls.b1.value = 1;
289
+ controls.t.value = 0.5;
290
+ controls.probeX.value = 0;
291
+ controls.showGrid.checked = true;
292
+ controls.showShade.checked = true;
293
+ controls.showDerivative.checked = false;
294
+ controls.preset.value = "default";
295
+ points.length = 0;
296
+ stopAnimation();
297
+ render();
298
+ }
299
+
300
+ function applyPreset(presetKey) {
301
+ const presetMap = {
302
+ default: { b0: 0, b1: 1, t: 0.5 },
303
+ steep: { b0: -1, b1: 3, t: 0.5 },
304
+ reversed: { b0: 0.5, b1: -2.2, t: 0.5 },
305
+ "high-threshold": { b0: 0, b1: 1, t: 0.8 },
306
+ };
307
+
308
+ const preset = presetMap[presetKey] || presetMap.default;
309
+ controls.b0.value = preset.b0;
310
+ controls.b1.value = preset.b1;
311
+ controls.t.value = preset.t;
312
+ render();
313
+ }
314
+
315
+ function startAnimation() {
316
+ if (animationTimer) return;
317
+
318
+ controls.animateBtn.textContent = "Stop Animation";
319
+
320
+ animationTimer = setInterval(() => {
321
+ const current = Number(controls.probeX.value);
322
+ let next = current + animationDirection * 0.18;
323
+
324
+ if (next > X_MAX) {
325
+ next = X_MAX;
326
+ animationDirection = -1;
327
+ }
328
+ if (next < X_MIN) {
329
+ next = X_MIN;
330
+ animationDirection = 1;
331
+ }
332
+
333
+ controls.probeX.value = next;
334
+ render();
335
+ }, 30);
336
+ }
337
+
338
+ function stopAnimation() {
339
+ if (animationTimer) {
340
+ clearInterval(animationTimer);
341
+ animationTimer = null;
342
+ }
343
+ controls.animateBtn.textContent = "Animate Probe";
344
+ }
345
+
346
+ for (const key of ["b0", "b1", "t", "probeX", "showGrid", "showShade", "showDerivative"]) {
347
+ controls[key].addEventListener("input", render);
348
+ }
349
+
350
+ controls.resetBtn.addEventListener("click", reset);
351
+
352
+ controls.preset.addEventListener("change", (event) => {
353
+ applyPreset(event.target.value);
354
+ });
355
+
356
+ controls.animateBtn.addEventListener("click", () => {
357
+ if (animationTimer) {
358
+ stopAnimation();
359
+ } else {
360
+ startAnimation();
361
+ }
362
+ });
363
+
364
+ canvas.addEventListener("click", (event) => {
365
+ const rect = canvas.getBoundingClientRect();
366
+ const clickX = event.clientX - rect.left;
367
+ const x = canvasToX((clickX / rect.width) * canvas.width);
368
+
369
+ if (x < X_MIN || x > X_MAX) {
370
+ return;
371
+ }
372
+
373
+ points.push(Number(x.toFixed(2)));
374
+ if (points.length > 30) {
375
+ points.shift();
376
+ }
377
+
378
+ render();
379
+ });
380
+
381
+ reset();
logistic-regression/confusion-matrix.html ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>DDW Interface - Confusion Matrix Practice</title>
7
+ <link rel="stylesheet" href="styles.css" />
8
+ </head>
9
+ <body>
10
+ <header class="site-header">
11
+ <div>
12
+ <p class="kicker">Data Driven World</p>
13
+ <h1>Confusion Matrix Practice (Page 3)</h1>
14
+ </div>
15
+ <nav>
16
+ <a href="index.html">Home</a>
17
+ <a href="simple-sigmoid.html">Simple Sigmoid</a>
18
+ <a class="active" href="confusion-matrix.html">Confusion Matrix (Page 3)</a>
19
+ <a href="sigmoid.html">Sigmoid Function</a>
20
+ <a href="cost-visualization.html">Cost Function</a>
21
+ <a href="about.html">Notes</a>
22
+ </nav>
23
+ </header>
24
+
25
+ <main class="layout">
26
+ <section class="panel controls">
27
+ <h2>Exercise Setup</h2>
28
+
29
+ <label for="classMode">Classification type</label>
30
+ <select id="classMode">
31
+ <option value="binary">2 classes (Positive / Negative)</option>
32
+ <option value="three">3 classes (A / B / C)</option>
33
+ </select>
34
+
35
+ <label for="threshold">Threshold t: <span id="thresholdValue">0.50</span></label>
36
+ <input id="threshold" type="range" min="0.30" max="0.80" step="0.01" value="0.50" />
37
+
38
+ <p class="hint" id="ruleText"></p>
39
+
40
+ <div class="button-row">
41
+ <button id="checkBtn" type="button">Check My Answers</button>
42
+ <button id="showSolutionBtn" type="button">Show Full Solution</button>
43
+ </div>
44
+
45
+ <div class="button-row">
46
+ <button id="newSamplesBtn" type="button">Generate New Samples</button>
47
+ <button id="resetBtn" type="button">Reset Inputs</button>
48
+ </div>
49
+
50
+ <div class="stats" id="feedbackStats"></div>
51
+ </section>
52
+
53
+ <section class="panel chart-panel confusion-panel">
54
+ <h2>Student Task</h2>
55
+ <p class="hint">
56
+ Six samples are given. Prediction for Sample 1 and 2 are worked examples. You calculate Sample 3 to 6, then
57
+ fill confusion matrix and metrics.
58
+ </p>
59
+
60
+ <div id="sampleArea"></div>
61
+ <div id="matrixArea"></div>
62
+ <div id="metricArea"></div>
63
+
64
+ <section class="solution-block" id="solutionBlock" hidden>
65
+ <h3>Detailed Process and Solutions</h3>
66
+ <div id="solutionText"></div>
67
+ </section>
68
+ </section>
69
+ </main>
70
+
71
+ <script src="confusion.js"></script>
72
+ </body>
73
+ </html>
logistic-regression/confusion.js ADDED
@@ -0,0 +1,608 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const controls = {
2
+ classMode: document.getElementById("classMode"),
3
+ threshold: document.getElementById("threshold"),
4
+ thresholdValue: document.getElementById("thresholdValue"),
5
+ ruleText: document.getElementById("ruleText"),
6
+ checkBtn: document.getElementById("checkBtn"),
7
+ showSolutionBtn: document.getElementById("showSolutionBtn"),
8
+ newSamplesBtn: document.getElementById("newSamplesBtn"),
9
+ resetBtn: document.getElementById("resetBtn"),
10
+ };
11
+
12
+ const sampleArea = document.getElementById("sampleArea");
13
+ const matrixArea = document.getElementById("matrixArea");
14
+ const metricArea = document.getElementById("metricArea");
15
+ const feedbackStats = document.getElementById("feedbackStats");
16
+ const solutionBlock = document.getElementById("solutionBlock");
17
+ const solutionText = document.getElementById("solutionText");
18
+
19
+ let binarySamples = [];
20
+ let threeClassSamples = [];
21
+
22
+ function fmt(num, digits = 2) {
23
+ return Number(num).toFixed(digits);
24
+ }
25
+
26
+ function round3(num) {
27
+ return Number(num).toFixed(3);
28
+ }
29
+
30
+ function round2(num) {
31
+ return Math.round(num * 100) / 100;
32
+ }
33
+
34
+ function rand(min, max) {
35
+ return min + Math.random() * (max - min);
36
+ }
37
+
38
+ function generateBinarySamples() {
39
+ const actualPattern = ["Positive", "Negative", "Positive", "Negative", "Positive", "Negative"];
40
+ const samples = [];
41
+
42
+ for (let i = 0; i < 6; i += 1) {
43
+ const actual = actualPattern[i];
44
+ const feature = round2(rand(0.2, 2.6));
45
+ const pPos = actual === "Positive" ? round2(rand(0.38, 0.92)) : round2(rand(0.12, 0.78));
46
+ samples.push({ id: `S${i + 1}`, feature, actual, pPos });
47
+ }
48
+
49
+ binarySamples = samples;
50
+ }
51
+
52
+ function generateThreeProbabilities(actual) {
53
+ const labels = ["A", "B", "C"];
54
+ const weights = {
55
+ A: rand(0.1, 0.45),
56
+ B: rand(0.1, 0.45),
57
+ C: rand(0.1, 0.45),
58
+ };
59
+
60
+ weights[actual] += rand(0.18, 0.5);
61
+ const total = labels.reduce((sum, label) => sum + weights[label], 0);
62
+
63
+ let pA = round2(weights.A / total);
64
+ let pB = round2(weights.B / total);
65
+ if (pA + pB > 0.98) {
66
+ const scale = 0.98 / (pA + pB);
67
+ pA = round2(pA * scale);
68
+ pB = round2(pB * scale);
69
+ }
70
+ const pC = round2(1 - pA - pB);
71
+
72
+ return { pA, pB, pC };
73
+ }
74
+
75
+ function generateThreeClassSamples() {
76
+ const actualPattern = ["A", "B", "C", "B", "C", "A"];
77
+ const samples = [];
78
+
79
+ for (let i = 0; i < 6; i += 1) {
80
+ const actual = actualPattern[i];
81
+ const feature = round2(rand(0.5, 2.4));
82
+ const probs = generateThreeProbabilities(actual);
83
+ samples.push({ id: `S${i + 1}`, feature, actual, ...probs });
84
+ }
85
+
86
+ threeClassSamples = samples;
87
+ }
88
+
89
+ function generateAllSamples() {
90
+ generateBinarySamples();
91
+ generateThreeClassSamples();
92
+ }
93
+
94
+ function computeBinaryPrediction(sample, threshold) {
95
+ return sample.pPos >= threshold ? "Positive" : "Negative";
96
+ }
97
+
98
+ function computeThreePrediction(sample, threshold) {
99
+ if (sample.pA >= threshold) return "A";
100
+ if (sample.pB >= threshold) return "B";
101
+ return "C";
102
+ }
103
+
104
+ function computeBinaryResults(threshold) {
105
+ const rows = binarySamples.map((sample) => {
106
+ const predicted = computeBinaryPrediction(sample, threshold);
107
+ return { ...sample, predicted };
108
+ });
109
+
110
+ let tp = 0;
111
+ let fp = 0;
112
+ let tn = 0;
113
+ let fn = 0;
114
+
115
+ for (const row of rows) {
116
+ if (row.actual === "Positive" && row.predicted === "Positive") tp += 1;
117
+ if (row.actual === "Negative" && row.predicted === "Positive") fp += 1;
118
+ if (row.actual === "Negative" && row.predicted === "Negative") tn += 1;
119
+ if (row.actual === "Positive" && row.predicted === "Negative") fn += 1;
120
+ }
121
+
122
+ const precision = tp + fp === 0 ? 0 : tp / (tp + fp);
123
+ const recall = tp + fn === 0 ? 0 : tp / (tp + fn);
124
+ const sensitivity = recall;
125
+ const specificity = tn + fp === 0 ? 0 : tn / (tn + fp);
126
+
127
+ return {
128
+ rows,
129
+ matrix: {
130
+ Positive: { Positive: tp, Negative: fn },
131
+ Negative: { Positive: fp, Negative: tn },
132
+ },
133
+ metrics: { precision, recall, sensitivity, specificity },
134
+ };
135
+ }
136
+
137
+ function getOneVsRestMetrics(matrix, className) {
138
+ const classes = ["A", "B", "C"];
139
+ const tp = matrix[className][className];
140
+
141
+ let fp = 0;
142
+ let fn = 0;
143
+ for (const c of classes) {
144
+ if (c !== className) {
145
+ fp += matrix[c][className];
146
+ fn += matrix[className][c];
147
+ }
148
+ }
149
+
150
+ const total = classes.reduce((sum, r) => sum + classes.reduce((acc, c) => acc + matrix[r][c], 0), 0);
151
+ const tn = total - tp - fp - fn;
152
+
153
+ const precision = tp + fp === 0 ? 0 : tp / (tp + fp);
154
+ const recall = tp + fn === 0 ? 0 : tp / (tp + fn);
155
+ const specificity = tn + fp === 0 ? 0 : tn / (tn + fp);
156
+
157
+ return { precision, recall, sensitivity: recall, specificity };
158
+ }
159
+
160
+ function computeThreeResults(threshold) {
161
+ const rows = threeClassSamples.map((sample) => {
162
+ const predicted = computeThreePrediction(sample, threshold);
163
+ return { ...sample, predicted };
164
+ });
165
+
166
+ const classes = ["A", "B", "C"];
167
+ const matrix = { A: { A: 0, B: 0, C: 0 }, B: { A: 0, B: 0, C: 0 }, C: { A: 0, B: 0, C: 0 } };
168
+
169
+ for (const row of rows) {
170
+ matrix[row.actual][row.predicted] += 1;
171
+ }
172
+
173
+ const perClass = classes.map((className) => getOneVsRestMetrics(matrix, className));
174
+ const macro = {
175
+ precision: perClass.reduce((s, m) => s + m.precision, 0) / classes.length,
176
+ recall: perClass.reduce((s, m) => s + m.recall, 0) / classes.length,
177
+ sensitivity: perClass.reduce((s, m) => s + m.sensitivity, 0) / classes.length,
178
+ specificity: perClass.reduce((s, m) => s + m.specificity, 0) / classes.length,
179
+ };
180
+
181
+ return { rows, matrix, metrics: macro, perClass };
182
+ }
183
+
184
+ function getCurrentResults() {
185
+ const mode = controls.classMode.value;
186
+ const t = Number(controls.threshold.value);
187
+ return mode === "binary" ? computeBinaryResults(t) : computeThreeResults(t);
188
+ }
189
+
190
+ function samplePredictionSelect(rowId, options) {
191
+ return `<select data-role="sample-pred" data-id="${rowId}">${options
192
+ .map((opt) => `<option value="${opt}">${opt}</option>`)
193
+ .join("")}</select>`;
194
+ }
195
+
196
+ function renderSamples(results) {
197
+ const mode = controls.classMode.value;
198
+ const rows = results.rows;
199
+
200
+ if (mode === "binary") {
201
+ sampleArea.innerHTML = `
202
+ <h3>1) Predict the class for each sample</h3>
203
+ <table class="exercise-table">
204
+ <thead>
205
+ <tr>
206
+ <th>Sample</th>
207
+ <th>Feature x</th>
208
+ <th>Actual Class</th>
209
+ <th>p(Positive)</th>
210
+ <th>Your Predicted Class</th>
211
+ </tr>
212
+ </thead>
213
+ <tbody>
214
+ ${rows
215
+ .map((row, index) => {
216
+ if (index < 2) {
217
+ return `
218
+ <tr>
219
+ <td>${row.id}</td>
220
+ <td>${fmt(row.feature)}</td>
221
+ <td>${row.actual}</td>
222
+ <td>${fmt(row.pPos)}</td>
223
+ <td><strong>${row.predicted}</strong> <span class="worked-tag">(worked example)</span></td>
224
+ </tr>
225
+ `;
226
+ }
227
+
228
+ return `
229
+ <tr>
230
+ <td>${row.id}</td>
231
+ <td>${fmt(row.feature)}</td>
232
+ <td>${row.actual}</td>
233
+ <td>${fmt(row.pPos)}</td>
234
+ <td>${samplePredictionSelect(row.id, ["Positive", "Negative"])}</td>
235
+ </tr>
236
+ `;
237
+ })
238
+ .join("")}
239
+ </tbody>
240
+ </table>
241
+ `;
242
+ return;
243
+ }
244
+
245
+ sampleArea.innerHTML = `
246
+ <h3>1) Predict the class for each sample</h3>
247
+ <table class="exercise-table">
248
+ <thead>
249
+ <tr>
250
+ <th>Sample</th>
251
+ <th>Feature x</th>
252
+ <th>Actual Class</th>
253
+ <th>p(A)</th>
254
+ <th>p(B)</th>
255
+ <th>p(C)</th>
256
+ <th>Your Predicted Class</th>
257
+ </tr>
258
+ </thead>
259
+ <tbody>
260
+ ${rows
261
+ .map((row, index) => {
262
+ if (index < 2) {
263
+ return `
264
+ <tr>
265
+ <td>${row.id}</td>
266
+ <td>${fmt(row.feature)}</td>
267
+ <td>${row.actual}</td>
268
+ <td>${fmt(row.pA)}</td>
269
+ <td>${fmt(row.pB)}</td>
270
+ <td>${fmt(row.pC)}</td>
271
+ <td><strong>${row.predicted}</strong> <span class="worked-tag">(worked example)</span></td>
272
+ </tr>
273
+ `;
274
+ }
275
+
276
+ return `
277
+ <tr>
278
+ <td>${row.id}</td>
279
+ <td>${fmt(row.feature)}</td>
280
+ <td>${row.actual}</td>
281
+ <td>${fmt(row.pA)}</td>
282
+ <td>${fmt(row.pB)}</td>
283
+ <td>${fmt(row.pC)}</td>
284
+ <td>${samplePredictionSelect(row.id, ["A", "B", "C"])}</td>
285
+ </tr>
286
+ `;
287
+ })
288
+ .join("")}
289
+ </tbody>
290
+ </table>
291
+ `;
292
+ }
293
+
294
+ function renderMatrixInputs() {
295
+ const mode = controls.classMode.value;
296
+
297
+ if (mode === "binary") {
298
+ matrixArea.innerHTML = `
299
+ <h3>2) Fill the confusion matrix</h3>
300
+ <p class="hint">Rows = Actual class, Columns = Predicted class.</p>
301
+ <table class="exercise-table matrix-table">
302
+ <thead>
303
+ <tr><th>Actual \ Predicted</th><th>Positive</th><th>Negative</th></tr>
304
+ </thead>
305
+ <tbody>
306
+ <tr>
307
+ <th>Positive</th>
308
+ <td><input type="number" min="0" data-role="matrix" data-key="Positive-Positive" /></td>
309
+ <td><input type="number" min="0" data-role="matrix" data-key="Positive-Negative" /></td>
310
+ </tr>
311
+ <tr>
312
+ <th>Negative</th>
313
+ <td><input type="number" min="0" data-role="matrix" data-key="Negative-Positive" /></td>
314
+ <td><input type="number" min="0" data-role="matrix" data-key="Negative-Negative" /></td>
315
+ </tr>
316
+ </tbody>
317
+ </table>
318
+ `;
319
+ return;
320
+ }
321
+
322
+ matrixArea.innerHTML = `
323
+ <h3>2) Fill the confusion matrix</h3>
324
+ <p class="hint">Rows = Actual class, Columns = Predicted class.</p>
325
+ <table class="exercise-table matrix-table">
326
+ <thead>
327
+ <tr><th>Actual \ Predicted</th><th>A</th><th>B</th><th>C</th></tr>
328
+ </thead>
329
+ <tbody>
330
+ <tr>
331
+ <th>A</th>
332
+ <td><input type="number" min="0" data-role="matrix" data-key="A-A" /></td>
333
+ <td><input type="number" min="0" data-role="matrix" data-key="A-B" /></td>
334
+ <td><input type="number" min="0" data-role="matrix" data-key="A-C" /></td>
335
+ </tr>
336
+ <tr>
337
+ <th>B</th>
338
+ <td><input type="number" min="0" data-role="matrix" data-key="B-A" /></td>
339
+ <td><input type="number" min="0" data-role="matrix" data-key="B-B" /></td>
340
+ <td><input type="number" min="0" data-role="matrix" data-key="B-C" /></td>
341
+ </tr>
342
+ <tr>
343
+ <th>C</th>
344
+ <td><input type="number" min="0" data-role="matrix" data-key="C-A" /></td>
345
+ <td><input type="number" min="0" data-role="matrix" data-key="C-B" /></td>
346
+ <td><input type="number" min="0" data-role="matrix" data-key="C-C" /></td>
347
+ </tr>
348
+ </tbody>
349
+ </table>
350
+ `;
351
+ }
352
+
353
+ function renderMetricInputs() {
354
+ const mode = controls.classMode.value;
355
+ const detail = mode === "binary" ? "(for Positive class)" : "(macro-average across A, B, C)";
356
+ metricArea.innerHTML = `
357
+ <h3>3) Calculate metrics ${detail}</h3>
358
+ <p class="hint">Enter decimal values (e.g., 0.667).</p>
359
+ <table class="exercise-table metric-table">
360
+ <thead>
361
+ <tr><th>Metric</th><th>Your value</th></tr>
362
+ </thead>
363
+ <tbody>
364
+ <tr><th>Precision</th><td><input type="number" step="0.001" data-role="metric" data-key="precision" /></td></tr>
365
+ <tr><th>Recall</th><td><input type="number" step="0.001" data-role="metric" data-key="recall" /></td></tr>
366
+ <tr><th>Sensitivity</th><td><input type="number" step="0.001" data-role="metric" data-key="sensitivity" /></td></tr>
367
+ <tr><th>Specificity</th><td><input type="number" step="0.001" data-role="metric" data-key="specificity" /></td></tr>
368
+ </tbody>
369
+ </table>
370
+ `;
371
+ }
372
+
373
+ function updateRuleText() {
374
+ const mode = controls.classMode.value;
375
+ const threshold = Number(controls.threshold.value);
376
+
377
+ controls.thresholdValue.textContent = fmt(threshold);
378
+
379
+ if (mode === "binary") {
380
+ controls.ruleText.textContent =
381
+ "Rule: predict Positive if p(Positive) >= t, otherwise predict Negative. (Default t = 0.50)";
382
+ } else {
383
+ controls.ruleText.textContent =
384
+ "Rule: check A first, then B. If p(A) >= t predict A; else if p(B) >= t predict B; otherwise predict C.";
385
+ }
386
+ }
387
+
388
+ function renderPage() {
389
+ const results = getCurrentResults();
390
+ updateRuleText();
391
+ renderSamples(results);
392
+ renderMatrixInputs();
393
+ renderMetricInputs();
394
+ feedbackStats.innerHTML = "";
395
+ }
396
+
397
+ function readUserSampleAnswers() {
398
+ const selects = [...document.querySelectorAll('select[data-role="sample-pred"]')];
399
+ const map = {};
400
+ for (const select of selects) {
401
+ map[select.dataset.id] = select.value;
402
+ }
403
+ return map;
404
+ }
405
+
406
+ function readUserMatrix() {
407
+ const inputs = [...document.querySelectorAll('input[data-role="matrix"]')];
408
+ const matrix = {};
409
+ for (const input of inputs) {
410
+ matrix[input.dataset.key] = input.value === "" ? NaN : Number(input.value);
411
+ }
412
+ return matrix;
413
+ }
414
+
415
+ function readUserMetrics() {
416
+ const inputs = [...document.querySelectorAll('input[data-role="metric"]')];
417
+ const metrics = {};
418
+ for (const input of inputs) {
419
+ metrics[input.dataset.key] = input.value === "" ? NaN : Number(input.value);
420
+ }
421
+ return metrics;
422
+ }
423
+
424
+ function expectedMatrixFlat(results) {
425
+ const mode = controls.classMode.value;
426
+ if (mode === "binary") {
427
+ return {
428
+ "Positive-Positive": results.matrix.Positive.Positive,
429
+ "Positive-Negative": results.matrix.Positive.Negative,
430
+ "Negative-Positive": results.matrix.Negative.Positive,
431
+ "Negative-Negative": results.matrix.Negative.Negative,
432
+ };
433
+ }
434
+
435
+ return {
436
+ "A-A": results.matrix.A.A,
437
+ "A-B": results.matrix.A.B,
438
+ "A-C": results.matrix.A.C,
439
+ "B-A": results.matrix.B.A,
440
+ "B-B": results.matrix.B.B,
441
+ "B-C": results.matrix.B.C,
442
+ "C-A": results.matrix.C.A,
443
+ "C-B": results.matrix.C.B,
444
+ "C-C": results.matrix.C.C,
445
+ };
446
+ }
447
+
448
+ function approximatelyEqual(a, b, eps = 0.02) {
449
+ return Math.abs(a - b) <= eps;
450
+ }
451
+
452
+ function checkAnswers() {
453
+ const results = getCurrentResults();
454
+
455
+ const expectedPreds = {};
456
+ for (const row of results.rows.slice(2)) {
457
+ expectedPreds[row.id] = row.predicted;
458
+ }
459
+
460
+ const userPreds = readUserSampleAnswers();
461
+ let predCorrect = 0;
462
+ const predTotal = Object.keys(expectedPreds).length;
463
+ for (const id of Object.keys(expectedPreds)) {
464
+ if (userPreds[id] === expectedPreds[id]) {
465
+ predCorrect += 1;
466
+ }
467
+ }
468
+
469
+ const userMatrix = readUserMatrix();
470
+ const expectedMatrix = expectedMatrixFlat(results);
471
+ let matrixCorrect = 0;
472
+ const matrixTotal = Object.keys(expectedMatrix).length;
473
+ for (const key of Object.keys(expectedMatrix)) {
474
+ if (userMatrix[key] === expectedMatrix[key]) {
475
+ matrixCorrect += 1;
476
+ }
477
+ }
478
+
479
+ const userMetrics = readUserMetrics();
480
+ const expectedMetrics = results.metrics;
481
+ let metricCorrect = 0;
482
+ const metricTotal = 4;
483
+ for (const metricName of ["precision", "recall", "sensitivity", "specificity"]) {
484
+ if (approximatelyEqual(userMetrics[metricName], expectedMetrics[metricName])) {
485
+ metricCorrect += 1;
486
+ }
487
+ }
488
+
489
+ feedbackStats.innerHTML = `
490
+ <span>Prediction check: ${predCorrect}/${predTotal} correct</span>
491
+ <span>Confusion matrix check: ${matrixCorrect}/${matrixTotal} correct</span>
492
+ <span>Metric check: ${metricCorrect}/${metricTotal} correct (tolerance +/- 0.02)</span>
493
+ <span>Expected metrics: Precision=${round3(expectedMetrics.precision)}, Recall=${round3(
494
+ expectedMetrics.recall,
495
+ )}, Sensitivity=${round3(expectedMetrics.sensitivity)}, Specificity=${round3(expectedMetrics.specificity)}</span>
496
+ `;
497
+ }
498
+
499
+ function renderBinarySolution(results) {
500
+ const steps = results.rows
501
+ .map((row) => {
502
+ return `<li>${row.id}: p(Positive)=${fmt(row.pPos)}; compare with t=${fmt(
503
+ controls.threshold.value,
504
+ )} => predicted <strong>${row.predicted}</strong>; actual ${row.actual}.</li>`;
505
+ })
506
+ .join("");
507
+
508
+ solutionText.innerHTML = `
509
+ <h4>Step A: Classify all 6 samples</h4>
510
+ <ol>${steps}</ol>
511
+
512
+ <h4>Step B: Build confusion matrix (rows=actual, columns=predicted)</h4>
513
+ <p>TP=${results.matrix.Positive.Positive}, FN=${results.matrix.Positive.Negative}, FP=${results.matrix.Negative.Positive}, TN=${results.matrix.Negative.Negative}</p>
514
+
515
+ <table class="exercise-table matrix-table">
516
+ <thead><tr><th>Actual \\ Predicted</th><th>Positive</th><th>Negative</th></tr></thead>
517
+ <tbody>
518
+ <tr><th>Positive</th><td>${results.matrix.Positive.Positive}</td><td>${results.matrix.Positive.Negative}</td></tr>
519
+ <tr><th>Negative</th><td>${results.matrix.Negative.Positive}</td><td>${results.matrix.Negative.Negative}</td></tr>
520
+ </tbody>
521
+ </table>
522
+
523
+ <h4>Step C: Metrics</h4>
524
+ <p>Precision = TP/(TP+FP) = ${results.matrix.Positive.Positive}/(${results.matrix.Positive.Positive}+${results.matrix.Negative.Positive}) = ${round3(
525
+ results.metrics.precision,
526
+ )}</p>
527
+ <p>Recall = TP/(TP+FN) = ${results.matrix.Positive.Positive}/(${results.matrix.Positive.Positive}+${results.matrix.Positive.Negative}) = ${round3(
528
+ results.metrics.recall,
529
+ )}</p>
530
+ <p>Sensitivity = Recall = ${round3(results.metrics.sensitivity)}</p>
531
+ <p>Specificity = TN/(TN+FP) = ${results.matrix.Negative.Negative}/(${results.matrix.Negative.Negative}+${results.matrix.Negative.Positive}) = ${round3(
532
+ results.metrics.specificity,
533
+ )}</p>
534
+ `;
535
+ }
536
+
537
+ function renderThreeSolution(results) {
538
+ const threshold = Number(controls.threshold.value);
539
+ const steps = results.rows
540
+ .map((row) => {
541
+ return `<li>${row.id}: p(A)=${fmt(row.pA)}, p(B)=${fmt(row.pB)}, p(C)=${fmt(
542
+ row.pC,
543
+ )}. Since t=${fmt(threshold)}, prediction is <strong>${row.predicted}</strong>; actual ${row.actual}.</li>`;
544
+ })
545
+ .join("");
546
+
547
+ const classes = ["A", "B", "C"];
548
+ const perClass = classes
549
+ .map((c, idx) => {
550
+ return `<li>Class ${c}: Precision=${round3(results.perClass[idx].precision)}, Recall/Sensitivity=${round3(
551
+ results.perClass[idx].recall,
552
+ )}, Specificity=${round3(results.perClass[idx].specificity)}</li>`;
553
+ })
554
+ .join("");
555
+
556
+ solutionText.innerHTML = `
557
+ <h4>Step A: Classify all 6 samples</h4>
558
+ <ol>${steps}</ol>
559
+
560
+ <h4>Step B: Build 3x3 confusion matrix (rows=actual, columns=predicted)</h4>
561
+ <table class="exercise-table matrix-table">
562
+ <thead><tr><th>Actual \\ Predicted</th><th>A</th><th>B</th><th>C</th></tr></thead>
563
+ <tbody>
564
+ <tr><th>A</th><td>${results.matrix.A.A}</td><td>${results.matrix.A.B}</td><td>${results.matrix.A.C}</td></tr>
565
+ <tr><th>B</th><td>${results.matrix.B.A}</td><td>${results.matrix.B.B}</td><td>${results.matrix.B.C}</td></tr>
566
+ <tr><th>C</th><td>${results.matrix.C.A}</td><td>${results.matrix.C.B}</td><td>${results.matrix.C.C}</td></tr>
567
+ </tbody>
568
+ </table>
569
+
570
+ <h4>Step C: One-vs-rest metrics for each class, then macro-average</h4>
571
+ <ol>${perClass}</ol>
572
+
573
+ <p><strong>Macro Precision</strong> = ${round3(results.metrics.precision)}</p>
574
+ <p><strong>Macro Recall</strong> = ${round3(results.metrics.recall)}</p>
575
+ <p><strong>Macro Sensitivity</strong> = ${round3(results.metrics.sensitivity)}</p>
576
+ <p><strong>Macro Specificity</strong> = ${round3(results.metrics.specificity)}</p>
577
+ `;
578
+ }
579
+
580
+ function showSolution() {
581
+ const results = getCurrentResults();
582
+ if (controls.classMode.value === "binary") {
583
+ renderBinarySolution(results);
584
+ } else {
585
+ renderThreeSolution(results);
586
+ }
587
+ solutionBlock.hidden = false;
588
+ }
589
+
590
+ function resetInputs() {
591
+ renderPage();
592
+ solutionBlock.hidden = true;
593
+ }
594
+
595
+ function regenerateSamples() {
596
+ generateAllSamples();
597
+ resetInputs();
598
+ }
599
+
600
+ controls.classMode.addEventListener("change", resetInputs);
601
+ controls.threshold.addEventListener("input", resetInputs);
602
+ controls.checkBtn.addEventListener("click", checkAnswers);
603
+ controls.showSolutionBtn.addEventListener("click", showSolution);
604
+ controls.newSamplesBtn.addEventListener("click", regenerateSamples);
605
+ controls.resetBtn.addEventListener("click", resetInputs);
606
+
607
+ generateAllSamples();
608
+ renderPage();
logistic-regression/cost-visualization.html ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>DDW Interface - Cost Function Visualization</title>
7
+ <link rel="stylesheet" href="styles.css" />
8
+ </head>
9
+ <body>
10
+ <header class="site-header">
11
+ <div>
12
+ <p class="kicker">Data Driven World</p>
13
+ <h1>Visualization for Cost Function</h1>
14
+ </div>
15
+ <nav>
16
+ <a href="index.html">Home</a>
17
+ <a href="simple-sigmoid.html">Simple Sigmoid</a>
18
+ <a href="confusion-matrix.html">Confusion Matrix (Page 3)</a>
19
+ <a href="sigmoid.html">Sigmoid Function</a>
20
+ <a class="active" href="cost-visualization.html">Cost Function</a>
21
+ <a href="about.html">Notes</a>
22
+ </nav>
23
+ </header>
24
+
25
+ <main class="layout">
26
+ <section class="panel controls">
27
+ <h2>Gradient Descent Setup</h2>
28
+
29
+ <label for="caseSelect">Select model case</label>
30
+ <select id="caseSelect">
31
+ <option value="case1">Case 1: one intercept + one feature</option>
32
+ <option value="case2">Case 2: two features, no intercept</option>
33
+ </select>
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" />
44
+
45
+ <label for="startP1">Start parameter 1: <span id="p1Value">-4.00</span></label>
46
+ <input id="startP1" type="range" min="-6" max="6" step="0.1" value="-4" />
47
+
48
+ <label for="startP2">Start parameter 2: <span id="p2Value">4.00</span></label>
49
+ <input id="startP2" type="range" min="-6" max="6" step="0.1" value="4" />
50
+
51
+ <label for="speed">Automatic run speed (ms): <span id="speedValue">90</span></label>
52
+ <input id="speed" type="range" min="20" max="260" step="5" value="90" />
53
+
54
+ <div class="button-row">
55
+ <button id="runBtn" type="button">Run Gradient Descent</button>
56
+ <button id="stepBtn" type="button">Next Step</button>
57
+ </div>
58
+
59
+ <div class="button-row">
60
+ <button id="autoBtn" type="button">Automatic Run</button>
61
+ <button id="restartBtn" type="button">Restart Path</button>
62
+ </div>
63
+
64
+ <div class="button-row">
65
+ <button id="regenBtn" type="button">Regenerate Synthetic Data</button>
66
+ <button id="clearBtn" type="button">Clear</button>
67
+ </div>
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>
logistic-regression/cost.js ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const canvas = document.getElementById("costCanvas");
2
+ const ctx = canvas.getContext("2d");
3
+
4
+ const controls = {
5
+ caseSelect: document.getElementById("caseSelect"),
6
+ lr: document.getElementById("lr"),
7
+ lrInput: document.getElementById("lrInput"),
8
+ iters: document.getElementById("iters"),
9
+ p1: document.getElementById("startP1"),
10
+ p2: document.getElementById("startP2"),
11
+ speed: document.getElementById("speed"),
12
+ runBtn: document.getElementById("runBtn"),
13
+ stepBtn: document.getElementById("stepBtn"),
14
+ autoBtn: document.getElementById("autoBtn"),
15
+ restartBtn: document.getElementById("restartBtn"),
16
+ regenBtn: document.getElementById("regenBtn"),
17
+ clearBtn: document.getElementById("clearBtn"),
18
+ };
19
+
20
+ const labels = {
21
+ formula: document.getElementById("formulaText"),
22
+ lr: document.getElementById("lrValue"),
23
+ iters: document.getElementById("iterValue"),
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) {
68
+ const u1 = Math.max(1e-12, Math.random());
69
+ const u2 = Math.random();
70
+ const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
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
+
122
+ return total / data.length;
123
+ }
124
+
125
+ function gradFor(caseKey, params) {
126
+ const data = datasets[caseKey];
127
+ let g1 = 0;
128
+ let g2 = 0;
129
+
130
+ if (caseKey === "case1") {
131
+ const [b0, b1] = params;
132
+ for (const row of data) {
133
+ const p = sigmoid(b0 + b1 * row.x);
134
+ const diff = p - row.y;
135
+ g1 += diff;
136
+ g2 += diff * row.x;
137
+ }
138
+ } else {
139
+ const [w1, w2] = params;
140
+ for (const row of data) {
141
+ const p = sigmoid(w1 * row.x1 + w2 * row.x2);
142
+ const diff = p - row.y;
143
+ g1 += diff * row.x1;
144
+ g2 += diff * row.x2;
145
+ }
146
+ }
147
+
148
+ const m = data.length;
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() {
470
+ stopAnimation();
471
+
472
+ const caseKey = controls.caseSelect.value;
473
+ const start = [Number(controls.p1.value), Number(controls.p2.value)];
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
+
544
+ 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");
logistic-regression/index.html ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>DDW Interface - Home</title>
7
+ <link rel="stylesheet" href="styles.css" />
8
+ </head>
9
+ <body>
10
+ <header class="site-header">
11
+ <div>
12
+ <p class="kicker">Data Driven World</p>
13
+ <h1>Classification Categorical Data</h1>
14
+ </div>
15
+ <nav>
16
+ <a class="active" href="index.html">Home</a>
17
+ <a href="simple-sigmoid.html">Simple Sigmoid</a>
18
+ <a href="confusion-matrix.html">Confusion Matrix (Page 3)</a>
19
+ <a href="sigmoid.html">Sigmoid Function</a>
20
+ <a href="cost-visualization.html">Cost Function</a>
21
+ <a href="about.html">Notes</a>
22
+ </nav>
23
+ </header>
24
+
25
+ <main class="panel page-copy home-page">
26
+ <h2>Lecture Objective</h2>
27
+ <p>
28
+ The objective of this lecture is classification of categorical data. Use these interactive pages to understand
29
+ how logistic regression and threshold-based decisions classify observations into categories.
30
+ </p>
31
+
32
+ <section class="home-grid">
33
+ <article class="home-card">
34
+ <h3>Simple Sigmoid (Page 1)</h3>
35
+ <p>Basic illustration of the function 1/(1+np.exp(-z)) with a single z control.</p>
36
+ <a class="cta-button" href="simple-sigmoid.html">Simple Sigmoid</a>
37
+ </article>
38
+
39
+ <article class="home-card">
40
+ <h3>Confusion Matrix Practice (Page 3)</h3>
41
+ <p>Practice threshold-based 2-class and 3-class predictions, then build confusion matrix and key metrics.</p>
42
+ <a class="cta-button secondary" href="confusion-matrix.html">Confusion Matrix</a>
43
+ </article>
44
+
45
+ <article class="home-card">
46
+ <h3>Sigmoid Function</h3>
47
+ <p>Interactive plot of p = 1 / (1 + exp(-(b0 + b1x))) with sliders and chart actions.</p>
48
+ <a class="cta-button secondary" href="sigmoid.html">Sigmoid Function</a>
49
+ </article>
50
+
51
+ <article class="home-card">
52
+ <h3>Visualization for Cost Function</h3>
53
+ <p>Compare two logistic-model cases and track gradient descent trajectory on the cost surface.</p>
54
+ <a class="cta-button secondary" href="cost-visualization.html">Cost Function</a>
55
+ </article>
56
+ </section>
57
+ </main>
58
+ </body>
59
+ </html>
logistic-regression/sigmoid.html ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>DDW Interface - Sigmoid Function</title>
7
+ <link rel="stylesheet" href="styles.css" />
8
+ </head>
9
+ <body>
10
+ <header class="site-header">
11
+ <div>
12
+ <p class="kicker">Data Driven World</p>
13
+ <h1>Sigmoid Function Playground</h1>
14
+ </div>
15
+ <nav>
16
+ <a href="index.html">Home</a>
17
+ <a href="simple-sigmoid.html">Simple Sigmoid</a>
18
+ <a href="confusion-matrix.html">Confusion Matrix (Page 3)</a>
19
+ <a class="active" href="sigmoid.html">Sigmoid Function</a>
20
+ <a href="cost-visualization.html">Cost Function</a>
21
+ <a href="about.html">Notes</a>
22
+ </nav>
23
+ </header>
24
+
25
+ <main class="layout">
26
+ <section class="panel controls">
27
+ <h2>Model Controls</h2>
28
+
29
+ <label for="b0">b0 (intercept): <span id="b0Value">0.00</span></label>
30
+ <input id="b0" type="range" min="-10" max="10" step="0.1" value="0" />
31
+
32
+ <label for="b1">b1 (slope): <span id="b1Value">1.00</span></label>
33
+ <input id="b1" type="range" min="-5" max="5" step="0.1" value="1" />
34
+
35
+ <label for="t">threshold t: <span id="tValue">0.50</span></label>
36
+ <input id="t" type="range" min="0.05" max="0.95" step="0.01" value="0.5" />
37
+
38
+ <label for="probeX">probe x: <span id="probeValue">0.00</span></label>
39
+ <input id="probeX" type="range" min="-10" max="10" step="0.1" value="0" />
40
+
41
+ <div class="button-row">
42
+ <button id="resetBtn" type="button">Reset</button>
43
+ <button id="animateBtn" type="button">Animate Probe</button>
44
+ </div>
45
+
46
+ <label for="preset">Preset examples</label>
47
+ <select id="preset">
48
+ <option value="default">Default (b0=0, b1=1, t=0.5)</option>
49
+ <option value="steep">Steep classifier</option>
50
+ <option value="reversed">Reversed slope</option>
51
+ <option value="high-threshold">High threshold</option>
52
+ </select>
53
+
54
+ <div class="checkboxes">
55
+ <label><input id="showGrid" type="checkbox" checked /> Show grid</label>
56
+ <label><input id="showShade" type="checkbox" checked /> Shade positive region</label>
57
+ <label><input id="showDerivative" type="checkbox" /> Show derivative curve</label>
58
+ </div>
59
+ </section>
60
+
61
+ <section class="panel chart-panel">
62
+ <h2>p(x) = 1 / (1 + exp(-(b0 + b1x)))</h2>
63
+ <canvas id="plot" width="900" height="520" aria-label="Logistic curve chart"></canvas>
64
+
65
+ <div class="stats" id="stats"></div>
66
+ <p class="hint">Interactive option: click anywhere on the chart to add sample x points and see class labels using threshold t.</p>
67
+ </section>
68
+ </main>
69
+
70
+ <script src="app.js"></script>
71
+ </body>
72
+ </html>
logistic-regression/simple-sigmoid.html ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>DDW Interface - Simple Sigmoid</title>
7
+ <link rel="stylesheet" href="styles.css" />
8
+ </head>
9
+ <body>
10
+ <header class="site-header">
11
+ <div>
12
+ <p class="kicker">Data Driven World</p>
13
+ <h1>Simple Sigmoid (Page 1)</h1>
14
+ </div>
15
+ <nav>
16
+ <a href="index.html">Home</a>
17
+ <a class="active" href="simple-sigmoid.html">Simple Sigmoid</a>
18
+ <a href="confusion-matrix.html">Confusion Matrix (Page 3)</a>
19
+ <a href="sigmoid.html">Sigmoid Function</a>
20
+ <a href="cost-visualization.html">Cost Function</a>
21
+ <a href="about.html">Notes</a>
22
+ </nav>
23
+ </header>
24
+
25
+ <main class="layout">
26
+ <section class="panel controls">
27
+ <h2>Single-Variable Function</h2>
28
+
29
+ <p class="hint">
30
+ Illustrating: <code>1 / (1 + np.exp(-z))</code>
31
+ </p>
32
+
33
+ <label for="z">z value: <span id="zValue">0.00</span></label>
34
+ <input id="z" type="range" min="-10" max="10" step="0.1" value="0" />
35
+
36
+ <div class="button-row">
37
+ <button id="resetSimple" type="button">Reset</button>
38
+ <button id="animateSimple" type="button">Animate z</button>
39
+ </div>
40
+
41
+ <div class="checkboxes">
42
+ <label><input id="showHalf" type="checkbox" checked /> Show y = 0.5 line</label>
43
+ <label><input id="showFormula" type="checkbox" checked /> Show formula annotation</label>
44
+ </div>
45
+ </section>
46
+
47
+ <section class="panel chart-panel">
48
+ <h2>sigmoid(z) = 1 / (1 + np.exp(-z))</h2>
49
+ <canvas id="simplePlot" width="900" height="520" aria-label="Simple sigmoid chart"></canvas>
50
+ <div class="stats" id="simpleStats"></div>
51
+ </section>
52
+ </main>
53
+
54
+ <script src="simple.js"></script>
55
+ </body>
56
+ </html>
logistic-regression/simple.js ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const canvas = document.getElementById("simplePlot");
2
+ const ctx = canvas.getContext("2d");
3
+
4
+ const zSlider = document.getElementById("z");
5
+ const zValue = document.getElementById("zValue");
6
+ const showHalf = document.getElementById("showHalf");
7
+ const showFormula = document.getElementById("showFormula");
8
+ const resetBtn = document.getElementById("resetSimple");
9
+ const animateBtn = document.getElementById("animateSimple");
10
+ const stats = document.getElementById("simpleStats");
11
+
12
+ const MARGIN = { left: 72, right: 24, top: 24, bottom: 58 };
13
+ const Z_MIN = -10;
14
+ const Z_MAX = 10;
15
+
16
+ let timer = null;
17
+ let direction = 1;
18
+
19
+ function sigmoid(z) {
20
+ return 1 / (1 + Math.exp(-z));
21
+ }
22
+
23
+ function zToX(z) {
24
+ const width = canvas.width - MARGIN.left - MARGIN.right;
25
+ return MARGIN.left + ((z - Z_MIN) / (Z_MAX - Z_MIN)) * width;
26
+ }
27
+
28
+ function yToCanvas(y) {
29
+ const height = canvas.height - MARGIN.top - MARGIN.bottom;
30
+ return canvas.height - MARGIN.bottom - y * height;
31
+ }
32
+
33
+ function drawAxes() {
34
+ const left = MARGIN.left;
35
+ const right = canvas.width - MARGIN.right;
36
+ const top = MARGIN.top;
37
+ const bottom = canvas.height - MARGIN.bottom;
38
+
39
+ ctx.strokeStyle = "#c9d9df";
40
+ ctx.lineWidth = 1;
41
+
42
+ for (let z = -10; z <= 10; z += 2) {
43
+ const x = zToX(z);
44
+ ctx.beginPath();
45
+ ctx.moveTo(x, top);
46
+ ctx.lineTo(x, bottom);
47
+ ctx.stroke();
48
+ }
49
+
50
+ for (let y = 0; y <= 1.001; y += 0.1) {
51
+ const py = yToCanvas(y);
52
+ ctx.beginPath();
53
+ ctx.moveTo(left, py);
54
+ ctx.lineTo(right, py);
55
+ ctx.stroke();
56
+ }
57
+
58
+ ctx.strokeStyle = "#14343f";
59
+ ctx.lineWidth = 1.4;
60
+
61
+ ctx.beginPath();
62
+ ctx.moveTo(left, yToCanvas(0));
63
+ ctx.lineTo(right, yToCanvas(0));
64
+ ctx.stroke();
65
+
66
+ ctx.beginPath();
67
+ ctx.moveTo(zToX(0), top);
68
+ ctx.lineTo(zToX(0), bottom);
69
+ ctx.stroke();
70
+
71
+ ctx.fillStyle = "#14343f";
72
+ ctx.font = "13px 'Avenir Next', sans-serif";
73
+
74
+ for (let z = -10; z <= 10; z += 2) {
75
+ ctx.fillText(String(z), zToX(z) - 8, yToCanvas(0) + 19);
76
+ }
77
+ for (let y = 0; y <= 1.001; y += 0.2) {
78
+ ctx.fillText(y.toFixed(1), left - 40, yToCanvas(y) + 4);
79
+ }
80
+
81
+ ctx.fillText("z", right - 12, yToCanvas(0) + 38);
82
+ ctx.fillText("y", zToX(0) + 12, top + 12);
83
+ }
84
+
85
+ function drawSigmoid() {
86
+ ctx.strokeStyle = "#0a8f7b";
87
+ ctx.lineWidth = 3;
88
+ ctx.beginPath();
89
+
90
+ const steps = 600;
91
+ for (let i = 0; i <= steps; i += 1) {
92
+ const z = Z_MIN + (i / steps) * (Z_MAX - Z_MIN);
93
+ const y = sigmoid(z);
94
+ const x = zToX(z);
95
+ const py = yToCanvas(y);
96
+ if (i === 0) ctx.moveTo(x, py);
97
+ else ctx.lineTo(x, py);
98
+ }
99
+
100
+ ctx.stroke();
101
+ }
102
+
103
+ function drawHalfLine() {
104
+ ctx.strokeStyle = "#d8534f";
105
+ ctx.lineWidth = 1.3;
106
+ ctx.setLineDash([7, 5]);
107
+ ctx.beginPath();
108
+ ctx.moveTo(MARGIN.left, yToCanvas(0.5));
109
+ ctx.lineTo(canvas.width - MARGIN.right, yToCanvas(0.5));
110
+ ctx.stroke();
111
+ ctx.setLineDash([]);
112
+ }
113
+
114
+ function drawProbe(z) {
115
+ const y = sigmoid(z);
116
+ const px = zToX(z);
117
+ const py = yToCanvas(y);
118
+
119
+ ctx.strokeStyle = "#14343f";
120
+ ctx.setLineDash([4, 4]);
121
+ ctx.beginPath();
122
+ ctx.moveTo(px, yToCanvas(0));
123
+ ctx.lineTo(px, py);
124
+ ctx.stroke();
125
+ ctx.setLineDash([]);
126
+
127
+ ctx.fillStyle = "#14343f";
128
+ ctx.beginPath();
129
+ ctx.arc(px, py, 6, 0, Math.PI * 2);
130
+ ctx.fill();
131
+
132
+ if (showFormula.checked) {
133
+ ctx.font = "13px 'Avenir Next', sans-serif";
134
+ ctx.fillText(`z=${z.toFixed(2)}, y=${y.toFixed(4)}`, px + 8, py - 8);
135
+ }
136
+ }
137
+
138
+ function renderStats(z) {
139
+ const y = sigmoid(z);
140
+ const inverse = Math.log(y / (1 - y));
141
+
142
+ stats.innerHTML = `
143
+ <span>Function: y = 1 / (1 + np.exp(-z))</span>
144
+ <span>Current z: ${z.toFixed(2)}</span>
145
+ <span>Output y: ${y.toFixed(4)}</span>
146
+ <span>Cross-check logit(y): ${inverse.toFixed(4)}</span>
147
+ `;
148
+ }
149
+
150
+ function render() {
151
+ const z = Number(zSlider.value);
152
+ zValue.textContent = z.toFixed(2);
153
+
154
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
155
+ drawAxes();
156
+ drawSigmoid();
157
+ if (showHalf.checked) drawHalfLine();
158
+ drawProbe(z);
159
+ renderStats(z);
160
+ }
161
+
162
+ function stopAnimation() {
163
+ if (timer) {
164
+ clearInterval(timer);
165
+ timer = null;
166
+ }
167
+ animateBtn.textContent = "Animate z";
168
+ }
169
+
170
+ function startAnimation() {
171
+ if (timer) return;
172
+ animateBtn.textContent = "Stop Animation";
173
+
174
+ timer = setInterval(() => {
175
+ let next = Number(zSlider.value) + direction * 0.18;
176
+ if (next >= Z_MAX) {
177
+ next = Z_MAX;
178
+ direction = -1;
179
+ } else if (next <= Z_MIN) {
180
+ next = Z_MIN;
181
+ direction = 1;
182
+ }
183
+ zSlider.value = next;
184
+ render();
185
+ }, 30);
186
+ }
187
+
188
+ function reset() {
189
+ zSlider.value = 0;
190
+ showHalf.checked = true;
191
+ showFormula.checked = true;
192
+ stopAnimation();
193
+ render();
194
+ }
195
+
196
+ zSlider.addEventListener("input", render);
197
+ showHalf.addEventListener("input", render);
198
+ showFormula.addEventListener("input", render);
199
+ resetBtn.addEventListener("click", reset);
200
+ animateBtn.addEventListener("click", () => {
201
+ if (timer) stopAnimation();
202
+ else startAnimation();
203
+ });
204
+
205
+ reset();
logistic-regression/styles.css ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #f4f7f8;
3
+ --panel: #ffffff;
4
+ --ink: #13272f;
5
+ --muted: #597179;
6
+ --accent: #0a8f7b;
7
+ --accent-2: #f2b84b;
8
+ --danger: #d8534f;
9
+ --line: #e1ebee;
10
+ }
11
+
12
+ * {
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ body {
17
+ margin: 0;
18
+ font-family: "Avenir Next", "Segoe UI", sans-serif;
19
+ color: var(--ink);
20
+ background:
21
+ radial-gradient(circle at 20% 10%, #d8f4ef 0%, transparent 35%),
22
+ radial-gradient(circle at 90% 90%, #fff4da 0%, transparent 30%),
23
+ var(--bg);
24
+ }
25
+
26
+ .site-header {
27
+ padding: 1.2rem 1.6rem;
28
+ border-bottom: 1px solid var(--line);
29
+ background: rgba(255, 255, 255, 0.9);
30
+ backdrop-filter: blur(4px);
31
+ display: flex;
32
+ gap: 1rem;
33
+ justify-content: space-between;
34
+ align-items: end;
35
+ flex-wrap: wrap;
36
+ }
37
+
38
+ .kicker {
39
+ margin: 0;
40
+ text-transform: uppercase;
41
+ letter-spacing: 0.06em;
42
+ font-size: 0.72rem;
43
+ color: var(--muted);
44
+ }
45
+
46
+ h1 {
47
+ margin: 0.1rem 0;
48
+ font-size: 1.4rem;
49
+ }
50
+
51
+ h2 {
52
+ margin-top: 0;
53
+ font-size: 1.1rem;
54
+ }
55
+
56
+ nav {
57
+ display: flex;
58
+ gap: 0.5rem;
59
+ flex-wrap: wrap;
60
+ }
61
+
62
+ nav a {
63
+ text-decoration: none;
64
+ color: var(--ink);
65
+ border: 1px solid var(--line);
66
+ padding: 0.45rem 0.7rem;
67
+ border-radius: 999px;
68
+ font-size: 0.9rem;
69
+ background: #fff;
70
+ }
71
+
72
+ nav a.active {
73
+ border-color: var(--accent);
74
+ color: var(--accent);
75
+ font-weight: 600;
76
+ }
77
+
78
+ .layout {
79
+ display: grid;
80
+ grid-template-columns: minmax(260px, 340px) 1fr;
81
+ gap: 1rem;
82
+ padding: 1rem;
83
+ }
84
+
85
+ .panel {
86
+ background: var(--panel);
87
+ border: 1px solid var(--line);
88
+ border-radius: 14px;
89
+ padding: 1rem;
90
+ box-shadow: 0 12px 20px rgba(14, 33, 41, 0.06);
91
+ }
92
+
93
+ .controls label,
94
+ .controls select,
95
+ .controls input,
96
+ .controls button {
97
+ display: block;
98
+ width: 100%;
99
+ }
100
+
101
+ .controls label {
102
+ margin-top: 0.7rem;
103
+ margin-bottom: 0.25rem;
104
+ font-size: 0.9rem;
105
+ }
106
+
107
+ .controls input[type="range"] {
108
+ accent-color: var(--accent);
109
+ }
110
+
111
+ .button-row {
112
+ margin-top: 0.8rem;
113
+ display: flex;
114
+ gap: 0.6rem;
115
+ }
116
+
117
+ button {
118
+ border: 1px solid var(--line);
119
+ background: #fff;
120
+ color: var(--ink);
121
+ border-radius: 9px;
122
+ padding: 0.5rem;
123
+ font-weight: 600;
124
+ cursor: pointer;
125
+ }
126
+
127
+ button:hover {
128
+ border-color: var(--accent);
129
+ }
130
+
131
+ select {
132
+ padding: 0.45rem;
133
+ border-radius: 8px;
134
+ border: 1px solid var(--line);
135
+ }
136
+
137
+ .checkboxes {
138
+ margin-top: 0.8rem;
139
+ display: grid;
140
+ gap: 0.3rem;
141
+ }
142
+
143
+ .checkboxes label {
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 0.5rem;
147
+ margin: 0;
148
+ }
149
+
150
+ .chart-panel canvas {
151
+ width: 100%;
152
+ height: auto;
153
+ border: 1px solid var(--line);
154
+ border-radius: 10px;
155
+ background: #fff;
156
+ }
157
+
158
+ .stats {
159
+ margin-top: 0.8rem;
160
+ display: grid;
161
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
162
+ gap: 0.55rem;
163
+ font-size: 0.9rem;
164
+ }
165
+
166
+ .stats span {
167
+ display: block;
168
+ border: 1px solid var(--line);
169
+ border-radius: 8px;
170
+ padding: 0.45rem 0.6rem;
171
+ background: #fcfeff;
172
+ }
173
+
174
+ .hint {
175
+ margin: 0.8rem 0 0;
176
+ color: var(--muted);
177
+ font-size: 0.9rem;
178
+ }
179
+
180
+ .page-copy {
181
+ max-width: 760px;
182
+ margin: 1.2rem auto;
183
+ line-height: 1.6;
184
+ }
185
+
186
+ @media (max-width: 980px) {
187
+ .layout {
188
+ grid-template-columns: 1fr;
189
+ }
190
+ }
191
+
192
+ .home-page {
193
+ margin-top: 1rem;
194
+ }
195
+
196
+ .home-grid {
197
+ margin-top: 1rem;
198
+ display: grid;
199
+ grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
200
+ gap: 0.9rem;
201
+ }
202
+
203
+ .home-card {
204
+ border: 1px solid var(--line);
205
+ border-radius: 12px;
206
+ padding: 0.9rem;
207
+ background: #fcfeff;
208
+ }
209
+
210
+ .home-card h3 {
211
+ margin: 0;
212
+ font-size: 1.05rem;
213
+ }
214
+
215
+ .home-card p {
216
+ margin: 0.45rem 0 0.9rem;
217
+ color: var(--muted);
218
+ }
219
+
220
+ .cta-button {
221
+ display: inline-block;
222
+ text-decoration: none;
223
+ border: 1px solid var(--accent);
224
+ color: #fff;
225
+ background: var(--accent);
226
+ border-radius: 9px;
227
+ padding: 0.5rem 0.7rem;
228
+ font-weight: 600;
229
+ }
230
+
231
+ .cta-button.secondary {
232
+ background: #fff;
233
+ color: var(--ink);
234
+ border-color: var(--line);
235
+ }
236
+
237
+ .home-card.muted {
238
+ background: #f8fafb;
239
+ }
240
+
241
+ .coming-soon {
242
+ color: var(--muted);
243
+ font-size: 0.9rem;
244
+ font-weight: 600;
245
+ }
246
+
247
+ .confusion-panel h3 {
248
+ margin: 1rem 0 0.45rem;
249
+ font-size: 1rem;
250
+ }
251
+
252
+ .exercise-table {
253
+ width: 100%;
254
+ border-collapse: collapse;
255
+ margin: 0.45rem 0 1rem;
256
+ font-size: 0.9rem;
257
+ }
258
+
259
+ .exercise-table th,
260
+ .exercise-table td {
261
+ border: 1px solid var(--line);
262
+ padding: 0.45rem 0.5rem;
263
+ text-align: left;
264
+ vertical-align: middle;
265
+ background: #fff;
266
+ }
267
+
268
+ .exercise-table th {
269
+ background: #f6fbfc;
270
+ }
271
+
272
+ .exercise-table input,
273
+ .exercise-table select {
274
+ width: 100%;
275
+ padding: 0.4rem;
276
+ border: 1px solid var(--line);
277
+ border-radius: 6px;
278
+ }
279
+
280
+ .worked-tag {
281
+ color: var(--muted);
282
+ font-size: 0.82rem;
283
+ }
284
+
285
+ .solution-block {
286
+ margin-top: 1rem;
287
+ border-top: 1px dashed var(--line);
288
+ padding-top: 0.8rem;
289
+ }
290
+
291
+ .solution-block h4 {
292
+ margin: 0.8rem 0 0.35rem;
293
+ }