Skip to content

Commit c7e95ff

Browse files
feat(bokeh): implement bubble-packed (#1076)
## Implementation: `bubble-packed` - bokeh Implements the **bokeh** version of `bubble-packed`. **File:** `plots/bubble-packed/implementations/bokeh.py` **Parent Issue:** #992 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20279594515)* --------- 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 70f8cf7 commit c7e95ff

2 files changed

Lines changed: 205 additions & 0 deletions

File tree

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""
2+
bubble-packed: Basic Packed Bubble Chart
3+
Library: bokeh
4+
"""
5+
6+
import numpy as np
7+
from bokeh.io import export_png, output_file, save
8+
from bokeh.models import ColumnDataSource, LabelSet
9+
from bokeh.plotting import figure
10+
11+
12+
# Data - department budgets (in millions)
13+
np.random.seed(42)
14+
categories = [
15+
"Engineering",
16+
"Marketing",
17+
"Sales",
18+
"Operations",
19+
"HR",
20+
"Finance",
21+
"R&D",
22+
"Legal",
23+
"IT",
24+
"Customer Support",
25+
"Product",
26+
"Design",
27+
"QA",
28+
"Data Science",
29+
"Security",
30+
]
31+
values = [45, 32, 38, 25, 12, 18, 42, 8, 22, 15, 28, 14, 10, 20, 6]
32+
33+
# Calculate radii from values (scale by area for accurate perception)
34+
# r = sqrt(value) * scaling factor
35+
max_radius = 400 # max radius in pixels for display
36+
radii = np.sqrt(values) / np.sqrt(max(values)) * max_radius
37+
38+
39+
# Circle packing simulation - position circles without overlap
40+
def pack_circles(radii, center=(2400, 1350), iterations=500):
41+
"""Simple force-directed packing algorithm."""
42+
n = len(radii)
43+
# Start with random positions near center
44+
np.random.seed(42)
45+
x = center[0] + (np.random.rand(n) - 0.5) * 1000
46+
y = center[1] + (np.random.rand(n) - 0.5) * 600
47+
48+
for _ in range(iterations):
49+
# Pull toward center
50+
for i in range(n):
51+
dx = center[0] - x[i]
52+
dy = center[1] - y[i]
53+
dist = np.sqrt(dx**2 + dy**2) + 0.01
54+
x[i] += dx * 0.01
55+
y[i] += dy * 0.01
56+
57+
# Push apart overlapping circles
58+
for i in range(n):
59+
for j in range(i + 1, n):
60+
dx = x[j] - x[i]
61+
dy = y[j] - y[i]
62+
dist = np.sqrt(dx**2 + dy**2) + 0.01
63+
min_dist = radii[i] + radii[j] + 10 # 10px padding
64+
65+
if dist < min_dist:
66+
overlap = (min_dist - dist) / 2
67+
x[i] -= dx / dist * overlap
68+
y[i] -= dy / dist * overlap
69+
x[j] += dx / dist * overlap
70+
y[j] += dy / dist * overlap
71+
72+
return x, y
73+
74+
75+
# Pack circles
76+
x_pos, y_pos = pack_circles(radii)
77+
78+
# Create color palette - using Python Blue as base with variations
79+
colors = [
80+
"#306998",
81+
"#FFD43B",
82+
"#4B8BBE",
83+
"#FFE873",
84+
"#3776AB",
85+
"#FFD43B",
86+
"#306998",
87+
"#4B8BBE",
88+
"#FFE873",
89+
"#3776AB",
90+
"#306998",
91+
"#FFD43B",
92+
"#4B8BBE",
93+
"#FFE873",
94+
"#3776AB",
95+
]
96+
97+
# Prepare data source
98+
source = ColumnDataSource(
99+
data={
100+
"x": x_pos,
101+
"y": y_pos,
102+
"radius": radii,
103+
"category": categories,
104+
"value": values,
105+
"color": colors,
106+
"label": [f"{c}\n${v}M" for c, v in zip(categories, values, strict=True)],
107+
}
108+
)
109+
110+
# Create figure
111+
p = figure(
112+
width=4800,
113+
height=2700,
114+
title="Department Budgets · bubble-packed · bokeh · pyplots.ai",
115+
x_range=(0, 4800),
116+
y_range=(0, 2700),
117+
tools="hover",
118+
tooltips=[("Department", "@category"), ("Budget", "$@value M")],
119+
)
120+
121+
# Draw circles
122+
p.circle(
123+
x="x", y="y", radius="radius", source=source, fill_color="color", fill_alpha=0.85, line_color="white", line_width=3
124+
)
125+
126+
# Add labels to circles (only for larger circles)
127+
# Filter labels for bubbles large enough to show text
128+
label_source = ColumnDataSource(
129+
data={
130+
"x": [x_pos[i] for i in range(len(values)) if radii[i] > 120],
131+
"y": [y_pos[i] for i in range(len(values)) if radii[i] > 120],
132+
"text": [categories[i] for i in range(len(values)) if radii[i] > 120],
133+
"value_text": [f"${values[i]}M" for i in range(len(values)) if radii[i] > 120],
134+
}
135+
)
136+
137+
labels = LabelSet(
138+
x="x",
139+
y="y",
140+
text="text",
141+
source=label_source,
142+
text_align="center",
143+
text_baseline="middle",
144+
text_font_size="24pt",
145+
text_color="white",
146+
text_font_style="bold",
147+
y_offset=15,
148+
)
149+
p.add_layout(labels)
150+
151+
value_labels = LabelSet(
152+
x="x",
153+
y="y",
154+
text="value_text",
155+
source=label_source,
156+
text_align="center",
157+
text_baseline="middle",
158+
text_font_size="20pt",
159+
text_color="white",
160+
y_offset=-20,
161+
)
162+
p.add_layout(value_labels)
163+
164+
# Style the plot
165+
p.title.text_font_size = "36pt"
166+
p.title.align = "center"
167+
168+
# Hide axes - packed bubble charts don't use positional axes
169+
p.xaxis.visible = False
170+
p.yaxis.visible = False
171+
p.xgrid.visible = False
172+
p.ygrid.visible = False
173+
174+
# Clean background
175+
p.background_fill_color = "#f8f9fa"
176+
p.border_fill_color = "#f8f9fa"
177+
p.outline_line_color = None
178+
179+
# Save as PNG and HTML
180+
export_png(p, filename="plot.png")
181+
output_file("plot.html", title="Packed Bubble Chart")
182+
save(p)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Per-library metadata for bokeh implementation of bubble-packed
2+
# Auto-generated by impl-generate.yml
3+
4+
library: bokeh
5+
specification_id: bubble-packed
6+
7+
# Preview URLs (filled by workflow)
8+
preview_url: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/bokeh/plot.png
9+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/bokeh/plot_thumb.png
10+
preview_html: https://storage.googleapis.com/pyplots-images/plots/bubble-packed/bokeh/plot.html
11+
12+
current:
13+
version: 0
14+
generated_at: 2025-12-16T19:12:52Z
15+
generated_by: claude-opus-4-5-20251101
16+
workflow_run: 20279594515
17+
issue: 992
18+
quality_score: 94
19+
# Version info (filled by workflow)
20+
python_version: "3.13.11"
21+
library_version: "unknown"
22+
23+
history: []

0 commit comments

Comments
 (0)