|
| 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) |
0 commit comments