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.
- parallel_animate/__init__.py +1 -0
- parallel_animate/animator.py +387 -0
- parallel_animate/examples/__init__.py +0 -0
- parallel_animate/examples/multi_panel_animation.py +144 -0
- parallel_animate/examples/scaling_test.py +96 -0
- parallel_animate/examples/simple_wave_animation.py +37 -0
- parallel_animate/examples/very_complex_animation.py +634 -0
- parallel_matplotlib_animation-0.1.0.dist-info/METADATA +125 -0
- parallel_matplotlib_animation-0.1.0.dist-info/RECORD +11 -0
- parallel_matplotlib_animation-0.1.0.dist-info/WHEEL +4 -0
- parallel_matplotlib_animation-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -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
|
+
)
|