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.
- plotlive/__init__.py +18 -0
- plotlive/_jupyter.py +127 -0
- plotlive/_parsers.py +88 -0
- plotlive/animation.py +279 -0
- plotlive/artists.py +207 -0
- plotlive/axes.py +634 -0
- plotlive/colors.py +324 -0
- plotlive/data/DejaVuSans-Bold.ttf +1 -0
- plotlive/data/DejaVuSans.ttf +1 -0
- plotlive/data/FreeSansBold.ttf +0 -0
- plotlive/drawing.py +168 -0
- plotlive/events.py +226 -0
- plotlive/figure.py +94 -0
- plotlive/fonts.py +73 -0
- plotlive/pyplot.py +333 -0
- plotlive/renderer.py +571 -0
- plotlive/ticks.py +112 -0
- plotlive/transform.py +205 -0
- plotlive-0.1.0.dist-info/METADATA +804 -0
- plotlive-0.1.0.dist-info/RECORD +21 -0
- plotlive-0.1.0.dist-info/WHEEL +4 -0
|
@@ -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+
|