Skip to content

Commit 5b6b736

Browse files
feat(matplotlib): implement circlepacking-basic (#2521)
## Implementation: `circlepacking-basic` - matplotlib Implements the **matplotlib** version of `circlepacking-basic`. **File:** `plots/circlepacking-basic/implementations/matplotlib.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20585399783)* --------- 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 eb93993 commit 5b6b736

2 files changed

Lines changed: 200 additions & 0 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
""" pyplots.ai
2+
circlepacking-basic: Circle Packing Chart
3+
Library: matplotlib 3.10.8 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-30
5+
"""
6+
7+
import matplotlib.patches as patches
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
11+
12+
# Set random seed for reproducibility
13+
np.random.seed(42)
14+
15+
# Hierarchical data: Company departments with team sizes
16+
# Structure: {name: value} for leaves, {name: {children}} for branches
17+
hierarchy_data = [
18+
("Company", None, 0), # Root
19+
("Engineering", "Company", 1),
20+
("Product", "Company", 1),
21+
("Operations", "Company", 1),
22+
("Sales", "Company", 1),
23+
("Frontend", "Engineering", 25),
24+
("Backend", "Engineering", 35),
25+
("DevOps", "Engineering", 15),
26+
("QA", "Engineering", 20),
27+
("Design", "Product", 18),
28+
("Research", "Product", 12),
29+
("PM", "Product", 8),
30+
("HR", "Operations", 10),
31+
("Finance", "Operations", 12),
32+
("Legal", "Operations", 6),
33+
("Admin", "Operations", 8),
34+
("North", "Sales", 22),
35+
("South", "Sales", 18),
36+
("Intl", "Sales", 28),
37+
]
38+
39+
# Build node structure with computed values
40+
nodes = {}
41+
for name, parent, value in hierarchy_data:
42+
nodes[name] = {"name": name, "parent": parent, "value": value, "children": []}
43+
44+
# Link children to parents
45+
for name, node in nodes.items():
46+
if node["parent"]:
47+
nodes[node["parent"]]["children"].append(name)
48+
49+
# Compute values for branch nodes (sum of children)
50+
for name in ["Engineering", "Product", "Operations", "Sales"]:
51+
nodes[name]["value"] = sum(nodes[c]["value"] for c in nodes[name]["children"])
52+
nodes["Company"]["value"] = sum(nodes[c]["value"] for c in nodes["Company"]["children"])
53+
54+
# Color scheme by depth level
55+
depth_colors = {
56+
0: "#306998", # Python Blue - root
57+
1: "#FFD43B", # Python Yellow - departments
58+
2: "#5BA0D0", # Light blue - teams
59+
}
60+
depth_alphas = {0: 0.3, 1: 0.7, 2: 0.7}
61+
62+
# Circle packing layout - compute positions
63+
# We'll use a simple analytical approach for cleaner containment
64+
circles = []
65+
66+
# Root circle
67+
root_radius = 280
68+
circles.append({"name": "Company", "x": 0, "y": 0, "radius": root_radius, "depth": 0})
69+
70+
# Department positioning (4 departments in quadrants)
71+
dept_names = ["Engineering", "Product", "Operations", "Sales"]
72+
dept_values = [nodes[d]["value"] for d in dept_names]
73+
total_dept = sum(dept_values)
74+
75+
# Position departments in a ring within root
76+
dept_ring_radius = root_radius * 0.52
77+
dept_angles = [np.pi * 0.75, np.pi * 0.25, -np.pi * 0.25, -np.pi * 0.75]
78+
79+
dept_circles = {}
80+
for i, dept in enumerate(dept_names):
81+
# Radius proportional to sqrt of value
82+
dept_radius = np.sqrt(dept_values[i] / total_dept) * root_radius * 0.42
83+
dept_x = dept_ring_radius * np.cos(dept_angles[i])
84+
dept_y = dept_ring_radius * np.sin(dept_angles[i])
85+
circles.append({"name": dept, "x": dept_x, "y": dept_y, "radius": dept_radius, "depth": 1})
86+
dept_circles[dept] = {"x": dept_x, "y": dept_y, "radius": dept_radius}
87+
88+
# Team positioning within each department
89+
for dept in dept_names:
90+
children = nodes[dept]["children"]
91+
if not children:
92+
continue
93+
94+
parent = dept_circles[dept]
95+
child_values = [nodes[c]["value"] for c in children]
96+
total_child = sum(child_values)
97+
n_children = len(children)
98+
99+
# Arrange children in a circle within parent
100+
child_ring_radius = parent["radius"] * 0.55
101+
angle_step = 2 * np.pi / n_children
102+
start_angle = np.pi / 2
103+
104+
for j, child in enumerate(children):
105+
child_radius = np.sqrt(child_values[j] / total_child) * parent["radius"] * 0.40
106+
angle = start_angle + j * angle_step
107+
child_x = parent["x"] + child_ring_radius * np.cos(angle)
108+
child_y = parent["y"] + child_ring_radius * np.sin(angle)
109+
110+
# Ensure child stays within parent boundary
111+
dist_to_parent_center = np.sqrt((child_x - parent["x"]) ** 2 + (child_y - parent["y"]) ** 2)
112+
max_dist = parent["radius"] - child_radius - 3 # padding
113+
if dist_to_parent_center + child_radius > parent["radius"] - 2:
114+
scale = max_dist / dist_to_parent_center
115+
child_x = parent["x"] + (child_x - parent["x"]) * scale
116+
child_y = parent["y"] + (child_y - parent["y"]) * scale
117+
118+
circles.append({"name": child, "x": child_x, "y": child_y, "radius": child_radius, "depth": 2})
119+
120+
# Create figure (square format for symmetric visualization)
121+
fig, ax = plt.subplots(figsize=(12, 12))
122+
123+
# Draw circles from largest to smallest (painter's algorithm)
124+
circles_sorted = sorted(circles, key=lambda c: -c["radius"])
125+
126+
for circle in circles_sorted:
127+
color = depth_colors.get(circle["depth"], "#AAAAAA")
128+
alpha = depth_alphas.get(circle["depth"], 0.7)
129+
130+
circ = patches.Circle(
131+
(circle["x"], circle["y"]), circle["radius"], facecolor=color, edgecolor="#2C3E50", linewidth=2.5, alpha=alpha
132+
)
133+
ax.add_patch(circ)
134+
135+
# Add labels for circles that are large enough
136+
if circle["radius"] > 30:
137+
fontsize = min(18, max(11, circle["radius"] * 0.25))
138+
# Dark text on yellow, white on other colors
139+
text_color = "#1A1A1A" if circle["depth"] == 1 else "#FFFFFF"
140+
ax.text(
141+
circle["x"],
142+
circle["y"],
143+
circle["name"],
144+
ha="center",
145+
va="center",
146+
fontsize=fontsize,
147+
fontweight="bold",
148+
color=text_color,
149+
)
150+
151+
# Set equal aspect ratio and limits
152+
ax.set_aspect("equal")
153+
padding = root_radius * 0.15
154+
ax.set_xlim(-root_radius - padding, root_radius + padding)
155+
ax.set_ylim(-root_radius - padding, root_radius + padding)
156+
157+
# Remove axes for cleaner visualization
158+
ax.axis("off")
159+
160+
# Title - exact format: {spec-id} · {library} · pyplots.ai
161+
ax.set_title("circlepacking-basic · matplotlib · pyplots.ai", fontsize=24, fontweight="bold", pad=20)
162+
163+
# Legend with correct colors matching actual rendering
164+
legend_elements = [
165+
patches.Patch(facecolor="#306998", edgecolor="#2C3E50", alpha=0.3, label="Company (Root)"),
166+
patches.Patch(facecolor="#FFD43B", edgecolor="#2C3E50", alpha=0.7, label="Departments"),
167+
patches.Patch(facecolor="#5BA0D0", edgecolor="#2C3E50", alpha=0.7, label="Teams"),
168+
]
169+
ax.legend(handles=legend_elements, loc="upper right", fontsize=16, framealpha=0.9)
170+
171+
plt.tight_layout()
172+
plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="white")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
library: matplotlib
2+
specification_id: circlepacking-basic
3+
created: '2025-12-30T00:04:06Z'
4+
updated: '2025-12-30T00:23:08Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20585399783
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 3.10.8
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/circlepacking-basic/matplotlib/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/circlepacking-basic/matplotlib/plot_thumb.png
12+
preview_html: null
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent hierarchical visualization with proper nesting and containment
17+
- Smart use of square figure format (12x12) appropriate for circular visualization
18+
- 'Colorblind-safe palette with good visual hierarchy (root: muted blue, departments:
19+
bright yellow, teams: light blue)'
20+
- Proper area-based sizing using sqrt scaling for accurate visual perception
21+
- Clean implementation of painter's algorithm (drawing larger circles first)
22+
- Realistic organizational data that makes the visualization immediately understandable
23+
- Well-positioned legend that doesn't interfere with the visualization
24+
weaknesses:
25+
- Team-level circles lack labels (only circles with radius > 30 get labels), making
26+
it hard to identify specific teams
27+
- Could benefit from showing team names at least for the larger team circles (e.g.,
28+
Backend at 35 employees)

0 commit comments

Comments
 (0)