Skip to content

Commit f260e7a

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

2 files changed

Lines changed: 217 additions & 0 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
""" pyplots.ai
2+
circlepacking-basic: Circle Packing Chart
3+
Library: plotnine 0.15.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-30
5+
"""
6+
7+
import numpy as np
8+
import pandas as pd
9+
from plotnine import (
10+
aes,
11+
coord_fixed,
12+
element_text,
13+
geom_polygon,
14+
geom_text,
15+
ggplot,
16+
guide_legend,
17+
guides,
18+
labs,
19+
scale_fill_manual,
20+
scale_size_identity,
21+
theme,
22+
theme_void,
23+
)
24+
25+
26+
np.random.seed(42)
27+
28+
# Hierarchical data - Company organizational structure
29+
# Format: (id, label, parent_id, value)
30+
nodes = [
31+
("root", "Company", None, None),
32+
("eng", "Engineering", "root", 50),
33+
("ops", "Operations", "root", 35),
34+
("prod", "Product", "root", 30),
35+
("be", "Backend", "eng", 20),
36+
("fe", "Frontend", "eng", 18),
37+
("dops", "DevOps", "eng", 12),
38+
("fin", "Finance", "ops", 15),
39+
("leg", "Legal", "ops", 10),
40+
("hr", "HR", "ops", 10),
41+
("des", "Design", "prod", 12),
42+
("pm", "PM", "prod", 10),
43+
("res", "Research", "prod", 8),
44+
]
45+
46+
# Circle positions and radii - manually laid out for proper size differentiation
47+
# Root circle
48+
root_x, root_y, root_r = 0.0, 0.0, 1.0
49+
50+
# Calculate department radii based on their total values (area encoding: r ∝ sqrt(value))
51+
dept_values = {"eng": 50, "ops": 35, "prod": 30}
52+
dept_total = sum(dept_values.values())
53+
dept_scale = 0.48 # Larger scale factor for department circles to fill space better
54+
55+
eng_r = np.sqrt(dept_values["eng"] / dept_total) * dept_scale
56+
ops_r = np.sqrt(dept_values["ops"] / dept_total) * dept_scale
57+
prod_r = np.sqrt(dept_values["prod"] / dept_total) * dept_scale
58+
59+
# Position departments more centrally - place them in a tighter triangular arrangement
60+
eng_x, eng_y = 0.0, 0.30 # Engineering at top center
61+
ops_x, ops_y = -0.28, -0.12 # Operations bottom left, moved up
62+
prod_x, prod_y = 0.28, -0.12 # Product bottom right, moved up
63+
64+
# Calculate team radii with more variation to show size differences clearly
65+
# Teams within Engineering (values: 20, 18, 12) - wider value range
66+
eng_team_values = {"be": 20, "fe": 18, "dops": 12}
67+
eng_team_total = sum(eng_team_values.values())
68+
team_scale_eng = eng_r * 0.75
69+
70+
be_r = np.sqrt(eng_team_values["be"] / eng_team_total) * team_scale_eng
71+
fe_r = np.sqrt(eng_team_values["fe"] / eng_team_total) * team_scale_eng
72+
dops_r = np.sqrt(eng_team_values["dops"] / eng_team_total) * team_scale_eng
73+
74+
# Teams within Operations (values: 15, 10, 10)
75+
ops_team_values = {"fin": 15, "leg": 10, "hr": 10}
76+
ops_team_total = sum(ops_team_values.values())
77+
team_scale_ops = ops_r * 0.75
78+
79+
fin_r = np.sqrt(ops_team_values["fin"] / ops_team_total) * team_scale_ops
80+
leg_r = np.sqrt(ops_team_values["leg"] / ops_team_total) * team_scale_ops
81+
hr_r = np.sqrt(ops_team_values["hr"] / ops_team_total) * team_scale_ops
82+
83+
# Teams within Product (values: 12, 10, 8)
84+
prod_team_values = {"des": 12, "pm": 10, "res": 8}
85+
prod_team_total = sum(prod_team_values.values())
86+
team_scale_prod = prod_r * 0.75
87+
88+
des_r = np.sqrt(prod_team_values["des"] / prod_team_total) * team_scale_prod
89+
pm_r = np.sqrt(prod_team_values["pm"] / prod_team_total) * team_scale_prod
90+
res_r = np.sqrt(prod_team_values["res"] / prod_team_total) * team_scale_prod
91+
92+
# Position teams within their parent departments - tighter arrangement
93+
# Engineering teams - arranged in a triangle
94+
be_x, be_y = eng_x - 0.12, eng_y + 0.0
95+
fe_x, fe_y = eng_x + 0.12, eng_y + 0.0
96+
dops_x, dops_y = eng_x + 0.0, eng_y - 0.14
97+
98+
# Operations teams - arranged in a triangle
99+
fin_x, fin_y = ops_x + 0.0, ops_y + 0.10
100+
leg_x, leg_y = ops_x - 0.10, ops_y - 0.05
101+
hr_x, hr_y = ops_x + 0.10, ops_y - 0.05
102+
103+
# Product teams - arranged in a triangle
104+
des_x, des_y = prod_x + 0.0, prod_y + 0.10
105+
pm_x, pm_y = prod_x - 0.10, prod_y - 0.05
106+
res_x, res_y = prod_x + 0.10, prod_y - 0.05
107+
108+
# All circles data: (id, label, cx, cy, r, depth)
109+
circles_data = [
110+
("root", "Company", root_x, root_y, root_r, 0),
111+
("eng", "Engineering", eng_x, eng_y, eng_r, 1),
112+
("ops", "Operations", ops_x, ops_y, ops_r, 1),
113+
("prod", "Product", prod_x, prod_y, prod_r, 1),
114+
("be", "Backend", be_x, be_y, be_r, 2),
115+
("fe", "Frontend", fe_x, fe_y, fe_r, 2),
116+
("dops", "DevOps", dops_x, dops_y, dops_r, 2),
117+
("fin", "Finance", fin_x, fin_y, fin_r, 2),
118+
("leg", "Legal", leg_x, leg_y, leg_r, 2),
119+
("hr", "HR", hr_x, hr_y, hr_r, 2),
120+
("des", "Design", des_x, des_y, des_r, 2),
121+
("pm", "PM", pm_x, pm_y, pm_r, 2),
122+
("res", "Research", res_x, res_y, res_r, 2),
123+
]
124+
125+
# Sort by depth for proper layering (draw root first, teams last/on top)
126+
circles_data = sorted(circles_data, key=lambda c: c[5])
127+
128+
# Build polygon dataframe for drawing circles
129+
polygon_rows = []
130+
n_points = 64
131+
132+
for circle_id, _label, cx, cy, r, depth in circles_data:
133+
angles = np.linspace(0, 2 * np.pi, n_points)
134+
xs = cx + r * np.cos(angles)
135+
ys = cy + r * np.sin(angles)
136+
for j, (x, y) in enumerate(zip(xs, ys, strict=True)):
137+
polygon_rows.append({"circle_id": circle_id, "x": x, "y": y, "order": j, "depth": depth})
138+
139+
df_circles = pd.DataFrame(polygon_rows)
140+
141+
# Build labels dataframe - include ALL labels (departments AND teams)
142+
label_rows = []
143+
for _circle_id, label, cx, cy, r, depth in circles_data:
144+
if depth == 0:
145+
continue # Skip root label
146+
if depth == 1:
147+
# Department labels: position at top edge of circle
148+
label_y = cy + r * 0.60
149+
text_size = 12
150+
else:
151+
# Team labels: centered in circle, larger size for visibility
152+
label_y = cy
153+
text_size = 10
154+
label_rows.append({"x": cx, "y": label_y, "label": label, "text_size": text_size, "depth": depth})
155+
156+
df_labels = pd.DataFrame(label_rows)
157+
158+
# Create the plot
159+
plot = (
160+
ggplot()
161+
+ geom_polygon(
162+
df_circles, aes(x="x", y="y", group="circle_id", fill="factor(depth)"), color="#333333", size=0.5, alpha=0.92
163+
)
164+
+ geom_text(
165+
df_labels,
166+
aes(x="x", y="y", label="label", size="text_size"),
167+
color="#222222",
168+
fontweight="bold",
169+
show_legend=False,
170+
)
171+
+ scale_fill_manual(
172+
values=["#E0E0E0", "#306998", "#FFD43B"], labels=["Root", "Departments", "Teams"], name="Hierarchy Level"
173+
)
174+
+ scale_size_identity()
175+
+ coord_fixed(ratio=1)
176+
+ labs(title="circlepacking-basic · plotnine · pyplots.ai")
177+
+ theme_void()
178+
+ theme(
179+
figure_size=(12, 12),
180+
plot_title=element_text(size=28, ha="center", weight="bold", margin={"b": 20}),
181+
legend_text=element_text(size=14),
182+
legend_title=element_text(size=16, weight="bold"),
183+
legend_position="bottom",
184+
legend_direction="horizontal",
185+
)
186+
+ guides(fill=guide_legend(override_aes={"size": 0.5}))
187+
)
188+
189+
plot.save("plot.png", dpi=300, width=12, height=12, verbose=False)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
library: plotnine
2+
specification_id: circlepacking-basic
3+
created: '2025-12-30T00:08:30Z'
4+
updated: '2025-12-30T00:55:11Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20585401342
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 0.15.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/circlepacking-basic/plotnine/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/circlepacking-basic/plotnine/plot_thumb.png
12+
preview_html: null
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent visual hierarchy with clear color differentiation between levels (gray
17+
root, blue departments, yellow teams)
18+
- Creative implementation using geom_polygon to draw circles since plotnine lacks
19+
native circle packing
20+
- Proper area encoding (r ∝ sqrt(value)) for accurate visual perception of sizes
21+
- Clean, readable code with well-documented manual layout calculations
22+
- Realistic company organizational data that effectively demonstrates the visualization
23+
concept
24+
- All labels readable with appropriate sizing and bold text for emphasis
25+
weaknesses:
26+
- Manual circle positioning rather than algorithmic packing means circles do not
27+
pack as tightly as possible
28+
- Slight empty space at bottom of the root circle could be better utilized

0 commit comments

Comments
 (0)