|
1 | | -""" pyplots.ai |
| 1 | +"""pyplots.ai |
2 | 2 | bubble-packed: Basic Packed Bubble Chart |
3 | | -Library: bokeh 3.8.1 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: bokeh 3.8.2 | Python 3.14.3 |
| 4 | +Quality: /100 | Updated: 2026-02-23 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import numpy as np |
8 | | -from bokeh.io import export_png, output_file, save |
9 | | -from bokeh.models import ColumnDataSource, LabelSet |
| 8 | +from bokeh.io import export_png |
| 9 | +from bokeh.models import ColumnDataSource, HoverTool, LabelSet |
10 | 10 | from bokeh.plotting import figure |
11 | 11 |
|
12 | 12 |
|
|
32 | 32 | values = [45, 32, 38, 25, 12, 18, 42, 8, 22, 15, 28, 14, 10, 20, 6] |
33 | 33 |
|
34 | 34 | # Calculate radii from values (scale by area for accurate perception) |
35 | | -max_radius = 400 |
| 35 | +max_radius = 380 |
36 | 36 | radii = np.sqrt(values) / np.sqrt(max(values)) * max_radius |
37 | 37 |
|
38 | 38 | # Circle packing simulation - position circles without overlap |
39 | 39 | n = len(radii) |
40 | | -center_x, center_y = 2400, 1350 |
| 40 | +center_x, center_y = 2400, 1400 |
41 | 41 |
|
42 | 42 | # Start with random positions near center |
43 | 43 | x_pos = center_x + (np.random.rand(n) - 0.5) * 1000 |
44 | 44 | y_pos = center_y + (np.random.rand(n) - 0.5) * 600 |
45 | 45 |
|
46 | 46 | # Force-directed packing iterations |
47 | | -for _ in range(500): |
48 | | - # Pull toward center |
| 47 | +padding = 10 |
| 48 | +margin = 100 |
| 49 | +for _ in range(800): |
49 | 50 | for i in range(n): |
50 | 51 | dx = center_x - x_pos[i] |
51 | 52 | dy = center_y - y_pos[i] |
52 | 53 | x_pos[i] += dx * 0.01 |
53 | 54 | y_pos[i] += dy * 0.01 |
54 | 55 |
|
55 | | - # Push apart overlapping circles |
56 | 56 | for i in range(n): |
57 | 57 | for j in range(i + 1, n): |
58 | 58 | dx = x_pos[j] - x_pos[i] |
59 | 59 | dy = y_pos[j] - y_pos[i] |
60 | 60 | dist = np.sqrt(dx**2 + dy**2) + 0.01 |
61 | | - min_dist = radii[i] + radii[j] + 10 # 10px padding |
| 61 | + min_dist = radii[i] + radii[j] + padding |
62 | 62 |
|
63 | 63 | if dist < min_dist: |
64 | 64 | overlap = (min_dist - dist) / 2 |
|
67 | 67 | x_pos[j] += dx / dist * overlap |
68 | 68 | y_pos[j] += dy / dist * overlap |
69 | 69 |
|
70 | | -# Create color palette - using Python Blue and Yellow with variations |
71 | | -colors = [ |
72 | | - "#306998", |
73 | | - "#FFD43B", |
74 | | - "#4B8BBE", |
75 | | - "#FFE873", |
76 | | - "#3776AB", |
77 | | - "#FFD43B", |
78 | | - "#306998", |
79 | | - "#4B8BBE", |
80 | | - "#FFE873", |
81 | | - "#3776AB", |
| 70 | + # Keep circles inside canvas bounds |
| 71 | + for i in range(n): |
| 72 | + x_pos[i] = np.clip(x_pos[i], radii[i] + margin, 4800 - radii[i] - margin) |
| 73 | + y_pos[i] = np.clip(y_pos[i], radii[i] + margin, 2700 - radii[i] - margin) |
| 74 | + |
| 75 | +# Re-center the packed group within the canvas |
| 76 | +x_min = min(x_pos[i] - radii[i] for i in range(n)) |
| 77 | +x_max = max(x_pos[i] + radii[i] for i in range(n)) |
| 78 | +y_min = min(y_pos[i] - radii[i] for i in range(n)) |
| 79 | +y_max = max(y_pos[i] + radii[i] for i in range(n)) |
| 80 | +x_shift = (4800 - (x_min + x_max)) / 2 |
| 81 | +y_shift = (2700 - (y_min + y_max)) / 2 |
| 82 | +x_pos += x_shift |
| 83 | +y_pos += y_shift |
| 84 | + |
| 85 | +# Color palette - cohesive blues/teals, all dark enough for white text |
| 86 | +palette = [ |
82 | 87 | "#306998", |
83 | | - "#FFD43B", |
84 | | - "#4B8BBE", |
85 | | - "#FFE873", |
86 | | - "#3776AB", |
| 88 | + "#2A5F8F", |
| 89 | + "#1B4F72", |
| 90 | + "#1A5276", |
| 91 | + "#2E86C1", |
| 92 | + "#21618C", |
| 93 | + "#2874A6", |
| 94 | + "#1F618D", |
| 95 | + "#2980B9", |
| 96 | + "#1B6B93", |
| 97 | + "#1C6EA4", |
| 98 | + "#256D85", |
| 99 | + "#2471A3", |
| 100 | + "#1A5276", |
| 101 | + "#154360", |
87 | 102 | ] |
| 103 | +# Sort indices by value descending so largest bubbles get most distinct colors |
| 104 | +sorted_idx = np.argsort(values)[::-1] |
| 105 | +colors = [""] * n |
| 106 | +for rank, idx in enumerate(sorted_idx): |
| 107 | + colors[idx] = palette[rank] |
88 | 108 |
|
89 | 109 | # Prepare data source |
90 | 110 | source = ColumnDataSource( |
91 | | - data={"x": x_pos, "y": y_pos, "radius": radii, "category": categories, "value": values, "color": colors} |
| 111 | + data={ |
| 112 | + "x": x_pos, |
| 113 | + "y": y_pos, |
| 114 | + "radius": radii, |
| 115 | + "category": categories, |
| 116 | + "value": values, |
| 117 | + "color": colors, |
| 118 | + "budget_text": [f"${v}M" for v in values], |
| 119 | + } |
92 | 120 | ) |
93 | 121 |
|
94 | 122 | # Create figure |
|
98 | 126 | title="Department Budgets · bubble-packed · bokeh · pyplots.ai", |
99 | 127 | x_range=(0, 4800), |
100 | 128 | y_range=(0, 2700), |
101 | | - tools="hover", |
102 | | - tooltips=[("Department", "@category"), ("Budget", "$@value M")], |
| 129 | + tools="", |
| 130 | + toolbar_location=None, |
103 | 131 | ) |
104 | 132 |
|
| 133 | +# Add hover tool with formatted tooltips |
| 134 | +hover = HoverTool(tooltips=[("Department", "@category"), ("Budget", "@budget_text")]) |
| 135 | +p.add_tools(hover) |
| 136 | + |
105 | 137 | # Draw circles |
106 | 138 | p.circle( |
107 | | - x="x", y="y", radius="radius", source=source, fill_color="color", fill_alpha=0.85, line_color="white", line_width=3 |
| 139 | + x="x", y="y", radius="radius", source=source, fill_color="color", fill_alpha=0.88, line_color="white", line_width=3 |
108 | 140 | ) |
109 | 141 |
|
110 | 142 | # Add labels to circles (only for larger circles) |
111 | | -large_indices = [i for i in range(len(values)) if radii[i] > 120] |
| 143 | +large_indices = [i for i in range(n) if radii[i] > 200] |
112 | 144 | label_source = ColumnDataSource( |
113 | 145 | data={ |
114 | 146 | "x": [x_pos[i] for i in large_indices], |
|
140 | 172 | text_align="center", |
141 | 173 | text_baseline="middle", |
142 | 174 | text_font_size="20pt", |
143 | | - text_color="white", |
| 175 | + text_color="rgba(255, 255, 255, 0.85)", |
144 | 176 | y_offset=-20, |
145 | 177 | ) |
146 | 178 | p.add_layout(value_labels) |
147 | 179 |
|
148 | | -# Style the plot |
| 180 | +# Style |
149 | 181 | p.title.text_font_size = "36pt" |
150 | 182 | p.title.align = "center" |
151 | | - |
152 | | -# Hide axes - packed bubble charts don't use positional axes |
153 | 183 | p.xaxis.visible = False |
154 | 184 | p.yaxis.visible = False |
155 | 185 | p.xgrid.visible = False |
156 | 186 | p.ygrid.visible = False |
157 | | - |
158 | | -# Clean background |
159 | 187 | p.background_fill_color = "#f8f9fa" |
160 | 188 | p.border_fill_color = "#f8f9fa" |
161 | 189 | p.outline_line_color = None |
162 | 190 |
|
163 | | -# Save as PNG and HTML |
| 191 | +# Save |
164 | 192 | export_png(p, filename="plot.png") |
165 | | -output_file("plot.html", title="Packed Bubble Chart") |
166 | | -save(p) |
|
0 commit comments