|
1 | | -""" pyplots.ai |
| 1 | +"""pyplots.ai |
2 | 2 | bubble-packed: Basic Packed Bubble Chart |
3 | | -Library: letsplot 4.8.2 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: letsplot 4.8.2 | Python 3.14.3 |
| 4 | +Quality: /100 | Updated: 2026-02-23 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import numpy as np |
|
16 | 16 | ggplot, |
17 | 17 | ggsize, |
18 | 18 | labs, |
| 19 | + layer_tooltips, |
19 | 20 | scale_color_manual, |
20 | 21 | scale_size, |
21 | 22 | theme, |
|
28 | 29 |
|
29 | 30 | LetsPlot.setup_html() |
30 | 31 |
|
31 | | -# Data - department budget allocation |
| 32 | +# Data - department budget allocation ($M) |
32 | 33 | np.random.seed(42) |
33 | 34 | categories = [ |
34 | 35 | "Engineering", |
|
66 | 67 | "Tech", |
67 | 68 | ] |
68 | 69 |
|
69 | | -# Circle packing using force simulation (inline) |
| 70 | +# Circle packing using force simulation |
70 | 71 | n = len(values) |
71 | | -radii = np.sqrt(values / np.pi) * 3.5 # Scale by area for accurate visual perception |
| 72 | +radii = np.sqrt(values / np.pi) * 3.5 |
72 | 73 |
|
73 | 74 | # Initialize positions |
74 | 75 | np.random.seed(42) |
|
77 | 78 |
|
78 | 79 | # Force-directed packing simulation |
79 | 80 | for _ in range(600): |
80 | | - # Pull toward center |
81 | 81 | x *= 0.99 |
82 | 82 | y *= 0.99 |
83 | 83 |
|
84 | | - # Push overlapping circles apart |
85 | 84 | for i in range(n): |
86 | 85 | for j in range(i + 1, n): |
87 | 86 | dx = x[j] - x[i] |
|
98 | 97 | x[j] += move_x |
99 | 98 | y[j] += move_y |
100 | 99 |
|
101 | | -df = pd.DataFrame({"label": categories, "value": values, "group": groups, "x": x, "y": y}) |
| 100 | +df = pd.DataFrame( |
| 101 | + {"label": categories, "value": values, "group": groups, "x": x, "y": y, "budget": [f"${v}M" for v in values]} |
| 102 | +) |
102 | 103 |
|
103 | | -# Abbreviate long labels to fit inside bubbles |
104 | | -label_map = { |
105 | | - "Customer Support": "Support", |
106 | | - "Engineering": "Engineering", |
107 | | - "Marketing": "Marketing", |
108 | | - "Sales": "Sales", |
109 | | - "Operations": "Ops", |
110 | | - "Finance": "Finance", |
111 | | - "R&D": "R&D", |
112 | | - "HR": "HR", |
113 | | - "Legal": "Legal", |
114 | | - "IT": "IT", |
115 | | - "Product": "Product", |
116 | | - "Design": "Design", |
117 | | - "Analytics": "Analytics", |
118 | | - "QA": "QA", |
119 | | - "Security": "Security", |
120 | | -} |
121 | | -df["short_label"] = df["label"].map(label_map) |
| 104 | +# Show labels only on bubbles large enough to fit text |
| 105 | +df["display_label"] = df.apply(lambda row: row["label"] if row["value"] >= 35 else "", axis=1) |
| 106 | +# Abbreviate long labels |
| 107 | +abbrev = {"Customer Support": "Support", "Operations": "Ops"} |
| 108 | +df["display_label"] = df["display_label"].replace(abbrev) |
122 | 109 |
|
123 | 110 | # Plot |
124 | 111 | plot = ( |
125 | 112 | ggplot(df, aes(x="x", y="y")) |
126 | | - + geom_point(aes(size="value", color="group"), alpha=0.85) |
127 | | - + geom_text(aes(label="short_label"), size=7, color="white", fontface="bold") |
| 113 | + + geom_point( |
| 114 | + aes(size="value", color="group"), |
| 115 | + alpha=0.85, |
| 116 | + tooltips=layer_tooltips().title("@label").line("Budget|@budget").line("Division|@group"), |
| 117 | + ) |
| 118 | + + geom_text(aes(label="display_label"), size=7, color="white", fontface="bold") |
128 | 119 | + scale_size(range=[20, 85], guide="none") |
129 | 120 | + scale_color_manual(values=["#FFD43B", "#4ECDC4", "#306998"]) |
130 | 121 | + labs(title="Department Budget Allocation · bubble-packed · letsplot · pyplots.ai", color="Division") |
|
141 | 132 | + ggsize(1600, 900) |
142 | 133 | ) |
143 | 134 |
|
144 | | -# Save PNG (scale 3x to get 4800x2700 px) |
| 135 | +# Save |
145 | 136 | export_ggsave(plot, "plot.png", path=".", scale=3) |
146 | | - |
147 | | -# Save HTML for interactive version |
148 | | -export_ggsave(plot, "plot.html", path=".") |
0 commit comments