Skip to content

Commit d578000

Browse files
feat(matplotlib): implement scatter-animated-controls (#3086)
## Implementation: `scatter-animated-controls` - matplotlib Implements the **matplotlib** version of `scatter-animated-controls`. **File:** `plots/scatter-animated-controls/implementations/matplotlib.py` **Parent Issue:** #3067 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20620301328)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent dcf40b0 commit d578000

2 files changed

Lines changed: 198 additions & 0 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
""" pyplots.ai
2+
scatter-animated-controls: Animated Scatter Plot with Play Controls
3+
Library: matplotlib 3.10.8 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-31
5+
"""
6+
7+
import matplotlib.pyplot as plt
8+
import numpy as np
9+
10+
11+
# Data - Simulated country data inspired by Gapminder
12+
np.random.seed(42)
13+
14+
# 8 countries tracked over 20 years
15+
n_countries = 8
16+
n_years = 20
17+
years = np.arange(2000, 2000 + n_years)
18+
19+
countries = ["Country A", "Country B", "Country C", "Country D", "Country E", "Country F", "Country G", "Country H"]
20+
21+
# Base values for each country (GDP per capita in thousands, life expectancy)
22+
base_gdp = np.array([5, 15, 25, 8, 35, 12, 45, 20])
23+
base_life = np.array([55, 65, 72, 58, 78, 62, 80, 68])
24+
base_pop = np.array([50, 120, 80, 200, 30, 150, 25, 90]) # Population in millions
25+
26+
# Growth rates (GDP grows, life expectancy improves)
27+
gdp_growth = np.array([0.06, 0.04, 0.03, 0.055, 0.02, 0.045, 0.015, 0.035])
28+
life_growth = np.array([0.4, 0.25, 0.15, 0.35, 0.1, 0.3, 0.08, 0.2])
29+
pop_growth = np.array([0.02, 0.01, 0.005, 0.025, 0.003, 0.015, 0.002, 0.008])
30+
31+
# Generate data for all years
32+
gdp_data = np.zeros((n_countries, n_years))
33+
life_data = np.zeros((n_countries, n_years))
34+
pop_data = np.zeros((n_countries, n_years))
35+
36+
for i in range(n_countries):
37+
for t in range(n_years):
38+
noise_gdp = np.random.randn() * 0.5
39+
noise_life = np.random.randn() * 0.3
40+
gdp_data[i, t] = base_gdp[i] * (1 + gdp_growth[i]) ** t + noise_gdp
41+
life_data[i, t] = min(85, base_life[i] + life_growth[i] * t + noise_life)
42+
pop_data[i, t] = base_pop[i] * (1 + pop_growth[i]) ** t
43+
44+
# Colors for countries (colorblind-safe palette - avoiding similar yellows)
45+
colors = ["#306998", "#E69F00", "#CC79A7", "#56B4E9", "#009E73", "#D55E00", "#0072B2", "#882255"]
46+
47+
# Select 4 key time points to show evolution
48+
key_years_idx = [0, 6, 13, 19] # 2000, 2006, 2013, 2019
49+
key_years = years[key_years_idx]
50+
51+
# Create faceted plot - 2x2 grid showing key time points
52+
fig, axes = plt.subplots(2, 2, figsize=(16, 9))
53+
axes = axes.flatten()
54+
55+
for idx, (ax, year_idx) in enumerate(zip(axes, key_years_idx, strict=True)):
56+
year = years[year_idx]
57+
58+
# Plot each country
59+
for i in range(n_countries):
60+
# Size based on population (scaled for visibility)
61+
size = pop_data[i, year_idx] * 3
62+
63+
ax.scatter(
64+
gdp_data[i, year_idx],
65+
life_data[i, year_idx],
66+
s=size,
67+
c=colors[i],
68+
alpha=0.7,
69+
edgecolors="white",
70+
linewidth=1.5,
71+
label=countries[i] if idx == 0 else None,
72+
)
73+
74+
# Add country labels for larger bubbles
75+
if pop_data[i, year_idx] > 80:
76+
ax.annotate(
77+
countries[i].split()[-1],
78+
(gdp_data[i, year_idx], life_data[i, year_idx]),
79+
fontsize=10,
80+
ha="center",
81+
va="center",
82+
fontweight="bold",
83+
color="white",
84+
)
85+
86+
# Year displayed prominently as watermark
87+
ax.text(
88+
0.5,
89+
0.5,
90+
str(year),
91+
transform=ax.transAxes,
92+
fontsize=72,
93+
color="gray",
94+
alpha=0.15,
95+
ha="center",
96+
va="center",
97+
fontweight="bold",
98+
)
99+
100+
# Panel title
101+
ax.set_title(f"Year {year}", fontsize=20, fontweight="bold")
102+
103+
# Axis labels
104+
ax.set_xlabel("GDP per Capita (thousands $)", fontsize=20)
105+
ax.set_ylabel("Life Expectancy (years)", fontsize=20)
106+
ax.tick_params(axis="both", labelsize=16)
107+
108+
# Consistent axis limits across all panels
109+
ax.set_xlim(0, 80)
110+
ax.set_ylim(50, 88)
111+
112+
# Grid
113+
ax.grid(True, alpha=0.3, linestyle="--")
114+
115+
# Main title with play control annotation
116+
fig.suptitle("scatter-animated-controls · matplotlib · pyplots.ai", fontsize=24, fontweight="bold", y=0.98)
117+
118+
# Add subtitle explaining the visualization
119+
fig.text(
120+
0.5,
121+
0.93,
122+
"GDP vs Life Expectancy Over Time (bubble size = population)",
123+
ha="center",
124+
fontsize=16,
125+
style="italic",
126+
color="gray",
127+
)
128+
129+
# Add legend - positioned in bottom right of figure
130+
handles, labels = axes[0].get_legend_handles_labels()
131+
fig.legend(
132+
handles,
133+
labels,
134+
loc="lower center",
135+
ncol=4,
136+
fontsize=12,
137+
frameon=True,
138+
fancybox=True,
139+
shadow=True,
140+
bbox_to_anchor=(0.5, -0.02),
141+
)
142+
143+
# Add prominent animation control panel
144+
control_box = fig.add_axes([0.3, 0.01, 0.4, 0.035])
145+
control_box.set_xlim(0, 10)
146+
control_box.set_ylim(0, 1)
147+
control_box.axis("off")
148+
149+
# Play button
150+
control_box.add_patch(plt.Rectangle((0.2, 0.15), 0.8, 0.7, facecolor="#306998", edgecolor="#1a3d5c", linewidth=2))
151+
control_box.text(0.6, 0.5, "▶", ha="center", va="center", fontsize=16, color="white", fontweight="bold")
152+
153+
# Pause button
154+
control_box.add_patch(plt.Rectangle((1.2, 0.15), 0.8, 0.7, facecolor="#666666", edgecolor="#444444", linewidth=2))
155+
control_box.text(1.6, 0.5, "||", ha="center", va="center", fontsize=14, color="white", fontweight="bold")
156+
157+
# Timeline slider
158+
control_box.add_patch(plt.Rectangle((2.5, 0.35), 7, 0.3, facecolor="#E0E0E0", edgecolor="#999999", linewidth=1))
159+
# Progress indicator
160+
control_box.add_patch(plt.Rectangle((2.5, 0.35), 5.25, 0.3, facecolor="#306998", edgecolor="none"))
161+
# Slider handle
162+
control_box.add_patch(plt.Circle((7.75, 0.5), 0.25, facecolor="white", edgecolor="#306998", linewidth=2))
163+
164+
# Year labels on timeline
165+
control_box.text(2.5, 0.1, "2000", ha="center", va="top", fontsize=10, color="#666666")
166+
control_box.text(9.5, 0.1, "2019", ha="center", va="top", fontsize=10, color="#666666")
167+
control_box.text(7.75, 0.9, "2015", ha="center", va="bottom", fontsize=11, color="#306998", fontweight="bold")
168+
169+
plt.tight_layout(rect=[0, 0.05, 1, 0.92])
170+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
library: matplotlib
2+
specification_id: scatter-animated-controls
3+
created: '2025-12-31T13:52:17Z'
4+
updated: '2025-12-31T14:12:38Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20620301328
7+
issue: 3067
8+
python_version: 3.13.11
9+
library_version: 3.10.8
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/scatter-animated-controls/matplotlib/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/scatter-animated-controls/matplotlib/plot_thumb.png
12+
preview_html: null
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent Gapminder-style visualization with clear temporal storytelling across
17+
4 key time points
18+
- Play/pause controls and timeline slider visually represented, showing the intended
19+
interactivity concept
20+
- Colorblind-safe palette with good distinction between 8 countries
21+
- Prominent year watermarks in each panel enhance time-context awareness
22+
- Clean code structure following KISS principles with realistic country development
23+
data
24+
weaknesses:
25+
- The control panel rendering shows some visual artifacts in the final image (timeline
26+
legend text appears garbled)
27+
- Missing optional trail visualization to show entity paths over time as mentioned
28+
in spec notes

0 commit comments

Comments
 (0)