Skip to content

Commit c44caf5

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

2 files changed

Lines changed: 111 additions & 163 deletions

File tree

Lines changed: 106 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,200 +1,147 @@
1-
""" pyplots.ai
1+
"""pyplots.ai
22
bubble-packed: Basic Packed Bubble Chart
3-
Library: seaborn 0.13.2 | Python 3.13.11
4-
Quality: 90/100 | Created: 2025-12-23
3+
Library: seaborn 0.13.2 | Python 3.14.3
4+
Quality: /100 | Updated: 2026-02-23
55
"""
66

7-
import matplotlib.patches as mpatches
87
import matplotlib.pyplot as plt
98
import numpy as np
109
import pandas as pd
1110
import seaborn as sns
1211

1312

1413
# Data - Company market values by sector (billions USD)
15-
np.random.seed(42)
16-
data = {
14+
sectors = {
1715
"Technology": [("Apple", 180), ("Microsoft", 160), ("Google", 120), ("NVIDIA", 95), ("Meta", 75)],
1816
"Finance": [("JPMorgan", 85), ("Visa", 70), ("Mastercard", 55), ("Goldman Sachs", 45)],
1917
"Healthcare": [("UnitedHealth", 90), ("J&J", 65), ("Merck", 50), ("Pfizer", 40)],
2018
"Retail": [("Amazon", 140), ("Walmart", 60), ("Costco", 45), ("Target", 30)],
2119
}
2220

23-
# Prepare circles sorted by size (largest first for better packing)
24-
all_circles = []
25-
for group, items in data.items():
26-
for name, value in items:
27-
radius = np.sqrt(value) * 4 # Scale by area
28-
all_circles.append({"name": name, "radius": radius, "group": group, "value": value})
29-
30-
all_circles.sort(key=lambda x: -x["radius"])
31-
32-
# Circle packing - place circles one by one without overlap (inline, no functions)
33-
placed_circles = []
34-
cx, cy = 0, 0
35-
36-
for circle in all_circles:
37-
new_radius = circle["radius"]
38-
39-
if not placed_circles:
40-
# First circle at origin
41-
best_x, best_y = cx, cy
42-
else:
43-
# Try positions around existing circles, find closest to center
44-
best_pos = None
45-
best_dist = float("inf")
46-
47-
for p in placed_circles:
48-
for angle in np.linspace(0, 2 * np.pi, 72, endpoint=False):
49-
dist = p["radius"] + new_radius + 2
50-
test_x = p["x"] + dist * np.cos(angle)
51-
test_y = p["y"] + dist * np.sin(angle)
52-
53-
# Check overlap with all placed circles
54-
valid = True
55-
for other in placed_circles:
56-
d = np.sqrt((test_x - other["x"]) ** 2 + (test_y - other["y"]) ** 2)
57-
if d < other["radius"] + new_radius + 1:
58-
valid = False
59-
break
60-
61-
if valid:
62-
center_dist = np.sqrt((test_x - cx) ** 2 + (test_y - cy) ** 2)
63-
if center_dist < best_dist:
64-
best_dist = center_dist
65-
best_pos = (test_x, test_y)
66-
67-
best_x, best_y = best_pos if best_pos else (cx, cy)
68-
69-
placed_circles.append(
70-
{
71-
"x": best_x,
72-
"y": best_y,
73-
"radius": new_radius,
74-
"name": circle["name"],
75-
"group": circle["group"],
76-
"value": circle["value"],
77-
}
78-
)
79-
80-
# Calculate bounds and recenter
81-
all_x = [c["x"] for c in placed_circles]
82-
all_y = [c["y"] for c in placed_circles]
83-
all_r = [c["radius"] for c in placed_circles]
84-
85-
min_x = min(x - r for x, r in zip(all_x, all_r, strict=True))
86-
max_x = max(x + r for x, r in zip(all_x, all_r, strict=True))
87-
min_y = min(y - r for y, r in zip(all_y, all_r, strict=True))
88-
max_y = max(y + r for y, r in zip(all_y, all_r, strict=True))
89-
90-
# Offset to center in plot area
91-
padding = 20
92-
offset_x = -min_x + padding
93-
offset_y = -min_y + padding
94-
95-
for c in placed_circles:
96-
c["x"] += offset_x
97-
c["y"] += offset_y
98-
99-
plot_width = max_x - min_x + 2 * padding
100-
plot_height = max_y - min_y + 2 * padding
101-
102-
# Create DataFrame for seaborn
103-
df = pd.DataFrame(placed_circles)
104-
# Scale marker size for scatterplot (s parameter uses area in points^2)
105-
df["marker_size"] = (df["radius"] * 2) ** 2 * 3.14 # Convert radius to area for proper sizing
106-
107-
# Set seaborn style
108-
sns.set_style("white")
109-
palette = sns.color_palette("Set2", n_colors=len(data))
110-
group_colors = {group: palette[i] for i, group in enumerate(data.keys())}
111-
112-
# Create figure
21+
records = []
22+
for sector, companies in sectors.items():
23+
for name, value in companies:
24+
records.append({"name": name, "value": value, "sector": sector, "radius": np.sqrt(value) * 4})
25+
26+
df = pd.DataFrame(records).sort_values("radius", ascending=False).reset_index(drop=True)
27+
28+
# Circle packing - place circles one by one, closest to center without overlap
29+
placed_x, placed_y, placed_r = [], [], []
30+
31+
for _, row in df.iterrows():
32+
r = row["radius"]
33+
34+
if not placed_x:
35+
placed_x.append(0.0)
36+
placed_y.append(0.0)
37+
placed_r.append(r)
38+
continue
39+
40+
best_pos, best_dist = None, float("inf")
41+
px_arr, py_arr, pr_arr = np.array(placed_x), np.array(placed_y), np.array(placed_r)
42+
43+
for i in range(len(placed_x)):
44+
for angle in np.linspace(0, 2 * np.pi, 72, endpoint=False):
45+
gap = placed_r[i] + r + 2
46+
tx = placed_x[i] + gap * np.cos(angle)
47+
ty = placed_y[i] + gap * np.sin(angle)
48+
49+
dists = np.sqrt((px_arr - tx) ** 2 + (py_arr - ty) ** 2)
50+
if np.all(dists >= pr_arr + r + 1):
51+
cdist = np.sqrt(tx**2 + ty**2)
52+
if cdist < best_dist:
53+
best_dist = cdist
54+
best_pos = (tx, ty)
55+
56+
bx, by = best_pos if best_pos else (0.0, 0.0)
57+
placed_x.append(bx)
58+
placed_y.append(by)
59+
placed_r.append(r)
60+
61+
df["x"] = placed_x
62+
df["y"] = placed_y
63+
64+
# Recenter so all coordinates are positive
65+
pad = 20
66+
df["x"] = df["x"] - (df["x"] - df["radius"]).min() + pad
67+
df["y"] = df["y"] - (df["y"] - df["radius"]).min() + pad
68+
plot_w = (df["x"] + df["radius"]).max() + pad
69+
plot_h = (df["y"] + df["radius"]).max() + pad
70+
71+
# Seaborn styling
72+
sns.set_theme(style="white", context="talk", font_scale=1.2)
73+
palette = sns.color_palette("Set2", n_colors=len(sectors))
74+
sector_colors = dict(zip(sectors.keys(), palette, strict=True))
75+
76+
# Plot
11377
fig, ax = plt.subplots(figsize=(16, 9))
78+
ax.set_xlim(0, plot_w)
79+
ax.set_ylim(0, plot_h)
80+
ax.set_aspect("equal")
11481

115-
# Use seaborn scatterplot for the bubbles
82+
# Compute scatter marker sizes: convert data-unit diameter to points^2
83+
# At dpi=100 (default), figsize=(16,9) => 1600x900 pixels for the figure
84+
# With aspect="equal", the effective data range that fits is limited by the smaller axis
85+
fig.canvas.draw()
86+
transform = ax.transData
87+
# Get scale: how many display points per data unit
88+
p0 = transform.transform((0, 0))
89+
p1 = transform.transform((1, 0))
90+
pts_per_unit = (p1[0] - p0[0]) * 72 / fig.dpi # convert pixels to points
91+
df["marker_size"] = (df["radius"] * 2 * pts_per_unit) ** 2
92+
93+
# Draw bubbles with seaborn scatterplot
11694
sns.scatterplot(
11795
data=df,
11896
x="x",
11997
y="y",
120-
hue="group",
98+
hue="sector",
12199
size="marker_size",
122100
sizes=(df["marker_size"].min(), df["marker_size"].max()),
123-
palette="Set2",
101+
palette=sector_colors,
124102
alpha=0.9,
125103
edgecolor="white",
126104
linewidth=3,
127105
legend=False,
128106
ax=ax,
129107
)
130108

131-
# Add labels for all circles using annotations
132-
# Sort by x position to manage external annotation placement
133-
df_sorted = df.sort_values("x")
134-
used_y_positions = [] # Track y positions for external labels to avoid overlap
135-
136-
for _, row in df_sorted.iterrows():
137-
name = row["name"]
138-
# Abbreviate long names for internal labels
139-
short_name = name if len(name) <= 10 else name[:9] + "."
140-
141-
if row["radius"] > 38:
142-
# Large circles - full name with large font
143-
ax.text(row["x"], row["y"], short_name, ha="center", va="center", fontsize=18, fontweight="bold", color="white")
144-
elif row["radius"] > 32:
145-
# Medium-large circles
146-
ax.text(row["x"], row["y"], short_name, ha="center", va="center", fontsize=14, fontweight="bold", color="white")
147-
elif row["radius"] > 26:
148-
# Medium circles - smaller font
149-
ax.text(row["x"], row["y"], short_name, ha="center", va="center", fontsize=11, fontweight="bold", color="white")
109+
# Labels inside circles - scale font with bubble size, clip long names
110+
for _, row in df.iterrows():
111+
label = row["name"]
112+
r = row["radius"]
113+
if r > 38:
114+
fs, max_chars = 18, 12
115+
elif r > 30:
116+
fs, max_chars = 14, 12
117+
elif r > 24:
118+
fs, max_chars = 11, 10
150119
else:
151-
# Small circles - external annotation with arrow
152-
# Determine label position (alternate left/right based on position)
153-
if row["x"] < plot_width / 2:
154-
# Left side - annotate to the left
155-
offset_x = -row["radius"] - 20
156-
ha = "right"
157-
else:
158-
# Right side - annotate to the right
159-
offset_x = row["radius"] + 20
160-
ha = "left"
161-
162-
# Adjust y to avoid overlapping labels
163-
target_y = row["y"]
164-
for used_y in used_y_positions:
165-
if abs(target_y - used_y) < 25:
166-
target_y = used_y + 25 if target_y >= used_y else used_y - 25
167-
used_y_positions.append(target_y)
168-
169-
ax.annotate(
170-
name,
171-
xy=(row["x"], row["y"]),
172-
xytext=(row["x"] + offset_x, target_y),
173-
fontsize=11,
174-
fontweight="bold",
175-
color="#444444",
176-
arrowprops={"arrowstyle": "->", "color": "#888888", "lw": 1.5, "connectionstyle": "arc3,rad=0.1"},
177-
ha=ha,
178-
va="center",
179-
)
180-
181-
# Configure axes
182-
ax.set_xlim(0, plot_width)
183-
ax.set_ylim(0, plot_height)
184-
ax.set_aspect("equal")
120+
fs, max_chars = 8, 7
121+
if len(label) > max_chars:
122+
label = label[: max_chars - 1] + "."
123+
ax.text(row["x"], row["y"], label, ha="center", va="center", fontsize=fs, fontweight="bold", color="white")
124+
185125
ax.axis("off")
186126

187127
# Title
188-
ax.set_title("bubble-packed · seaborn · pyplots.ai", fontsize=24, fontweight="bold", pad=20)
128+
ax.set_title(
129+
"Market Capitalization by Sector\nbubble-packed \u00b7 seaborn \u00b7 pyplots.ai",
130+
fontsize=24,
131+
fontweight="medium",
132+
pad=20,
133+
linespacing=1.4,
134+
)
189135

190-
# Create legend - position below the plot to avoid any overlap with data
191-
legend_elements = [
192-
mpatches.Patch(facecolor=group_colors[group], edgecolor="white", linewidth=2, label=group) for group in data.keys()
136+
# Legend with circular markers matching bubble colors
137+
handles = [
138+
plt.Line2D([0], [0], marker="o", color="w", markerfacecolor=sector_colors[s], markersize=14, label=s)
139+
for s in sectors
193140
]
194141
ax.legend(
195-
handles=legend_elements,
142+
handles=handles,
196143
loc="upper center",
197-
bbox_to_anchor=(0.5, -0.02),
144+
bbox_to_anchor=(0.5, -0.01),
198145
ncol=4,
199146
fontsize=14,
200147
framealpha=0.95,
@@ -203,5 +150,6 @@
203150
edgecolor="gray",
204151
)
205152

153+
sns.despine(left=True, bottom=True)
206154
plt.tight_layout()
207155
plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="white")

plots/bubble-packed/metadata/seaborn.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
library: seaborn
22
specification_id: bubble-packed
33
created: '2025-12-23T09:17:35Z'
4-
updated: '2025-12-23T09:33:29Z'
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: 20456557869
77
issue: 0
8-
python_version: 3.13.11
9-
library_version: 0.13.2
8+
python_version: "3.14.3"
9+
library_version: "0.13.2"
1010
preview_url: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/seaborn/plot.png
1111
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/seaborn/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)