parallel-matplotlib-animation 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,634 @@
1
+ from pathlib import Path
2
+
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import matplotlib.ticker as ticker
6
+ from matplotlib.gridspec import GridSpec
7
+
8
+ from parallel_animate import Animator
9
+
10
+
11
+ class VeryComplexAnimation(Animator):
12
+ """
13
+ A showcase animation with extremely complex layout to demonstrate the efficiency
14
+ of setup-once, update-many approach. This creates 12+ diverse plots with:
15
+ - GridSpec with panels of varying sizes
16
+ - Custom formatters, locators, and tick labels
17
+ - Multiple colorbars and legends
18
+ - 3D projections
19
+ - Polar plots
20
+ - Complex annotations and text
21
+ """
22
+
23
+ def setup(self):
24
+ # Create figure with complex GridSpec layout
25
+ self.fig = plt.figure(figsize=(24, 18))
26
+ gs = GridSpec(
27
+ 6,
28
+ 6,
29
+ figure=self.fig,
30
+ hspace=0.4,
31
+ wspace=0.4,
32
+ left=0.05,
33
+ right=0.95,
34
+ top=0.95,
35
+ bottom=0.05,
36
+ )
37
+
38
+ # Dictionary to store all artists and data arrays
39
+ self.artists = {}
40
+ self.data_arrays = {}
41
+
42
+ # ===== PLOT 1: Large 3D Surface (spans 2x2) =====
43
+ self.ax1 = self.fig.add_subplot(gs[0:2, 0:2], projection="3d")
44
+ x = np.linspace(-3, 3, 50)
45
+ y = np.linspace(-3, 3, 50)
46
+ X, Y = np.meshgrid(x, y)
47
+ Z = np.sin(np.sqrt(X**2 + Y**2))
48
+ self.artists["surf"] = self.ax1.plot_surface(
49
+ X,
50
+ Y,
51
+ Z,
52
+ cmap="viridis",
53
+ alpha=0.9,
54
+ linewidth=0,
55
+ antialiased=True,
56
+ vmin=-1,
57
+ vmax=1,
58
+ )
59
+ self.data_arrays["surf_X"] = X
60
+ self.data_arrays["surf_Y"] = Y
61
+ self.ax1.set_xlabel("X Axis", fontsize=10, labelpad=10)
62
+ self.ax1.set_ylabel("Y Axis", fontsize=10, labelpad=10)
63
+ self.ax1.set_zlabel("Z = f(X,Y,t)", fontsize=10, labelpad=10)
64
+ self.ax1.set_title(
65
+ "3D Surface Evolution", fontsize=12, fontweight="bold", pad=20
66
+ )
67
+ self.ax1.set_xlim(-3, 3)
68
+ self.ax1.set_ylim(-3, 3)
69
+ self.ax1.set_zlim(-1.5, 1.5)
70
+ # Custom tick labels
71
+ self.ax1.set_xticks([-3, -1.5, 0, 1.5, 3])
72
+ self.ax1.set_yticks([-3, -1.5, 0, 1.5, 3])
73
+ self.ax1.set_zticks([-1, 0, 1])
74
+ self.ax1.view_init(elev=25, azim=45)
75
+
76
+ # ===== PLOT 2: Contour plot with colorbar (spans 2x2) =====
77
+ self.ax2 = self.fig.add_subplot(gs[0:2, 2:4])
78
+ x = np.linspace(-4, 4, 100)
79
+ y = np.linspace(-4, 4, 100)
80
+ X, Y = np.meshgrid(x, y)
81
+ Z = np.sin(X) * np.cos(Y)
82
+ levels = np.linspace(-1, 1, 21)
83
+ self.artists["contourf"] = self.ax2.contourf(
84
+ X, Y, Z, levels=levels, cmap="RdBu_r"
85
+ )
86
+ self.artists["contour"] = self.ax2.contour(
87
+ X, Y, Z, levels=10, colors="black", linewidths=0.5, alpha=0.4
88
+ )
89
+ self.data_arrays["contour_X"] = X
90
+ self.data_arrays["contour_Y"] = Y
91
+ cbar = self.fig.colorbar(
92
+ self.artists["contourf"], ax=self.ax2, orientation="vertical"
93
+ )
94
+ cbar.set_label("Field Strength", rotation=270, labelpad=20, fontsize=10)
95
+ self.ax2.set_xlabel("X Position (m)", fontsize=10)
96
+ self.ax2.set_ylabel("Y Position (m)", fontsize=10)
97
+ self.ax2.set_title("2D Scalar Field Contours", fontsize=12, fontweight="bold")
98
+ self.ax2.grid(True, alpha=0.2, linestyle="--")
99
+ self.ax2.set_aspect("equal")
100
+
101
+ # ===== PLOT 3: Polar plot with custom angles =====
102
+ self.ax3 = self.fig.add_subplot(gs[0:2, 4:6], projection="polar")
103
+ theta = np.linspace(0, 2 * np.pi, 100)
104
+ r = 1 + 0.5 * np.sin(5 * theta)
105
+ (self.artists["polar_line"],) = self.ax3.plot(theta, r, "b-", linewidth=2)
106
+ self.artists["polar_fill"] = self.ax3.fill(theta, r, alpha=0.3, color="blue")
107
+ self.data_arrays["polar_theta"] = theta
108
+ self.ax3.set_ylim(0, 2)
109
+ self.ax3.set_title(
110
+ "Polar Pattern Evolution", fontsize=12, fontweight="bold", pad=20
111
+ )
112
+ # Custom angle labels
113
+ self.ax3.set_xticks(np.linspace(0, 2 * np.pi, 8, endpoint=False))
114
+ self.ax3.set_xticklabels(
115
+ ["0°", "45°", "90°", "135°", "180°", "225°", "270°", "315°"]
116
+ )
117
+ self.ax3.set_yticks([0.5, 1.0, 1.5, 2.0])
118
+ self.ax3.grid(True, alpha=0.3)
119
+
120
+ # ===== PLOT 4: Heatmap with custom colormap (spans 1x2) =====
121
+ self.ax4 = self.fig.add_subplot(gs[2, 0:2])
122
+ data = np.random.randn(20, 40)
123
+ self.artists["heatmap"] = self.ax4.imshow(
124
+ data,
125
+ aspect="auto",
126
+ cmap="plasma",
127
+ interpolation="bilinear",
128
+ vmin=-3,
129
+ vmax=3,
130
+ )
131
+ cbar = self.fig.colorbar(
132
+ self.artists["heatmap"], ax=self.ax4, orientation="horizontal", pad=0.1
133
+ )
134
+ cbar.set_label("Intensity (a.u.)", fontsize=9)
135
+ self.ax4.set_title("Temporal Heatmap", fontsize=12, fontweight="bold")
136
+ self.ax4.set_xlabel("Time Index", fontsize=10)
137
+ self.ax4.set_ylabel("Channel", fontsize=10)
138
+ # Custom tick spacing
139
+ self.ax4.set_xticks(np.linspace(0, 39, 5))
140
+ self.ax4.set_xticklabels(["0", "10", "20", "30", "40"])
141
+ self.ax4.set_yticks([0, 5, 10, 15, 19])
142
+
143
+ # ===== PLOT 5: Multi-line plot with legend =====
144
+ self.ax5 = self.fig.add_subplot(gs[2, 2:4])
145
+ x = np.linspace(0, 10, 200)
146
+ self.data_arrays["multiline_x"] = x
147
+ colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"]
148
+ labels = ["Signal α", "Signal β", "Signal γ", "Signal δ"]
149
+ self.artists["multilines"] = []
150
+ for i, (color, label) in enumerate(zip(colors, labels)):
151
+ (line,) = self.ax5.plot(
152
+ x, np.sin(x + i * np.pi / 4), color=color, linewidth=2, label=label
153
+ )
154
+ self.artists["multilines"].append(line)
155
+ self.ax5.set_xlim(0, 10)
156
+ self.ax5.set_ylim(-2, 2)
157
+ self.ax5.set_xlabel("Time (s)", fontsize=10)
158
+ self.ax5.set_ylabel("Amplitude", fontsize=10)
159
+ self.ax5.set_title("Multi-Channel Signals", fontsize=12, fontweight="bold")
160
+ self.ax5.legend(loc="upper right", fontsize=8, framealpha=0.9)
161
+ self.ax5.grid(True, alpha=0.3, linestyle=":")
162
+ # Custom x-axis formatter
163
+ self.ax5.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f s"))
164
+
165
+ # ===== PLOT 6: Quiver (vector field) plot =====
166
+ self.ax6 = self.fig.add_subplot(gs[2, 4:6])
167
+ x = np.linspace(-2, 2, 15)
168
+ y = np.linspace(-2, 2, 15)
169
+ X, Y = np.meshgrid(x, y)
170
+ U = -Y
171
+ V = X
172
+ self.artists["quiver"] = self.ax6.quiver(
173
+ X,
174
+ Y,
175
+ U,
176
+ V,
177
+ np.sqrt(U**2 + V**2),
178
+ cmap="coolwarm",
179
+ scale=30,
180
+ width=0.004,
181
+ alpha=0.8,
182
+ )
183
+ self.data_arrays["quiver_X"] = X
184
+ self.data_arrays["quiver_Y"] = Y
185
+ self.ax6.set_xlim(-2.5, 2.5)
186
+ self.ax6.set_ylim(-2.5, 2.5)
187
+ self.ax6.set_xlabel("X", fontsize=10)
188
+ self.ax6.set_ylabel("Y", fontsize=10)
189
+ self.ax6.set_title("Vector Field Dynamics", fontsize=12, fontweight="bold")
190
+ self.ax6.set_aspect("equal")
191
+ self.ax6.grid(True, alpha=0.2)
192
+ cbar = self.fig.colorbar(
193
+ self.artists["quiver"], ax=self.ax6, orientation="vertical"
194
+ )
195
+ cbar.set_label("Magnitude", rotation=270, labelpad=15, fontsize=9)
196
+
197
+ # ===== PLOT 7: Bar chart with error bars =====
198
+ self.ax7 = self.fig.add_subplot(gs[3, 0:2])
199
+ categories = ["A", "B", "C", "D", "E", "F", "G", "H"]
200
+ x_pos = np.arange(len(categories))
201
+ values = np.random.rand(len(categories)) * 10 + 5
202
+ errors = np.random.rand(len(categories)) * 2
203
+ self.artists["bars"] = self.ax7.bar(
204
+ x_pos,
205
+ values,
206
+ yerr=errors,
207
+ color="steelblue",
208
+ alpha=0.8,
209
+ capsize=5,
210
+ edgecolor="black",
211
+ linewidth=1.2,
212
+ )
213
+ self.ax7.set_ylabel("Measurement (units)", fontsize=10)
214
+ self.ax7.set_xlabel("Category", fontsize=10)
215
+ self.ax7.set_title(
216
+ "Categorical Measurements with Uncertainty", fontsize=12, fontweight="bold"
217
+ )
218
+ self.ax7.set_xticks(x_pos)
219
+ self.ax7.set_xticklabels(categories, fontsize=9)
220
+ self.ax7.set_ylim(0, 20)
221
+ self.ax7.grid(True, axis="y", alpha=0.3, linestyle="--")
222
+ # Add value labels on bars
223
+ self.artists["bar_labels"] = []
224
+ for bar in self.artists["bars"]:
225
+ height = bar.get_height()
226
+ label = self.ax7.text(
227
+ bar.get_x() + bar.get_width() / 2.0,
228
+ height,
229
+ f"{height:.1f}",
230
+ ha="center",
231
+ va="bottom",
232
+ fontsize=8,
233
+ )
234
+ self.artists["bar_labels"].append(label)
235
+
236
+ # ===== PLOT 8: Scatter plot with varying sizes and colors =====
237
+ self.ax8 = self.fig.add_subplot(gs[3, 2:4])
238
+ n_points = 100
239
+ x = np.random.randn(n_points)
240
+ y = np.random.randn(n_points)
241
+ colors = np.random.rand(n_points)
242
+ sizes = np.random.rand(n_points) * 200 + 50
243
+ self.artists["scatter"] = self.ax8.scatter(
244
+ x,
245
+ y,
246
+ c=colors,
247
+ s=sizes,
248
+ cmap="viridis",
249
+ alpha=0.6,
250
+ edgecolors="black",
251
+ linewidth=0.5,
252
+ )
253
+ self.ax8.set_xlim(-3, 3)
254
+ self.ax8.set_ylim(-3, 3)
255
+ self.ax8.set_xlabel("Feature X", fontsize=10)
256
+ self.ax8.set_ylabel("Feature Y", fontsize=10)
257
+ self.ax8.set_title(
258
+ "2D Point Cloud Distribution", fontsize=12, fontweight="bold"
259
+ )
260
+ self.ax8.grid(True, alpha=0.3)
261
+ cbar = self.fig.colorbar(
262
+ self.artists["scatter"], ax=self.ax8, orientation="vertical"
263
+ )
264
+ cbar.set_label("Color Value", rotation=270, labelpad=15, fontsize=9)
265
+ self.ax8.set_aspect("equal")
266
+
267
+ # ===== PLOT 9: Histogram with custom bins =====
268
+ self.ax9 = self.fig.add_subplot(gs[3, 4:6])
269
+ data = np.random.normal(0, 1, 1000)
270
+ bins = np.linspace(-4, 4, 31)
271
+ n, bins_edges, patches = self.ax9.hist(
272
+ data, bins=bins, color="skyblue", edgecolor="black", linewidth=1, alpha=0.7
273
+ )
274
+ self.artists["hist_patches"] = patches
275
+ self.data_arrays["hist_bins"] = bins
276
+ self.ax9.set_xlabel("Value", fontsize=10)
277
+ self.ax9.set_ylabel("Frequency", fontsize=10)
278
+ self.ax9.set_title("Distribution Analysis", fontsize=12, fontweight="bold")
279
+ self.ax9.grid(True, axis="y", alpha=0.3)
280
+ # Add statistics text
281
+ self.artists["stats_text"] = self.ax9.text(
282
+ 0.95,
283
+ 0.95,
284
+ f"μ = 0.00\nσ = 1.00\nN = 1000",
285
+ transform=self.ax9.transAxes,
286
+ fontsize=9,
287
+ verticalalignment="top",
288
+ horizontalalignment="right",
289
+ bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5),
290
+ )
291
+
292
+ # ===== PLOT 10: Pie chart with custom formatting =====
293
+ self.ax10 = self.fig.add_subplot(gs[4, 0:2])
294
+ labels = ["Sector A", "Sector B", "Sector C", "Sector D", "Sector E"]
295
+ sizes = [30, 25, 20, 15, 10]
296
+ colors_pie = ["#ff9999", "#66b3ff", "#99ff99", "#ffcc99", "#ff99cc"]
297
+ explode = (0.05, 0, 0, 0, 0)
298
+ self.artists["pie_wedges"], texts, autotexts = self.ax10.pie(
299
+ sizes,
300
+ explode=explode,
301
+ labels=labels,
302
+ colors=colors_pie,
303
+ autopct="%1.1f%%",
304
+ startangle=90,
305
+ textprops={"fontsize": 9},
306
+ )
307
+ self.artists["pie_texts"] = texts
308
+ self.artists["pie_autotexts"] = autotexts
309
+ self.ax10.set_title("Market Share Distribution", fontsize=12, fontweight="bold")
310
+ # Make percentage text bold
311
+ for autotext in autotexts:
312
+ autotext.set_color("black")
313
+ autotext.set_fontweight("bold")
314
+
315
+ # ===== PLOT 11: Step plot with filled regions =====
316
+ self.ax11 = self.fig.add_subplot(gs[4, 2:4])
317
+ x = np.arange(0, 20, 1)
318
+ y = np.random.randint(0, 10, size=len(x))
319
+ self.data_arrays["step_x"] = x
320
+ (self.artists["step_line"],) = self.ax11.step(
321
+ x, y, where="mid", linewidth=2, color="darkgreen", label="Signal"
322
+ )
323
+ self.artists["step_fill"] = self.ax11.fill_between(
324
+ x, 0, y, step="mid", alpha=0.3, color="lightgreen"
325
+ )
326
+ self.ax11.set_xlim(-1, 20)
327
+ self.ax11.set_ylim(0, 12)
328
+ self.ax11.set_xlabel("Sample Index", fontsize=10)
329
+ self.ax11.set_ylabel("Discrete Value", fontsize=10)
330
+ self.ax11.set_title("Step Function Evolution", fontsize=12, fontweight="bold")
331
+ self.ax11.grid(True, alpha=0.3, linestyle=":")
332
+ self.ax11.legend(loc="upper right", fontsize=9)
333
+
334
+ # ===== PLOT 12: Box plot with multiple groups =====
335
+ self.ax12 = self.fig.add_subplot(gs[4, 4:6])
336
+ # Create 5 groups of data
337
+ data_groups = [np.random.normal(0, std, 100) for std in [1, 2, 1.5, 2.5, 1.8]]
338
+ bp = self.ax12.boxplot(
339
+ data_groups,
340
+ tick_labels=["G1", "G2", "G3", "G4", "G5"],
341
+ patch_artist=True,
342
+ notch=True,
343
+ showmeans=True,
344
+ )
345
+ self.artists["boxplot"] = bp
346
+ # Color the boxes
347
+ colors_box = ["lightblue", "lightgreen", "lightyellow", "lightcoral", "plum"]
348
+ for patch, color in zip(bp["boxes"], colors_box):
349
+ patch.set_facecolor(color)
350
+ patch.set_alpha(0.7)
351
+ self.ax12.set_ylabel("Value Distribution", fontsize=10)
352
+ self.ax12.set_xlabel("Group", fontsize=10)
353
+ self.ax12.set_title("Statistical Comparison", fontsize=12, fontweight="bold")
354
+ self.ax12.grid(True, axis="y", alpha=0.3)
355
+
356
+ # ===== PLOT 13: Stream plot (spans 2x3) =====
357
+ self.ax13 = self.fig.add_subplot(gs[5, 0:3])
358
+ x = np.linspace(-3, 3, 100)
359
+ y = np.linspace(-3, 3, 100)
360
+ X, Y = np.meshgrid(x, y)
361
+ U = -Y
362
+ V = X
363
+ speed = np.sqrt(U**2 + V**2)
364
+ self.artists["streamplot"] = self.ax13.streamplot(
365
+ X,
366
+ Y,
367
+ U,
368
+ V,
369
+ color=speed,
370
+ cmap="autumn",
371
+ linewidth=1.5,
372
+ density=1.5,
373
+ arrowsize=1.5,
374
+ arrowstyle="->",
375
+ )
376
+ self.data_arrays["stream_X"] = X
377
+ self.data_arrays["stream_Y"] = Y
378
+ self.ax13.set_xlim(-3, 3)
379
+ self.ax13.set_ylim(-3, 3)
380
+ self.ax13.set_xlabel("X Coordinate", fontsize=10)
381
+ self.ax13.set_ylabel("Y Coordinate", fontsize=10)
382
+ self.ax13.set_title("Fluid Flow Streamlines", fontsize=12, fontweight="bold")
383
+ self.ax13.set_aspect("equal")
384
+ cbar = self.fig.colorbar(
385
+ self.artists["streamplot"].lines,
386
+ ax=self.ax13,
387
+ orientation="horizontal",
388
+ pad=0.1,
389
+ )
390
+ cbar.set_label("Flow Speed", fontsize=9)
391
+
392
+ # ===== PLOT 14: Error band plot (spans 2x3) =====
393
+ self.ax14 = self.fig.add_subplot(gs[5, 3:6])
394
+ x = np.linspace(0, 10, 100)
395
+ y_mean = np.sin(x)
396
+ y_std = 0.2 + 0.1 * x / 10
397
+ self.data_arrays["errorband_x"] = x
398
+ (self.artists["errorband_line"],) = self.ax14.plot(
399
+ x, y_mean, "b-", linewidth=2.5, label="Mean", zorder=3
400
+ )
401
+ self.artists["errorband_fill"] = self.ax14.fill_between(
402
+ x,
403
+ y_mean - y_std,
404
+ y_mean + y_std,
405
+ alpha=0.3,
406
+ color="blue",
407
+ label="±1σ",
408
+ zorder=1,
409
+ )
410
+ self.artists["errorband_fill2"] = self.ax14.fill_between(
411
+ x,
412
+ y_mean - 2 * y_std,
413
+ y_mean + 2 * y_std,
414
+ alpha=0.15,
415
+ color="blue",
416
+ label="±2σ",
417
+ zorder=0,
418
+ )
419
+ self.ax14.set_xlim(0, 10)
420
+ self.ax14.set_ylim(-2, 2)
421
+ self.ax14.set_xlabel("Time (s)", fontsize=10)
422
+ self.ax14.set_ylabel("Response", fontsize=10)
423
+ self.ax14.set_title(
424
+ "Time Series with Confidence Bands", fontsize=12, fontweight="bold"
425
+ )
426
+ self.ax14.grid(True, alpha=0.3, linestyle="--")
427
+ self.ax14.legend(loc="upper right", fontsize=9, framealpha=0.9)
428
+ # Custom minor ticks
429
+ self.ax14.xaxis.set_minor_locator(ticker.AutoMinorLocator())
430
+ self.ax14.yaxis.set_minor_locator(ticker.AutoMinorLocator())
431
+ self.ax14.tick_params(which="minor", length=3, color="gray")
432
+
433
+ # Add overall title
434
+ self.fig.suptitle(
435
+ "Complex Multi-Panel Scientific Visualization Dashboard",
436
+ fontsize=16,
437
+ fontweight="bold",
438
+ y=0.98,
439
+ )
440
+
441
+ return self.fig
442
+
443
+ def update(self, frame_idx, params):
444
+ """
445
+ Update only the data, not the complex structure.
446
+ This is where we save massive amounts of time by not recreating everything.
447
+ """
448
+ phase = params["phase"]
449
+
450
+ # Update 3D surface
451
+ X = self.data_arrays["surf_X"]
452
+ Y = self.data_arrays["surf_Y"]
453
+ Z = np.sin(np.sqrt(X**2 + Y**2) + phase)
454
+ self.artists["surf"].remove()
455
+ self.artists["surf"] = self.ax1.plot_surface(
456
+ X,
457
+ Y,
458
+ Z,
459
+ cmap="viridis",
460
+ alpha=0.9,
461
+ linewidth=0,
462
+ antialiased=True,
463
+ vmin=-1,
464
+ vmax=1,
465
+ )
466
+
467
+ # Update contour plot
468
+ X = self.data_arrays["contour_X"]
469
+ Y = self.data_arrays["contour_Y"]
470
+ Z = np.sin(X + phase) * np.cos(Y)
471
+ # Remove old collections
472
+ while self.ax2.collections:
473
+ self.ax2.collections[0].remove()
474
+ levels = np.linspace(-1, 1, 21)
475
+ self.artists["contourf"] = self.ax2.contourf(
476
+ X, Y, Z, levels=levels, cmap="RdBu_r"
477
+ )
478
+ self.artists["contour"] = self.ax2.contour(
479
+ X, Y, Z, levels=10, colors="black", linewidths=0.5, alpha=0.4
480
+ )
481
+
482
+ # Update polar plot
483
+ theta = self.data_arrays["polar_theta"]
484
+ r = 1 + 0.5 * np.sin(5 * theta + phase)
485
+ self.artists["polar_line"].set_ydata(r)
486
+ self.artists["polar_fill"][0].remove()
487
+ self.artists["polar_fill"] = self.ax3.fill(theta, r, alpha=0.3, color="blue")
488
+
489
+ # Update heatmap
490
+ data = np.random.randn(20, 40) * (1 + 0.3 * np.sin(phase))
491
+ self.artists["heatmap"].set_data(data)
492
+
493
+ # Update multi-line plot
494
+ x = self.data_arrays["multiline_x"]
495
+ for i, line in enumerate(self.artists["multilines"]):
496
+ y = np.sin(x + i * np.pi / 4 + phase) * (1 + 0.2 * np.sin(phase * 2))
497
+ line.set_ydata(y)
498
+
499
+ # Update quiver plot
500
+ X = self.data_arrays["quiver_X"]
501
+ Y = self.data_arrays["quiver_Y"]
502
+ rotation = phase
503
+ U = -Y * np.cos(rotation) + X * np.sin(rotation)
504
+ V = X * np.cos(rotation) + Y * np.sin(rotation)
505
+ self.artists["quiver"].set_UVC(U, V, np.sqrt(U**2 + V**2))
506
+
507
+ # Update bar chart
508
+ new_values = np.abs(np.sin(np.arange(8) * 0.5 + phase)) * 10 + 5
509
+ for bar, val in zip(self.artists["bars"], new_values):
510
+ bar.set_height(val)
511
+ # Update bar labels
512
+ for bar, label in zip(self.artists["bars"], self.artists["bar_labels"]):
513
+ height = bar.get_height()
514
+ label.set_text(f"{height:.1f}")
515
+ label.set_position((bar.get_x() + bar.get_width() / 2.0, height))
516
+
517
+ # Update scatter plot
518
+ n_points = 100
519
+ angle = phase
520
+ x = np.cos(angle) * np.random.randn(n_points) - np.sin(angle) * np.random.randn(
521
+ n_points
522
+ )
523
+ y = np.sin(angle) * np.random.randn(n_points) + np.cos(angle) * np.random.randn(
524
+ n_points
525
+ )
526
+ self.artists["scatter"].set_offsets(np.c_[x, y])
527
+
528
+ # Update histogram
529
+ data = np.random.normal(np.sin(phase), 1, 1000)
530
+ n, _ = np.histogram(data, bins=self.data_arrays["hist_bins"])
531
+ for count, patch in zip(n, self.artists["hist_patches"]):
532
+ patch.set_height(count)
533
+ mean_val = np.mean(data)
534
+ std_val = np.std(data)
535
+ self.artists["stats_text"].set_text(
536
+ f"μ = {mean_val:.2f}\nσ = {std_val:.2f}\nN = 1000"
537
+ )
538
+
539
+ # Update pie chart
540
+ sizes_new = [
541
+ 30 + 10 * np.sin(phase),
542
+ 25 + 8 * np.sin(phase + 1),
543
+ 20 + 6 * np.sin(phase + 2),
544
+ 15 + 4 * np.sin(phase + 3),
545
+ 10 + 2 * np.sin(phase + 4),
546
+ ]
547
+ sizes_new = [max(5, s) for s in sizes_new] # Ensure positive
548
+ total = sum(sizes_new)
549
+ for i, (wedge, size) in enumerate(zip(self.artists["pie_wedges"], sizes_new)):
550
+ wedge.set_theta1(wedge.theta1)
551
+ wedge.set_theta2(wedge.theta2)
552
+ # Recalculate angles
553
+ angle_start = 90
554
+ for i, (wedge, size) in enumerate(zip(self.artists["pie_wedges"], sizes_new)):
555
+ angle = 360 * size / total
556
+ wedge.set_theta1(angle_start)
557
+ wedge.set_theta2(angle_start + angle)
558
+ angle_start += angle
559
+
560
+ # Update step plot
561
+ x = self.data_arrays["step_x"]
562
+ y = np.abs(np.sin(x * 0.5 + phase)) * 10
563
+ self.artists["step_line"].set_ydata(y)
564
+ self.artists["step_fill"].remove()
565
+ self.artists["step_fill"] = self.ax11.fill_between(
566
+ x, 0, y, step="mid", alpha=0.3, color="lightgreen"
567
+ )
568
+
569
+ # Box plot is static in this example (would be expensive to update)
570
+
571
+ # Update stream plot (this is expensive, so we do it sparingly)
572
+ if frame_idx % 3 == 0: # Only update every 3 frames
573
+ X = self.data_arrays["stream_X"]
574
+ Y = self.data_arrays["stream_Y"]
575
+ rotation = phase
576
+ U = -Y * np.cos(rotation) + X * np.sin(rotation)
577
+ V = X * np.cos(rotation) + Y * np.sin(rotation)
578
+ speed = np.sqrt(U**2 + V**2)
579
+ # Clear the axes and redraw
580
+ self.ax13.clear()
581
+ self.artists["streamplot"] = self.ax13.streamplot(
582
+ X,
583
+ Y,
584
+ U,
585
+ V,
586
+ color=speed,
587
+ cmap="autumn",
588
+ linewidth=1.5,
589
+ density=1.5,
590
+ arrowsize=1.5,
591
+ arrowstyle="->",
592
+ )
593
+ # Restore axes properties
594
+ self.ax13.set_xlim(-3, 3)
595
+ self.ax13.set_ylim(-3, 3)
596
+ self.ax13.set_xlabel("X Coordinate", fontsize=10)
597
+ self.ax13.set_ylabel("Y Coordinate", fontsize=10)
598
+ self.ax13.set_title(
599
+ "Fluid Flow Streamlines", fontsize=12, fontweight="bold"
600
+ )
601
+ self.ax13.set_aspect("equal")
602
+
603
+ # Update error band plot
604
+ x = self.data_arrays["errorband_x"]
605
+ y_mean = np.sin(x + phase)
606
+ y_std = 0.2 + 0.1 * x / 10
607
+ self.artists["errorband_line"].set_ydata(y_mean)
608
+ self.artists["errorband_fill"].remove()
609
+ self.artists["errorband_fill2"].remove()
610
+ self.artists["errorband_fill"] = self.ax14.fill_between(
611
+ x, y_mean - y_std, y_mean + y_std, alpha=0.3, color="blue", zorder=1
612
+ )
613
+ self.artists["errorband_fill2"] = self.ax14.fill_between(
614
+ x,
615
+ y_mean - 2 * y_std,
616
+ y_mean + 2 * y_std,
617
+ alpha=0.15,
618
+ color="blue",
619
+ zorder=0,
620
+ )
621
+
622
+
623
+ if __name__ == "__main__":
624
+ # Generate parameters
625
+ num_frames = 120
626
+ params = [{"phase": 2 * np.pi * i / num_frames} for i in range(num_frames)]
627
+
628
+ # Create and render
629
+ anim = VeryComplexAnimation()
630
+ output_path = Path("example_output/very_complex_animation.mp4")
631
+ output_path.parent.mkdir(parents=True, exist_ok=True)
632
+ anim.make_video(
633
+ output_file=output_path, param_by_frame=params, fps=30, num_workers=8
634
+ )