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