Skip to content

Commit 6ff3779

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

2 files changed

Lines changed: 54 additions & 76 deletions

File tree

plots/bubble-packed/implementations/plotnine.py

Lines changed: 49 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
""" pyplots.ai
1+
"""pyplots.ai
22
bubble-packed: Basic Packed Bubble Chart
3-
Library: plotnine 0.15.2 | Python 3.13.11
4-
Quality: 90/100 | Created: 2025-12-23
3+
Library: plotnine 0.15.3 | Python 3.14.3
4+
Quality: /100 | Updated: 2026-02-23
55
"""
66

77
import numpy as np
88
import pandas as pd
99
from plotnine import (
1010
aes,
1111
coord_fixed,
12-
element_blank,
12+
element_rect,
1313
element_text,
1414
geom_polygon,
1515
geom_text,
1616
ggplot,
17+
guides,
1718
labs,
1819
scale_fill_manual,
1920
theme,
@@ -22,8 +23,7 @@
2223

2324

2425
# Data - department budgets (in millions)
25-
np.random.seed(42)
26-
data = {
26+
departments = {
2727
"label": [
2828
"Engineering",
2929
"Marketing",
@@ -34,7 +34,7 @@
3434
"R&D",
3535
"IT Support",
3636
"Legal",
37-
"Customer Service",
37+
"Customer Svc",
3838
"Design",
3939
"Logistics",
4040
"Quality",
@@ -61,41 +61,36 @@
6161
],
6262
}
6363

64-
df = pd.DataFrame(data)
64+
df = pd.DataFrame(departments)
6565

6666
# Scale values to radii (area-based scaling for accurate perception)
6767
max_radius = 1.0
68-
min_radius = 0.3
68+
min_radius = 0.25
6969
df["radius"] = min_radius + (max_radius - min_radius) * np.sqrt(df["value"] / df["value"].max())
7070

71-
# Circle packing using force simulation (inline, KISS style)
71+
# Circle packing - greedy placement + force simulation
7272
n = len(df)
7373
radii = df["radius"].values
74-
75-
# Sort by size (largest first) for better packing
7674
idx = np.argsort(-radii)
7775
sorted_radii = radii[idx]
76+
gap = 0.03
7877

79-
# Initialize positions
8078
x = np.zeros(n)
8179
y = np.zeros(n)
8280

83-
# Place circles using greedy algorithm
8481
for i in range(1, n):
8582
best_dist = float("inf")
8683
best_x, best_y = 0.0, 0.0
8784

88-
for angle in np.linspace(0, 2 * np.pi, 36):
85+
for angle in np.linspace(0, 2 * np.pi, 72, endpoint=False):
8986
for ref in range(i):
90-
# Try placing next to reference circle
91-
test_x = x[ref] + (sorted_radii[ref] + sorted_radii[i] + 0.05) * np.cos(angle)
92-
test_y = y[ref] + (sorted_radii[ref] + sorted_radii[i] + 0.05) * np.sin(angle)
87+
test_x = x[ref] + (sorted_radii[ref] + sorted_radii[i] + gap) * np.cos(angle)
88+
test_y = y[ref] + (sorted_radii[ref] + sorted_radii[i] + gap) * np.sin(angle)
9389

94-
# Check for collisions
9590
valid = True
9691
for j in range(i):
9792
dist = np.sqrt((test_x - x[j]) ** 2 + (test_y - y[j]) ** 2)
98-
if dist < sorted_radii[i] + sorted_radii[j] + 0.03:
93+
if dist < sorted_radii[i] + sorted_radii[j] + gap:
9994
valid = False
10095
break
10196

@@ -109,94 +104,77 @@
109104
y[i] = best_y
110105

111106
# Force simulation to tighten packing
112-
for _ in range(1000):
113-
# Move toward center
114-
x -= x * 0.001
115-
y -= y * 0.001
107+
for _ in range(2000):
108+
x -= x * 0.003
109+
y -= y * 0.003
116110

117-
# Separate overlapping circles
118111
for i in range(n):
119112
for j in range(i + 1, n):
120113
dx = x[j] - x[i]
121114
dy = y[j] - y[i]
122115
dist = np.sqrt(dx * dx + dy * dy)
123-
min_dist = sorted_radii[i] + sorted_radii[j] + 0.03
116+
min_dist = sorted_radii[i] + sorted_radii[j] + gap
124117

125118
if dist < min_dist and dist > 0.001:
126119
overlap = (min_dist - dist) / 2
127120
dx_norm = dx / dist
128121
dy_norm = dy / dist
129-
x[i] -= overlap * dx_norm * 0.5
130-
y[i] -= overlap * dy_norm * 0.5
131-
x[j] += overlap * dx_norm * 0.5
132-
y[j] += overlap * dy_norm * 0.5
122+
x[i] -= overlap * dx_norm
123+
y[i] -= overlap * dy_norm
124+
x[j] += overlap * dx_norm
125+
y[j] += overlap * dy_norm
133126

134127
# Restore original order
135-
x_out = np.zeros(n)
136-
y_out = np.zeros(n)
128+
x_final = np.zeros(n)
129+
y_final = np.zeros(n)
137130
for i, orig_idx in enumerate(idx):
138-
x_out[orig_idx] = x[i]
139-
y_out[orig_idx] = y[i]
131+
x_final[orig_idx] = x[i]
132+
y_final[orig_idx] = y[i]
140133

141-
df["x"] = x_out
142-
df["y"] = y_out
134+
df["x"] = x_final
135+
df["y"] = y_final
143136

144-
# Create circle polygons for geom_polygon
137+
# Build circle polygons for geom_polygon
145138
circle_dfs = []
139+
angles = np.linspace(0, 2 * np.pi, 64)
146140
for i, row in df.iterrows():
147-
angles = np.linspace(0, 2 * np.pi, 64)
148141
cx = row["x"] + row["radius"] * np.cos(angles)
149142
cy = row["y"] + row["radius"] * np.sin(angles)
150-
circle_df = pd.DataFrame({"x": cx, "y": cy, "label": row["label"], "group": row["group"], "circle_id": i})
151-
circle_dfs.append(circle_df)
152-
143+
circle_dfs.append(pd.DataFrame({"x": cx, "y": cy, "label": row["label"], "group": row["group"], "circle_id": i}))
153144
circles_df = pd.concat(circle_dfs, ignore_index=True)
145+
circles_df["group"] = pd.Categorical(circles_df["group"], categories=["Tech", "Business", "Operations", "Support"])
154146

155-
# Color palette for groups - colorblind-safe (Okabe-Ito palette)
156-
group_colors = {
157-
"Tech": "#0072B2", # Blue
158-
"Business": "#E69F00", # Orange
159-
"Operations": "#009E73", # Bluish Green
160-
"Support": "#CC79A7", # Reddish Purple
161-
}
162-
163-
# Create label dataframe (centers) - show full labels for circles large enough
164-
labels_df = df[["x", "y", "label", "radius"]].copy()
165-
166-
# Show full label for large circles, abbreviated for medium, none for small
147+
# Labels - full name for large, first word for medium, none for small
148+
labels_df = df.copy()
167149
labels_df["display_label"] = labels_df.apply(
168-
lambda row: (
169-
row["label"]
170-
if row["radius"] >= 0.85
171-
else (
172-
(row["label"][:8] if len(row["label"]) > 8 else row["label"])
173-
if row["radius"] >= 0.6
174-
else ((row["label"][:5] if len(row["label"]) > 5 else row["label"]) if row["radius"] >= 0.45 else "")
175-
)
176-
),
177-
axis=1,
150+
lambda row: row["label"] if row["value"] >= 22 else (row["label"].split()[0] if row["value"] >= 12 else ""), axis=1
178151
)
179152

180-
# Create plot
153+
# Color palette - Okabe-Ito colorblind-safe
154+
group_colors = {"Tech": "#0072B2", "Business": "#E69F00", "Operations": "#009E73", "Support": "#CC79A7"}
155+
156+
# Plot
181157
plot = (
182158
ggplot()
183159
+ geom_polygon(
184-
data=circles_df, mapping=aes(x="x", y="y", fill="group", group="circle_id"), color="white", size=0.5, alpha=0.85
160+
data=circles_df, mapping=aes(x="x", y="y", fill="group", group="circle_id"), color="white", size=0.6, alpha=0.85
185161
)
186162
+ geom_text(
187163
data=labels_df, mapping=aes(x="x", y="y", label="display_label"), size=9, color="white", fontweight="bold"
188164
)
189-
+ scale_fill_manual(values=group_colors)
165+
+ scale_fill_manual(values=group_colors, name="Department Group")
166+
+ guides(fill="legend")
190167
+ coord_fixed()
191-
+ labs(title="bubble-packed · plotnine · pyplots.ai", fill="Department Group")
168+
+ labs(title="bubble-packed · plotnine · pyplots.ai")
192169
+ theme_void()
193170
+ theme(
194171
figure_size=(16, 9),
195-
plot_title=element_text(size=24, ha="center", weight="bold"),
196-
legend_title=element_text(size=18),
197-
legend_text=element_text(size=14),
172+
plot_title=element_text(size=24, ha="center", weight="bold", margin={"b": 12}),
173+
legend_title=element_text(size=18, weight="bold"),
174+
legend_text=element_text(size=16),
198175
legend_position="right",
199-
plot_background=element_blank(),
176+
legend_key=element_rect(fill="white", color="none"),
177+
plot_background=element_rect(fill="white", color="none"),
200178
)
201179
)
202180

plots/bubble-packed/metadata/plotnine.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
library: plotnine
22
specification_id: bubble-packed
33
created: '2025-12-23T09:16:24Z'
4-
updated: '2025-12-23T09:27:41Z'
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: 20456560252
77
issue: 0
8-
python_version: 3.13.11
9-
library_version: 0.15.2
8+
python_version: "3.14.3"
9+
library_version: "0.15.3"
1010
preview_url: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/plotnine/plot.png
1111
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/plotnine/plot_thumb.png
1212
preview_html: null
13-
quality_score: 90
13+
quality_score: null
1414
impl_tags:
1515
dependencies: []
1616
techniques:

0 commit comments

Comments
 (0)