Skip to content

Commit ef4222d

Browse files
update(bubble-packed): matplotlib — comprehensive quality review
Comprehensive review improving code quality, data choice, visual design, spec compliance, and library feature usage.
1 parent 7e47de1 commit ef4222d

3 files changed

Lines changed: 117 additions & 85 deletions

File tree

Lines changed: 108 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
""" pyplots.ai
1+
"""pyplots.ai
22
bubble-packed: Basic Packed Bubble Chart
3-
Library: matplotlib 3.10.8 | Python 3.13.11
4-
Quality: 92/100 | Created: 2025-12-23
3+
Library: matplotlib 3.10.8 | Python 3.14.3
4+
Quality: /100 | Updated: 2026-02-23
55
"""
66

7+
import matplotlib.collections as mcoll
78
import matplotlib.patches as mpatches
89
import matplotlib.pyplot as plt
910
import numpy as np
1011

1112

1213
# Data - Department budget allocation (in thousands)
13-
np.random.seed(42)
1414
labels = [
1515
"Engineering",
1616
"Marketing",
@@ -28,137 +28,167 @@
2828
"Security",
2929
"QA",
3030
]
31-
values = [850, 420, 680, 320, 180, 290, 750, 210, 150, 380, 240, 550, 460, 170, 195]
32-
33-
# Colors by group (Python Blue primary, Yellow secondary, others colorblind-safe)
34-
colors = [
35-
"#306998", # Engineering - Blue (Tech)
36-
"#FFD43B", # Marketing - Yellow (Business)
37-
"#306998", # Sales - Blue (Revenue)
38-
"#4A90A4", # Operations - Teal (Support)
39-
"#4A90A4", # HR - Teal (Support)
40-
"#4A90A4", # Finance - Teal (Support)
41-
"#FFD43B", # R&D - Yellow (Innovation)
42-
"#4A90A4", # Customer Support - Teal (Support)
43-
"#7B9E89", # Legal - Sage (Compliance)
44-
"#306998", # IT - Blue (Tech)
45-
"#FFD43B", # Design - Yellow (Creative)
46-
"#306998", # Product - Blue (Tech)
47-
"#FFD43B", # Data Science - Yellow (Analytics)
48-
"#7B9E89", # Security - Sage (Compliance)
49-
"#7B9E89", # QA - Sage (Quality)
50-
]
31+
values = [950, 420, 680, 310, 160, 280, 820, 200, 130, 370, 230, 580, 470, 145, 175]
32+
33+
# Group assignments and colors (colorblind-safe palette)
34+
group_map = {
35+
"Engineering": "Tech",
36+
"IT": "Tech",
37+
"Product": "Tech",
38+
"Sales": "Tech",
39+
"Marketing": "Creative",
40+
"R&D": "Creative",
41+
"Design": "Creative",
42+
"Data Science": "Creative",
43+
"Operations": "Support",
44+
"HR": "Support",
45+
"Finance": "Support",
46+
"Customer Support": "Support",
47+
"Legal": "Compliance",
48+
"Security": "Compliance",
49+
"QA": "Compliance",
50+
}
51+
group_colors = {"Tech": "#306998", "Creative": "#FFD43B", "Support": "#4A90A4", "Compliance": "#7B9E89"}
52+
colors = [group_colors[group_map[label]] for label in labels]
5153

5254
# Scale values to radius (sqrt for area-proportional sizing)
53-
min_radius = 0.35
54-
max_radius = 1.9
55-
values_array = np.array(values)
55+
min_radius = 0.30
56+
max_radius = 2.0
57+
values_array = np.array(values, dtype=float)
5658
radii = min_radius + (max_radius - min_radius) * np.sqrt(
5759
(values_array - values_array.min()) / (values_array.max() - values_array.min())
5860
)
5961

60-
# Circle packing using physics simulation
61-
n = len(labels)
62-
63-
# Initial positions in grid
64-
grid_size = int(np.ceil(np.sqrt(n)))
65-
positions = np.zeros((n, 2))
66-
for i in range(n):
67-
positions[i] = [(i % grid_size) * 4 - grid_size * 2, (i // grid_size) * 4 - grid_size * 2]
68-
6962
# Sort by size (largest first) for better packing
63+
n = len(labels)
7064
order = np.argsort(-radii)
71-
positions = positions[order]
7265
radii_sorted = radii[order]
7366
labels_sorted = [labels[i] for i in order]
7467
values_sorted = [values[i] for i in order]
7568
colors_sorted = [colors[i] for i in order]
7669

70+
# Initial positions in spiral pattern for tighter convergence
71+
angles = np.linspace(0, 4 * np.pi, n)
72+
spiral_r = np.linspace(0, 3, n)
73+
positions = np.column_stack([spiral_r * np.cos(angles), spiral_r * np.sin(angles)])
74+
7775
# Physics simulation for packing
78-
for iteration in range(350):
79-
# Pull toward center with decreasing strength
80-
pull_strength = 0.06 * (1 - iteration / 400)
76+
for iteration in range(400):
77+
pull_strength = 0.07 * (1 - iteration / 450)
78+
79+
# Pull toward center
8180
for i in range(n):
82-
dist = np.sqrt(positions[i, 0] ** 2 + positions[i, 1] ** 2)
81+
dist = np.linalg.norm(positions[i])
8382
if dist > 0.01:
8483
positions[i] -= pull_strength * positions[i] / dist
8584

8685
# Push apart overlapping circles
8786
for i in range(n):
8887
for j in range(i + 1, n):
89-
dx = positions[j, 0] - positions[i, 0]
90-
dy = positions[j, 1] - positions[i, 1]
91-
dist = np.sqrt(dx**2 + dy**2)
92-
min_dist = radii_sorted[i] + radii_sorted[j] + 0.05 # Small gap between circles
88+
delta = positions[j] - positions[i]
89+
dist = np.linalg.norm(delta)
90+
min_dist = radii_sorted[i] + radii_sorted[j] + 0.06
9391

9492
if dist < min_dist and dist > 0.001:
9593
overlap = (min_dist - dist) / 2
96-
dx_norm = dx / dist
97-
dy_norm = dy / dist
98-
positions[i, 0] -= overlap * dx_norm
99-
positions[i, 1] -= overlap * dy_norm
100-
positions[j, 0] += overlap * dx_norm
101-
positions[j, 1] += overlap * dy_norm
102-
103-
# Create plot (4800x2700 px at 300 dpi)
94+
direction = delta / dist
95+
positions[i] -= overlap * direction
96+
positions[j] += overlap * direction
97+
98+
# Plot (4800x2700 px at 300 dpi)
10499
fig, ax = plt.subplots(figsize=(16, 9))
105100

106-
# Draw circles
101+
# Draw circles using PatchCollection for efficient rendering
102+
circles = []
103+
face_colors = []
107104
for i in range(n):
108-
circle = mpatches.Circle(
109-
(positions[i, 0], positions[i, 1]),
110-
radii_sorted[i],
111-
facecolor=colors_sorted[i],
112-
edgecolor="white",
113-
linewidth=2.5,
114-
alpha=0.88,
115-
)
116-
ax.add_patch(circle)
117-
118-
# Add labels inside larger circles
119-
label_len = len(labels_sorted[i])
120-
min_radius_for_label = 0.55 + label_len * 0.025
121-
if radii_sorted[i] > min_radius_for_label:
105+
circle = mpatches.Circle((positions[i, 0], positions[i, 1]), radii_sorted[i])
106+
circles.append(circle)
107+
face_colors.append(colors_sorted[i])
108+
109+
collection = mcoll.PatchCollection(
110+
circles, facecolors=face_colors, edgecolors="white", linewidths=2.5, alpha=0.90, zorder=2
111+
)
112+
ax.add_collection(collection)
113+
114+
# Add labels inside circles that are large enough
115+
for i in range(n):
116+
label_chars = len(labels_sorted[i])
117+
min_r_for_label = 0.48 + label_chars * 0.018
118+
if radii_sorted[i] > min_r_for_label:
122119
font_scale = min(1.0, radii_sorted[i] / 1.4)
123120
label_fontsize = max(9, int(15 * font_scale))
124121
value_fontsize = max(8, int(13 * font_scale))
122+
123+
# Determine text color based on background brightness
124+
bg_color = colors_sorted[i]
125+
text_color = "#1a1a2e" if bg_color == "#FFD43B" else "white"
126+
127+
# Wrap long labels for smaller circles
128+
display_label = labels_sorted[i]
129+
is_wrapped = False
130+
if " " in display_label and radii_sorted[i] < 1.0:
131+
display_label = display_label.replace(" ", "\n")
132+
is_wrapped = True
133+
134+
# Adjust vertical offsets for wrapped vs single-line labels
135+
label_y_offset = 0.05 if is_wrapped else 0.12
136+
value_y_offset = -0.35 if is_wrapped else -0.22
137+
125138
ax.text(
126139
positions[i, 0],
127-
positions[i, 1] + radii_sorted[i] * 0.1,
128-
labels_sorted[i],
140+
positions[i, 1] + radii_sorted[i] * label_y_offset,
141+
display_label,
129142
ha="center",
130143
va="center",
131144
fontsize=label_fontsize,
132145
fontweight="bold",
133-
color="white",
146+
color=text_color,
147+
zorder=3,
134148
)
135149
ax.text(
136150
positions[i, 0],
137-
positions[i, 1] - radii_sorted[i] * 0.22,
151+
positions[i, 1] + radii_sorted[i] * value_y_offset,
138152
f"${values_sorted[i]}K",
139153
ha="center",
140154
va="center",
141155
fontsize=value_fontsize,
142-
color="white",
143-
alpha=0.95,
156+
color=text_color,
157+
alpha=0.85,
158+
zorder=3,
144159
)
145160

146-
# Set axis limits with padding
161+
# Axis limits with padding
147162
all_x = positions[:, 0]
148163
all_y = positions[:, 1]
149164
max_r = radii_sorted.max()
150-
padding = 0.6
165+
padding = 0.8
151166
ax.set_xlim(all_x.min() - max_r - padding, all_x.max() + max_r + padding)
152167
ax.set_ylim(all_y.min() - max_r - padding, all_y.max() + max_r + padding)
153168
ax.set_aspect("equal")
154-
155-
# Remove axes for clean visualization
156169
ax.axis("off")
157170

158171
# Title
159172
ax.set_title(
160173
"Department Budget Allocation · bubble-packed · matplotlib · pyplots.ai", fontsize=24, fontweight="bold", pad=20
161174
)
162175

176+
# Legend for group colors
177+
legend_handles = [
178+
mpatches.Patch(facecolor=color, edgecolor="white", linewidth=1.5, label=group)
179+
for group, color in group_colors.items()
180+
]
181+
ax.legend(
182+
handles=legend_handles,
183+
loc="lower right",
184+
fontsize=16,
185+
framealpha=0.9,
186+
edgecolor="#cccccc",
187+
fancybox=True,
188+
borderpad=0.8,
189+
handlelength=1.5,
190+
handleheight=1.2,
191+
)
192+
163193
plt.tight_layout()
164194
plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="white")

plots/bubble-packed/metadata/matplotlib.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
library: matplotlib
22
specification_id: bubble-packed
33
created: '2025-12-23T09:16:10Z'
4-
updated: '2025-12-23T09:18:25Z'
5-
generated_by: claude-opus-4-5-20251101
4+
updated: 2026-02-23T15:35:00+00:00
5+
generated_by: claude-opus-4-6
66
workflow_run: 20456558183
77
issue: 0
8-
python_version: 3.13.11
9-
library_version: 3.10.8
8+
python_version: "3.14.3"
9+
library_version: "3.10.8"
1010
preview_url: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/matplotlib/plot.png
1111
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/matplotlib/plot_thumb.png
1212
preview_html: null
13-
quality_score: 92
13+
quality_score: null
1414
impl_tags:
1515
dependencies: []
1616
techniques:

plots/bubble-packed/specification.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: Basic Packed Bubble Chart
66

77
# Specification tracking
88
created: 2025-12-15T20:43:45Z
9-
updated: 2025-12-15T20:43:45Z
9+
updated: 2026-02-23T12:00:00Z
1010
issue: 992
1111
suggested: MarkusNeusinger
1212

@@ -15,13 +15,15 @@ tags:
1515
plot_type:
1616
- bubble
1717
- packed
18+
- circlepacking
1819
data_type:
1920
- categorical
2021
- numeric
22+
- proportional
2123
domain:
2224
- general
2325
- business
2426
features:
2527
- basic
2628
- comparison
27-
- hierarchical
29+
- proportional

0 commit comments

Comments
 (0)