plotlive 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,804 @@
1
+ Metadata-Version: 2.4
2
+ Name: plotlive
3
+ Version: 0.1.0
4
+ Summary: Interactive matplotlib-compatible graphs rendered with pygame
5
+ Project-URL: Homepage, https://github.com/assoulsidali/plotlive
6
+ Project-URL: Repository, https://github.com/assoulsidali/plotlive
7
+ Project-URL: Issues, https://github.com/assoulsidali/plotlive/issues
8
+ Author-email: Sidali Assoul <assoulsidali@gmail.com>
9
+ License: MIT
10
+ Keywords: animation,interactive,machine-learning,matplotlib,plotting,pygame,tutorial,visualization
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Education
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Education
21
+ Classifier: Topic :: Multimedia :: Graphics
22
+ Classifier: Topic :: Scientific/Engineering :: Visualization
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: numpy>=1.24.0
25
+ Requires-Dist: pygame-ce>=2.4.0
26
+ Provides-Extra: export
27
+ Requires-Dist: imageio[ffmpeg]>=2.20; extra == 'export'
28
+ Requires-Dist: pillow>=9.0; extra == 'export'
29
+ Provides-Extra: gif
30
+ Requires-Dist: pillow>=9.0; extra == 'gif'
31
+ Provides-Extra: video
32
+ Requires-Dist: imageio[ffmpeg]>=2.20; extra == 'video'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # plotlive
36
+
37
+ Interactive matplotlib-compatible graphs rendered with pygame. Write the same code you'd write for matplotlib — get a live, interactive window instead of a static plot.
38
+
39
+ Built for ML tutorial creators who want to explain concepts step by step with pan, zoom, hover tooltips, and frame-by-frame animation.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ python3 -m venv .venv
45
+ source .venv/bin/activate
46
+ pip3 install -e .
47
+ ```
48
+
49
+ ## Quick start
50
+
51
+ ```python
52
+ import plotlive.pyplot as plt
53
+ import numpy as np
54
+
55
+ x = np.arange(50)
56
+ plt.plot(x, np.exp(-x/10), label='train loss')
57
+ plt.plot(x, np.exp(-x/12) + 0.05*np.random.randn(50), label='val loss')
58
+ plt.xlabel('Epoch')
59
+ plt.ylabel('Loss')
60
+ plt.title('Training Curve')
61
+ plt.legend()
62
+ plt.grid()
63
+ plt.show()
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Controls
69
+
70
+ ### Mouse
71
+
72
+ | Action | Result |
73
+ |--------|--------|
74
+ | Scroll up | Zoom in (centered on cursor) |
75
+ | Scroll down | Zoom out (centered on cursor) |
76
+ | Click and drag | Pan the view |
77
+ | Double-click | Reset zoom and pan |
78
+
79
+ ### Keyboard
80
+
81
+ | Key | Action |
82
+ |-----|--------|
83
+ | `?` or `H` | Show / hide the help panel |
84
+ | `Space` | Play / pause animation |
85
+ | `→` Right arrow | Step forward one frame (while paused) |
86
+ | `←` Left arrow | Step back one frame (while paused) |
87
+ | `R` | Reset zoom / pan + restart animation from frame 0 (paused) |
88
+ | `S` | Save current frame as `frame_NNNN.png` |
89
+ | `Esc` | Close the help panel |
90
+
91
+ **Animations start paused.** Press `Space` to begin. Use `←` / `→` to step one frame at a time.
92
+
93
+ Each subplot is independently interactive — zoom and pan apply only to the subplot your cursor is over.
94
+
95
+ ---
96
+
97
+ ## Supported plot types
98
+
99
+ | Function | EDA use case |
100
+ |----------|-------------|
101
+ | `plt.plot(x, y)` | Line plots — training curves, time series |
102
+ | `plt.scatter(x, y, c=labels)` | Scatter — clusters, feature relationships |
103
+ | `plt.hist(data, bins=20)` | Histogram — feature distributions |
104
+ | `plt.bar(x, height)` / `plt.barh(y, width)` | Bar charts — feature importance, class counts |
105
+ | `plt.imshow(matrix, cmap='Blues')` | Heatmap — confusion matrix, correlation |
106
+ | `plt.boxplot(data)` | Box plot — distribution summary + outliers |
107
+ | `plt.violinplot(data)` | Violin plot — full distribution shape per group |
108
+ | `plt.fill_between(x, y1, y2)` | Shaded area — confidence bands, regions |
109
+ | `plt.errorbar(x, y, yerr=std)` | Error bars — mean ± std / confidence |
110
+ | `plt.stackplot(x, y1, y2, y3)` | Stacked area — cumulative contributions |
111
+ | `plt.pie(values, labels=...)` | Pie chart — class proportions |
112
+
113
+ ---
114
+
115
+ ## API reference
116
+
117
+ ```python
118
+ # ── Figure / layout ──────────────────────────────────────────────────
119
+ fig = plt.figure(figsize=(10, 6))
120
+ fig, ax = plt.subplots()
121
+ fig, axs = plt.subplots(2, 3, figsize=(14, 8)) # returns 2-D array of Axes
122
+ fig.suptitle('Overall title')
123
+ plt.tight_layout()
124
+ plt.savefig('output.png')
125
+ plt.save_animation('output.gif') # requires: pip install Pillow
126
+ plt.save_animation('output.mp4') # requires: pip install imageio[ffmpeg]
127
+ plt.show()
128
+
129
+ # ── Plots ────────────────────────────────────────────────────────────
130
+ plt.plot(x, y, 'b--', label='data', linewidth=2)
131
+ plt.scatter(x, y, c=colors, cmap='viridis', s=50, alpha=0.7)
132
+ plt.hist(data, bins=30, color='steelblue', edgecolor='white')
133
+ plt.bar(categories, values, color='steelblue')
134
+ plt.barh(categories, values)
135
+ plt.imshow(matrix, cmap='coolwarm', vmin=-1, vmax=1)
136
+ plt.colorbar()
137
+
138
+ plt.boxplot([group_a, group_b, group_c])
139
+ plt.violinplot([group_a, group_b], positions=[1, 2], widths=0.6)
140
+ plt.fill_between(x, y_low, y_high, alpha=0.3, color='steelblue')
141
+ plt.errorbar(x, y, yerr=std, fmt='o', capsize=4)
142
+ plt.stackplot(x, y1, y2, y3, labels=['A', 'B', 'C'], alpha=0.8)
143
+ plt.pie(values, labels=['Cat A', 'Cat B', 'Cat C'], startangle=90)
144
+
145
+ # ── Axes decoration ──────────────────────────────────────────────────
146
+ plt.xlabel('x label')
147
+ plt.ylabel('y label')
148
+ plt.title('Axes title')
149
+ plt.legend()
150
+ plt.grid()
151
+ plt.xlim(0, 10)
152
+ plt.ylim(-1, 1)
153
+ plt.xscale('log')
154
+ plt.yscale('log')
155
+ plt.xticks([0, 1, 2], ['zero', 'one', 'two'])
156
+ plt.yticks([0, 0.5, 1])
157
+ plt.axhline(y=0, color='k', linewidth=0.8)
158
+ plt.axvline(x=0, color='k', linewidth=0.8)
159
+
160
+ # ── OOP API ─────────────────────────────────────────────────────────
161
+ fig, ax = plt.subplots(2, 2, figsize=(12, 8))
162
+ ax[0, 0].plot(x, y)
163
+ ax[0, 1].scatter(x, y, c=labels, cmap='tab10')
164
+ ax[1, 0].boxplot([a, b, c])
165
+ ax[1, 1].violinplot(data)
166
+ ax[0, 0].set_xlabel('x'); ax[0, 0].set_ylabel('y')
167
+ ax[0, 0].set_title('subplot title')
168
+ ax[0, 0].legend(); ax[0, 0].grid()
169
+
170
+ # ── Animation ────────────────────────────────────────────────────────
171
+ def update(frame):
172
+ plt.cla()
173
+ plt.plot(x[:frame], y[:frame])
174
+ plt.title(f'Frame {frame}')
175
+
176
+ plt.animate(update, frames=100, interval=50, repeat=True)
177
+ plt.show()
178
+
179
+ # ── Animation export ─────────────────────────────────────────────────
180
+ anim = plt.animate(update, frames=100, interval=50)
181
+ plt.save_animation('output.gif') # requires: pip install Pillow
182
+ plt.save_animation('output.mp4') # requires: pip install imageio[ffmpeg]
183
+ plt.save_animation('output.gif', fps=24) # override frame rate
184
+ anim.save('output.gif') # or call directly on the object
185
+ plt.show() # interactive window opens afterwards
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Animation
191
+
192
+ ### FuncAnimation — matplotlib-compatible
193
+
194
+ The animation class matches `matplotlib.animation.FuncAnimation` exactly, so existing matplotlib animation code runs unchanged:
195
+
196
+ ```python
197
+ from plotlive.animation import FuncAnimation
198
+
199
+ fig = plt.figure()
200
+ ax = fig.add_subplot(1, 1, 1)
201
+ x = np.linspace(-3, 3, 200)
202
+
203
+ def update(frame):
204
+ ax.cla()
205
+ ax.plot(x, np.sin(x + frame * 0.1))
206
+ ax.set_title(f'Frame {frame}')
207
+
208
+ anim = FuncAnimation(fig, update, frames=60, interval=50, repeat=True)
209
+ plt.show()
210
+ ```
211
+
212
+ All constructor parameters are supported:
213
+
214
+ | Parameter | Default | Description |
215
+ |-----------|---------|-------------|
216
+ | `fig` | — | Figure to animate |
217
+ | `func` | — | Called as `func(frame, *fargs)` each step |
218
+ | `frames` | `None` | int, list, generator, or None (→ 100 frames) |
219
+ | `init_func` | `None` | Accepted, not used (no blit) |
220
+ | `fargs` | `None` | Extra positional args forwarded to `func` |
221
+ | `save_count` | `None` | Frame count when `frames` is None |
222
+ | `interval` | `200` | Milliseconds between frames |
223
+ | `repeat` | `True` | Loop when finished |
224
+ | `blit` | `False` | Accepted, not used (full redraw always) |
225
+
226
+ `frames` as a list passes the list values directly to `func` — matching matplotlib's behaviour:
227
+
228
+ ```python
229
+ # func receives 0.0, 0.5, 1.0, 1.5, … not the list index
230
+ anim = FuncAnimation(fig, update, frames=np.linspace(0, 2*np.pi, 60))
231
+ ```
232
+
233
+ ### plt.animate() — convenience shorthand
234
+
235
+ ```python
236
+ plt.animate(update, frames=60, interval=50, repeat=True)
237
+ plt.show()
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Animation export
243
+
244
+ Export any animation to a GIF or video file without opening a window.
245
+
246
+ ### Install
247
+
248
+ ```bash
249
+ pip install plotlive[gif] # GIF support (Pillow)
250
+ pip install plotlive[video] # MP4/MOV/AVI (imageio + ffmpeg)
251
+ pip install plotlive[export] # both
252
+ ```
253
+
254
+ Or install the optional dependency directly:
255
+
256
+ ```bash
257
+ pip install Pillow # for GIF
258
+ pip install imageio[ffmpeg] # for MP4 / MOV / AVI
259
+ ```
260
+
261
+ ### Usage
262
+
263
+ ```python
264
+ import plotlive.pyplot as plt
265
+ import numpy as np
266
+
267
+ x = np.linspace(-3, 3, 200)
268
+ w = [2.5]
269
+
270
+ def update(frame):
271
+ w[0] -= 0.15 * 2 * w[0]
272
+ plt.cla()
273
+ plt.plot(x, x**2, 'b-', linewidth=2, label='f(w) = w²')
274
+ plt.scatter([w[0]], [w[0]**2], c='red', s=120, label=f'w = {w[0]:.3f}')
275
+ plt.ylim(-0.2, 7)
276
+ plt.legend()
277
+ plt.title(f'Gradient Descent — step {frame + 1}')
278
+
279
+ plt.animate(update, frames=25, interval=200)
280
+ plt.save_animation('gradient_descent.gif') # export first
281
+ plt.show() # then open interactive window
282
+ ```
283
+
284
+ `save_animation` renders all frames off-screen — no window appears during export. After saving it restores the figure to frame 0 so the subsequent `show()` opens at the beginning.
285
+
286
+ Supported formats: `.gif` · `.mp4` · `.mov` · `.avi` · `.webm`
287
+
288
+ ### API
289
+
290
+ | Call | Description |
291
+ |------|-------------|
292
+ | `plt.save_animation(filename)` | Export current figure's animation |
293
+ | `plt.save_animation(filename, fps=24)` | Override frame rate |
294
+ | `anim.save(filename)` | Call on any `FuncAnimation` object |
295
+ | `anim.save(filename, writer='pillow', fps=12)` | Explicit writer (matplotlib-compatible) |
296
+ | `anim.save(filename, writer='ffmpeg', fps=30)` | ffmpeg writer |
297
+ | `anim.save(filename, progress_callback=fn)` | `fn(current, total)` called each frame |
298
+
299
+ Default `fps` is derived from `interval`: `fps = 1000 / interval`.
300
+ Accepted `writer` values: `'pillow'` (GIF), `'ffmpeg'` / `'imageio'` (video), or `None` (inferred from extension).
301
+
302
+ ### Quick test
303
+
304
+ Run this one-liner — no window opens, it just renders and saves:
305
+
306
+ ```bash
307
+ python3 -c "
308
+ import sys; sys.path.insert(0, 'src')
309
+ import plotlive.pyplot as plt, numpy as np
310
+ x = np.linspace(-3, 3, 200); w = [2.5]
311
+ def update(frame):
312
+ w[0] -= 0.15 * 2 * w[0]; plt.cla()
313
+ plt.plot(x, x**2, 'b-', linewidth=2, label='f(w)=w²')
314
+ plt.scatter([w[0]], [w[0]**2], c='red', s=120, label=f'w={w[0]:.3f}')
315
+ plt.ylim(-0.2, 7); plt.legend(); plt.title(f'Gradient Descent — step {frame+1}')
316
+ plt.animate(update, frames=20, interval=200)
317
+ plt.save_animation('gradient_descent.gif')
318
+ "
319
+ ```
320
+
321
+ You should see frame progress printed to the terminal and a `gradient_descent.gif` appear in the current directory. Open it in any browser or image viewer to confirm it animates.
322
+
323
+ ---
324
+
325
+ ## Examples
326
+
327
+ Run from the `examples/` directory after activating the venv:
328
+
329
+ ```bash
330
+ cd examples
331
+ source ../.venv/bin/activate
332
+ ```
333
+
334
+ ---
335
+
336
+ ### Static plots
337
+
338
+ #### Training curves
339
+ ```bash
340
+ python3 -c "
341
+ import plotlive.pyplot as plt, numpy as np
342
+ x = np.arange(50)
343
+ plt.plot(x, np.exp(-x/10), label='train loss')
344
+ plt.plot(x, np.exp(-x/12) + 0.05*np.random.randn(50), label='val loss')
345
+ plt.fill_between(x, np.exp(-x/10)-0.05, np.exp(-x/10)+0.05, alpha=0.2, label='± 1σ')
346
+ plt.xlabel('Epoch'); plt.ylabel('Loss'); plt.title('Training Curve')
347
+ plt.legend(); plt.grid(); plt.show()
348
+ "
349
+ ```
350
+
351
+ #### Confusion matrix
352
+ ```bash
353
+ python3 -c "
354
+ import plotlive.pyplot as plt, numpy as np
355
+ cm = np.array([[50,2,1],[3,45,5],[2,4,48]])
356
+ fig, ax = plt.subplots()
357
+ im = ax.imshow(cm, cmap='Blues')
358
+ plt.colorbar(im, ax=ax); ax.set_title('Confusion Matrix'); plt.show()
359
+ "
360
+ ```
361
+
362
+ #### Feature importance + error bars
363
+ ```bash
364
+ python3 -c "
365
+ import plotlive.pyplot as plt, numpy as np
366
+ feats = ['age','income','tenure','score','region']
367
+ vals = [0.40, 0.30, 0.18, 0.08, 0.04]
368
+ errs = [0.04, 0.03, 0.02, 0.01, 0.005]
369
+ plt.barh(feats, vals)
370
+ plt.errorbar(vals, range(len(feats)), xerr=errs, fmt='none', color='black', capsize=4)
371
+ plt.xlabel('Importance'); plt.title('Feature Importance ± std'); plt.show()
372
+ "
373
+ ```
374
+
375
+ #### Distribution comparison — box + violin
376
+ ```bash
377
+ python3 -c "
378
+ import plotlive.pyplot as plt, numpy as np
379
+ np.random.seed(0)
380
+ data = [np.random.normal(m, s, 120) for m, s in [(0,1),(1,1.5),(3,0.5),(-1,2)]]
381
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 5))
382
+ ax1.boxplot(data); ax1.set_title('Box Plot')
383
+ ax2.violinplot(data); ax2.set_title('Violin Plot')
384
+ plt.show()
385
+ "
386
+ ```
387
+
388
+ #### Correlation heatmap
389
+ ```bash
390
+ python3 -c "
391
+ import plotlive.pyplot as plt, numpy as np
392
+ np.random.seed(0)
393
+ corr = np.corrcoef(np.random.randn(5, 100))
394
+ fig, ax = plt.subplots(figsize=(6,5))
395
+ im = ax.imshow(corr, cmap='coolwarm', vmin=-1, vmax=1)
396
+ plt.colorbar(im, ax=ax); ax.set_title('Correlation Matrix'); plt.show()
397
+ "
398
+ ```
399
+
400
+ #### Stacked area — class proportions over time
401
+ ```bash
402
+ python3 -c "
403
+ import plotlive.pyplot as plt, numpy as np
404
+ x = np.arange(20)
405
+ a = np.random.dirichlet([3,2,1], 20).T
406
+ plt.stackplot(x, a[0], a[1], a[2], labels=['Class A','Class B','Class C'], alpha=0.85)
407
+ plt.xlabel('Time step'); plt.ylabel('Proportion'); plt.title('Class Distribution Over Time')
408
+ plt.legend(); plt.show()
409
+ "
410
+ ```
411
+
412
+ #### Pie chart — class balance
413
+ ```bash
414
+ python3 -c "
415
+ import plotlive.pyplot as plt
416
+ plt.pie([52, 31, 17], labels=['Negative','Neutral','Positive'], startangle=90)
417
+ plt.title('Sentiment Distribution'); plt.legend(); plt.show()
418
+ "
419
+ ```
420
+
421
+ ---
422
+
423
+ ### Animated examples
424
+
425
+ Animations **start paused**. Press `Space` to play, `←` / `→` to step frame by frame, `S` to save.
426
+
427
+ #### Gradient descent
428
+ ```bash
429
+ python3 -c "
430
+ import plotlive.pyplot as plt, numpy as np
431
+ x = np.linspace(-3, 3, 200); w = [2.5]
432
+ def update(frame):
433
+ plt.cla(); w[0] -= 0.15 * 2 * w[0]
434
+ plt.plot(x, x**2, 'b-', linewidth=2, label='f(w)=w²')
435
+ plt.scatter([w[0]], [w[0]**2], c='red', s=120, zorder=5, label=f'w={w[0]:.3f}')
436
+ plt.fill_between(x, 0, x**2, alpha=0.07)
437
+ plt.ylim(-0.2, 7); plt.legend(); plt.title(f'Gradient Descent — step {frame+1}')
438
+ plt.animate(update, frames=25, interval=200); plt.show()
439
+ "
440
+ ```
441
+
442
+ #### K-means clustering
443
+ ```bash
444
+ python3 -c "
445
+ import plotlive.pyplot as plt, numpy as np
446
+ np.random.seed(7); K=3
447
+ data = np.vstack([np.random.randn(60,2)*0.7+c for c in [(-2,-2),(2,-2),(0,2)]])
448
+ centroids = data[np.random.choice(len(data),K,replace=False)].copy()
449
+ def update(frame):
450
+ global centroids
451
+ labels = np.array([[np.linalg.norm(p-c) for c in centroids] for p in data]).argmin(1).astype(float)
452
+ centroids = np.array([data[labels==k].mean(0) if (labels==k).any() else centroids[k] for k in range(K)])
453
+ plt.cla(); plt.scatter(data[:,0],data[:,1],c=labels,cmap='viridis',s=30,alpha=0.7)
454
+ plt.scatter(centroids[:,0],centroids[:,1],c='red',s=200,marker='*',zorder=5,label='Centroids')
455
+ plt.legend(); plt.title(f'K-Means — iteration {frame+1}')
456
+ plt.animate(update, frames=12, interval=500); plt.show()
457
+ "
458
+ ```
459
+
460
+ #### Multi-class classification boundaries
461
+
462
+ Trains a softmax classifier with gradient descent and draws the three learned decision boundary lines (one per pair of classes). Each class uses a distinct marker shape. Watch the lines rotate into place as accuracy climbs.
463
+
464
+ ```bash
465
+ python3 -c "
466
+ import plotlive.pyplot as plt, numpy as np
467
+
468
+ np.random.seed(0)
469
+ K = 3
470
+ centers = [(-2, -1), (2, -1), (0, 2.5)]
471
+ X = np.vstack([np.random.randn(50, 2) * 0.8 + c for c in centers])
472
+ y = np.repeat(np.arange(K), 50)
473
+
474
+ W = np.zeros((2, K))
475
+ b = np.zeros(K)
476
+
477
+ MARKERS = ['+', 'o', '^' ]
478
+ COLORS = ['#e74c3c', '#3498db', '#2ecc71']
479
+ BD_COLORS = ['#8e44ad', '#e67e22', '#2c3e50']
480
+ x_edge = np.array([-5.5, 5.5])
481
+
482
+ def softmax(z):
483
+ e = np.exp(z - z.max(axis=1, keepdims=True))
484
+ return e / e.sum(axis=1, keepdims=True)
485
+
486
+ def plot_boundary(i, j, color):
487
+ dw = W[:, i] - W[:, j]
488
+ db = b[i] - b[j]
489
+ if abs(dw[1]) < 1e-9:
490
+ return
491
+ plt.plot(x_edge, -(dw[0] * x_edge + db) / dw[1],
492
+ color=color, linewidth=2, label=f'Boundary {i} vs {j}')
493
+
494
+ def update(frame):
495
+ global W, b
496
+ for _ in range(5):
497
+ p = softmax(X @ W + b)
498
+ oh = np.eye(K)[y]
499
+ W -= 0.1 * (X.T @ (p - oh)) / len(X)
500
+ b -= 0.1 * (p - oh).mean(axis=0)
501
+ plt.cla()
502
+ for k in range(K):
503
+ m = y == k
504
+ plt.scatter(X[m, 0], X[m, 1], c=COLORS[k], marker=MARKERS[k],
505
+ s=80, label=f'Class {k}')
506
+ for (i, j), col in zip([(0, 1), (0, 2), (1, 2)], BD_COLORS):
507
+ plot_boundary(i, j, col)
508
+ acc = (np.argmax(X @ W + b, axis=1) == y).mean()
509
+ plt.xlim(-5, 5); plt.ylim(-4, 5); plt.legend()
510
+ plt.title(f'Softmax classifier — epoch {frame * 5} | acc {acc:.0%}')
511
+
512
+ plt.animate(update, frames=80, interval=100)
513
+ plt.show()
514
+ "
515
+ ```
516
+
517
+ Export to GIF:
518
+ ```bash
519
+ python3 -c "
520
+ import plotlive.pyplot as plt, numpy as np
521
+ np.random.seed(0); K=3
522
+ X=np.vstack([np.random.randn(50,2)*0.8+c for c in [(-2,-1),(2,-1),(0,2.5)]])
523
+ y=np.repeat(np.arange(K),50); W=np.zeros((2,K)); b=np.zeros(K)
524
+ MARKERS=['+','o','^']; COLORS=['#e74c3c','#3498db','#2ecc71']
525
+ BD_COLORS=['#8e44ad','#e67e22','#2c3e50']; x_edge=np.array([-5.5,5.5])
526
+ def softmax(z):
527
+ e=np.exp(z-z.max(axis=1,keepdims=True)); return e/e.sum(axis=1,keepdims=True)
528
+ def plot_boundary(i,j,color):
529
+ dw=W[:,i]-W[:,j]; db=b[i]-b[j]
530
+ if abs(dw[1])<1e-9: return
531
+ plt.plot(x_edge,-(dw[0]*x_edge+db)/dw[1],color=color,linewidth=2,label=f'Boundary {i} vs {j}')
532
+ def update(frame):
533
+ global W,b
534
+ for _ in range(5):
535
+ p=softmax(X@W+b); oh=np.eye(K)[y]
536
+ W-=0.1*(X.T@(p-oh))/len(X); b-=0.1*(p-oh).mean(axis=0)
537
+ plt.cla()
538
+ for k in range(K):
539
+ m=y==k; plt.scatter(X[m,0],X[m,1],c=COLORS[k],marker=MARKERS[k],s=80,label=f'Class {k}')
540
+ for (i,j),col in zip([(0,1),(0,2),(1,2)],BD_COLORS): plot_boundary(i,j,col)
541
+ acc=(np.argmax(X@W+b,axis=1)==y).mean()
542
+ plt.xlim(-5,5); plt.ylim(-4,5); plt.legend()
543
+ plt.title(f'Softmax classifier — epoch {frame*5} | acc {acc:.0%}')
544
+ plt.animate(update, frames=60, interval=100)
545
+ plt.save_animation('classification.gif')
546
+ "
547
+ ```
548
+
549
+ #### Neural network — hidden unit boundaries (ReLU)
550
+
551
+ Trains a 1-hidden-layer ReLU network on the two-moon dataset. Each subplot shows one hidden unit: the **shaded region** is where that unit fires (ReLU active), and the **black line** is its learned linear boundary. The output weight `w=` in the title shows how much each unit contributes to the final prediction.
552
+
553
+ Watch how eight straight lines, each specialising on a different slice of the space, combine to form the curved decision boundary needed to separate the two moons. Double-click any subplot to expand it with **focus mode**.
554
+
555
+ ```bash
556
+ python3 -c "
557
+ import plotlive.pyplot as plt, numpy as np
558
+
559
+ np.random.seed(0)
560
+ n_h = 8
561
+
562
+ # Two-moon dataset
563
+ theta = np.linspace(0, np.pi, 60)
564
+ X0 = np.c_[np.cos(theta), np.sin(theta) ] + np.random.randn(60,2)*0.15
565
+ X1 = np.c_[1-np.cos(theta), 0.5-np.sin(theta) ] + np.random.randn(60,2)*0.15
566
+ X = np.vstack([X0, X1]); X = (X - X.mean(0)) / X.std(0)
567
+ y = np.repeat([0, 1], 60)
568
+
569
+ # Network weights
570
+ W1 = np.random.randn(2, n_h) * np.sqrt(2/2)
571
+ b1 = np.zeros(n_h)
572
+ W2 = np.random.randn(n_h, 1) * np.sqrt(2/n_h)
573
+ b2 = np.zeros(1)
574
+
575
+ # Decision-boundary mesh
576
+ g = 28
577
+ gx, gy = np.linspace(-3, 3, g), np.linspace(-3, 3, g)
578
+ xx, yy = np.meshgrid(gx, gy)
579
+ grid = np.c_[xx.ravel(), yy.ravel()]
580
+ gx_flat = xx.ravel(); gy_flat = yy.ravel()
581
+ x_edge = np.array([-3.5, 3.5])
582
+ COLORS = ['#e74c3c', '#3498db']; MARKERS = ['o', '^']
583
+
584
+ def relu(z): return np.maximum(0, z)
585
+ def sigmoid(z): return 1 / (1 + np.exp(-np.clip(z, -50, 50)))
586
+
587
+ def forward(Xb):
588
+ z1 = Xb @ W1 + b1
589
+ return sigmoid(relu(z1) @ W2 + b2).ravel(), z1
590
+
591
+ fig, axs = plt.subplots(2, 4, figsize=(14, 7))
592
+
593
+ def update(frame):
594
+ global W1, b1, W2, b2
595
+ lr = 0.05
596
+ for _ in range(10):
597
+ p, z1 = forward(X); a1 = relu(z1); N = len(X)
598
+ dz2 = (p - y).reshape(-1, 1) / N
599
+ dW2 = a1.T @ dz2; db2 = dz2.sum(0)
600
+ dz1 = (dz2 @ W2.T) * (z1 > 0)
601
+ W1 -= lr * X.T @ dz1; b1 -= lr * dz1.sum(0)
602
+ W2 -= lr * dW2; b2 -= lr * db2
603
+ p_tr, _ = forward(X)
604
+ acc = ((p_tr > 0.5).astype(int) == y).mean()
605
+ _, z1_g = forward(grid)
606
+ for i, ax in enumerate(axs.flat):
607
+ ax.cla()
608
+ active = z1_g[:, i] > 0
609
+ ax.scatter(gx_flat[~active], gy_flat[~active], c='#eeeeee', s=55)
610
+ ax.scatter(gx_flat[ active], gy_flat[ active], c='#c6e2f5', s=55)
611
+ w, b = W1[:, i], b1[i]
612
+ if abs(w[1]) > 1e-9:
613
+ ax.plot(x_edge, -(w[0]*x_edge + b)/w[1], 'k-', linewidth=1.5)
614
+ for k in range(2):
615
+ m = y == k
616
+ ax.scatter(X[m,0], X[m,1], c=COLORS[k], marker=MARKERS[k],
617
+ s=45, edgecolors='k', linewidths=0.5)
618
+ ax.set_xlim(-3, 3); ax.set_ylim(-3, 3)
619
+ ax.set_title(f'Unit {i+1} w={W2[i,0]:+.2f}')
620
+ plt.suptitle(f'1-hidden-layer ReLU — epoch {frame*10} | acc {acc:.0%}'
621
+ ' [double-click any panel to focus]')
622
+
623
+ plt.animate(update, frames=100, interval=100)
624
+ plt.show()
625
+ "
626
+ ```
627
+
628
+ #### Neural network training curves
629
+ ```bash
630
+ python3 -c "
631
+ import plotlive.pyplot as plt, numpy as np
632
+ np.random.seed(1); losses, accs = [], []
633
+ def update(frame):
634
+ t = frame/80
635
+ losses.append(2.3*np.exp(-3*t)+0.08+0.03*np.random.randn())
636
+ accs.append(min(0.99,1-np.exp(-4*t)*0.9+0.01*np.random.randn()))
637
+ axs = plt.gcf().axes
638
+ axs[0].cla(); axs[1].cla()
639
+ axs[0].plot(losses,'b-',linewidth=2); axs[0].set_title('Loss'); axs[0].grid()
640
+ axs[1].plot(accs,'g-',linewidth=2); axs[1].set_title('Accuracy'); axs[1].set_ylim(0,1); axs[1].grid()
641
+ plt.subplots(1,2,figsize=(10,4))
642
+ plt.animate(update, frames=80, interval=80); plt.show()
643
+ "
644
+ ```
645
+
646
+ #### fill_between — confidence band widening under distribution shift
647
+ ```bash
648
+ python3 -c "
649
+ import plotlive.pyplot as plt, numpy as np
650
+ np.random.seed(0)
651
+ x = np.linspace(0, 10, 80)
652
+ mean = np.sin(x) * np.exp(-x/8)
653
+ noise_levels = np.linspace(0.05, 0.6, 30)
654
+ def update(frame):
655
+ sigma = noise_levels[frame]
656
+ plt.cla()
657
+ plt.plot(x, mean, 'steelblue', linewidth=2, label='prediction')
658
+ plt.fill_between(x, mean - sigma, mean + sigma, alpha=0.35, color='steelblue', label=f'± {sigma:.2f}')
659
+ plt.fill_between(x, mean - 2*sigma, mean + 2*sigma, alpha=0.15, color='steelblue', label='± 2σ')
660
+ plt.ylim(-1.8, 1.8); plt.legend(); plt.grid()
661
+ plt.title(f'Uncertainty grows under distribution shift — σ={sigma:.2f}')
662
+ plt.animate(update, frames=30, interval=150); plt.show()
663
+ "
664
+ ```
665
+
666
+ #### errorbar — learning curve: accuracy rises, uncertainty shrinks with more data
667
+ ```bash
668
+ python3 -c "
669
+ import plotlive.pyplot as plt, numpy as np
670
+ np.random.seed(0)
671
+ sizes = np.array([10, 25, 50, 100, 200, 400, 800])
672
+ means = 1 - 0.88*np.exp(-sizes/120) + 0.015*np.random.randn(len(sizes))
673
+ stds = 0.32*np.exp(-sizes/80) + 0.01
674
+ def update(frame):
675
+ n = frame + 1
676
+ plt.cla()
677
+ plt.errorbar(sizes[:n], means[:n], yerr=stds[:n], fmt='o-', capsize=5,
678
+ color='steelblue', label='accuracy ± std')
679
+ plt.xlim(-30, 850); plt.ylim(0, 1.1)
680
+ plt.xlabel('Training set size'); plt.ylabel('Accuracy')
681
+ plt.title('Learning Curve — more data, less variance'); plt.legend(); plt.grid()
682
+ plt.animate(update, frames=len(sizes), interval=600); plt.show()
683
+ "
684
+ ```
685
+
686
+ #### boxplot — prediction distribution tightens as model trains
687
+ ```bash
688
+ python3 -c "
689
+ import plotlive.pyplot as plt, numpy as np
690
+ np.random.seed(1)
691
+ epochs = [5, 10, 20, 40, 80, 160]
692
+ data = [np.random.normal(0.35 + 0.55*(i/len(epochs)), max(0.28 - i*0.04, 0.04), 80)
693
+ for i in range(len(epochs))]
694
+ def update(frame):
695
+ n = frame + 1
696
+ plt.cla()
697
+ plt.boxplot(data[:n], labels=[str(e) for e in epochs[:n]])
698
+ plt.ylim(-0.1, 1.1)
699
+ plt.xlabel('Epoch'); plt.ylabel('Predicted probability')
700
+ plt.title(f'Prediction Distribution — epoch {epochs[frame]}')
701
+ plt.animate(update, frames=len(epochs), interval=700); plt.show()
702
+ "
703
+ ```
704
+
705
+ #### violinplot — activation distribution shifts as layers train
706
+ ```bash
707
+ python3 -c "
708
+ import plotlive.pyplot as plt, numpy as np
709
+ np.random.seed(2)
710
+ steps = 6
711
+ data = [np.random.normal(i*0.5, max(1.1 - i*0.16, 0.15), 120) for i in range(steps)]
712
+ def update(frame):
713
+ n = frame + 1
714
+ plt.cla()
715
+ plt.violinplot(data[:n], positions=list(range(1, n+1)), widths=0.7)
716
+ plt.xlim(0, steps+1); plt.ylim(-3.5, 5.5)
717
+ plt.xlabel('Training step'); plt.ylabel('Activation value')
718
+ plt.title(f'Activation Distribution — step {frame+1} of {steps}')
719
+ plt.animate(update, frames=steps, interval=700); plt.show()
720
+ "
721
+ ```
722
+
723
+ #### pie — class proportions shift as dataset is rebalanced
724
+ ```bash
725
+ python3 -c "
726
+ import plotlive.pyplot as plt
727
+ labels = ['Negative', 'Neutral', 'Positive']
728
+ stages = [[70,20,10],[60,25,15],[50,30,20],[45,32,23],[40,35,25],[33,34,33]]
729
+ captions = ['raw','oversample pos','oversample more','near balance','balanced','uniform']
730
+ def update(frame):
731
+ plt.cla()
732
+ vals = stages[frame]
733
+ plt.pie(vals, labels=labels, startangle=90)
734
+ pcts = ' | '.join(f'{l}: {v}%' for l,v in zip(labels,vals))
735
+ plt.title(f'Class Balance — {captions[frame]}\n{pcts}')
736
+ plt.animate(update, frames=len(stages), interval=900); plt.show()
737
+ "
738
+ ```
739
+
740
+ #### stackplot — feature contributions accumulate as model complexity grows
741
+ ```bash
742
+ python3 -c "
743
+ import plotlive.pyplot as plt, numpy as np
744
+ np.random.seed(3)
745
+ x = np.arange(20)
746
+ feats = ['linear','interactions','polynomials','residuals']
747
+ components = [np.abs(np.random.randn(20))*(i+1)*0.4 for i in range(len(feats))]
748
+ def update(frame):
749
+ n = frame + 1
750
+ plt.cla()
751
+ plt.stackplot(x, *components[:n], labels=feats[:n], alpha=0.85)
752
+ plt.xlim(0, 19); plt.ylim(0, sum(c.max() for c in components)*1.05)
753
+ plt.xlabel('Sample'); plt.ylabel('Explained variance')
754
+ plt.title(f'Model Complexity — adding {feats[frame]}')
755
+ plt.legend()
756
+ plt.animate(update, frames=len(feats), interval=900); plt.show()
757
+ "
758
+ ```
759
+
760
+ ---
761
+
762
+ ### Sorting algorithm visualizations
763
+
764
+ Each opens its own window with 7 elements, a color legend, and step descriptions.
765
+
766
+ ```bash
767
+ cd examples
768
+ python3 bubble_sort.py
769
+ python3 insertion_sort.py
770
+ python3 selection_sort.py
771
+ python3 heap_sort.py
772
+ python3 merge_sort.py
773
+ python3 quick_sort.py
774
+ ```
775
+
776
+ | Color | Meaning |
777
+ |-------|---------|
778
+ | Blue | Unsorted |
779
+ | Orange | Being compared |
780
+ | Red | Being swapped |
781
+ | Green | Confirmed sorted |
782
+
783
+ Use `←` / `→` to step frame by frame. The value of each element is shown below its bar.
784
+
785
+ #### Runtime benchmark — all 6 algorithms
786
+ ```bash
787
+ python3 sort_benchmark.py
788
+ ```
789
+
790
+ Benchmarks all 6 in the terminal first, then opens an animated log-scale line chart that reveals one input size at a time. Watch O(n²) and O(n log n) algorithms diverge as N grows.
791
+
792
+ ---
793
+
794
+ ## Run tests
795
+
796
+ ```bash
797
+ pytest tests/
798
+ ```
799
+
800
+ ## Dependencies
801
+
802
+ - `pygame-ce >= 2.4.0`
803
+ - `numpy >= 1.24.0`
804
+ - Python 3.10+