8000 Added example on how to make packed bubble charts (#18223) · matplotlib/matplotlib@efa8bb0 · GitHub
[go: up one dir, main page]

Skip to content

Commit efa8bb0

Browse files
McToeltimhoffmjklymak
authored
Added example on how to make packed bubble charts (#18223)
* Added example on how to make packed bubble charts * fixed style errors * Apply suggestions from code review Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Jody Klymak <jklymak@gmail.com> * applied rewiews * fixed typo * readded title * style fix * Apply suggestions from code review * removed blank line * nicer array casting * Apply suggestions from code review * fixed variable names in docstring Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Co-authored-by: Jody Klymak <jklymak@gmail.com>
1 parent 591b857 commit efa8bb0

File tree

1 file changed

+171
-0
lines changed

1 file changed

+171
-0
lines changed

examples/misc/packed_bubbles.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"""
2+
===================
3+
Packed-bubble chart
4+
===================
5+
6+
Create a packed-bubble chart to represent scalar data.
7+
The presented algorithm tries to move all bubbles as close to the center of
8+
mass as possible while avoiding some collisions by moving around colliding
9+
objects. In this example we plot the market share of different desktop
10+
browsers.
11+
(source: https://gs.statcounter.com/browser-market-share/desktop/worldwidev)
12+
"""
13+
14+
import numpy as np
15+
import matplotlib.pyplot as plt
16+
17+
browser_market_share = {
18+
'browsers': ['firefox', 'chrome', 'safari', 'edge', 'ie', 'opera'],
19+
'market_share': [8.61, 69.55, 8.36, 4.12, 2.76, 2.43],
20+
'color': ['#5A69AF', '#579E65', '#F9C784', '#FC944A', '#F24C00', '#00B825']
21+
}
22+
23+
24+
class BubbleChart:
25+
def __init__(self, area, bubble_spacing=0):
26+
"""
27+
Setup for bubble collapse.
28+
29+
Parameters
30+
----------
31+
area : array-like
32+
Area of the bubbles.
33+
bubble_spacing : float, default: 0
34+
Minimal spacing between bubbles after collapsing.
35+
36+
Notes
37+
-----
38+
If "area" is sorted, the results might look weird.
39+
"""
40+
area = np.asarray(area)
41+
r = np.sqrt(area / np.pi)
42+
43+
self.bubble_spacing = bubble_spacing
44+
self.bubbles = np.ones((len(area), 4))
45+
self.bubbles[:, 2] = r
46+
self.bubbles[:, 3] = area
47+
self.maxstep = 2 * self.bubbles[:, 2].max() + self.bubble_spacing
48+
self.step_dist = self.maxstep / 2
49+
50+
# calculate initial grid layout for bubbles
51+
length = np.ceil(np.sqrt(len(self.bubbles)))
52+
grid = np.arange(length) * self.maxstep
53+
gx, gy = np.meshgrid(grid, grid)
54+
self.bubbles[:, 0] = gx.flatten()[:len(self.bubbles)]
55+
self.bubbles[:, 1] = gy.flatten()[:len(self.bubbles)]
56+
57+
self.com = self.center_of_mass()
58+
59+
def center_of_mass(self):
60+
return np.average(
61+
self.bubbles[:, :2], axis=0, weights=self.bubbles[:, 3]
62+
)
63+
64+
def center_distance(self, bubble, bubbles):
65+
return np.hypot(bubble[0] - bubbles[:, 0],
66+
bubble[1] - bubbles[:, 1])
67+
68+
def outline_distance(self, bubble, bubbles):
69+
center_distance = self.center_distance(bubble, bubbles)
70+
return center_distance - bubble[2] - \
71+
bubbles[:, 2] - self.bubble_spacing
72+
73+
def check_collisions(self, bubble, bubbles):
74+
distance = self.outline_distance(bubble, bubbles)
75+
return len(distance[distance < 0])
76+
77+
def collides_with(self, bubble, bubbles):
78+
distance = self.outline_distance(bubble, bubbles)
79+
idx_min = np.argmin(distance)
80+
return idx_min if type(idx_min) == np.ndarray else [idx_min]
81+
82+
def collapse(self, n_iterations=50):
83+
"""
84+
Move bubbles to the center of mass.
85+
86+
Parameters
87+
----------
88+
n_iterations : int, default: 50
89+
Number of moves to perform.
90+
"""
91+
for _i in range(n_iterations):
92+
moves = 0
93+
for i in range(len(self.bubbles)):
94+
rest_bub = np.delete(self.bubbles, i, 0)
95+
# try to move directly towards the center of mass
96+
# direction vector from bubble to the center of mass
97+
dir_vec = self.com - self.bubbles[i, :2]
98+
99+
# shorten direction vector to have length of 1
100+
dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec))
101+
102+
# calculate new bubble position
103+
new_point = self.bubbles[i, :2] + dir_vec * self.step_dist
104+
new_bubble = np.append(new_point, self.bubbles[i, 2:4])
105+
106+
# check whether new bubble collides with other bubbles
107+
if not self.check_collisions(new_bubble, rest_bub):
108+
self.bubbles[i, :] = new_bubble
109+
self.com = self.center_of_mass()
110+
moves += 1
111+
else:
112+
# try to move around a bubble that you collide with
113+
# find colliding bubble
114+
for colliding in self.collides_with(new_bubble, rest_bub):
115+
# calculate direction vector
116+
dir_vec = rest_bub[colliding, :2] - self.bubbles[i, :2]
117+
dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec))
118+
# calculate orthagonal vector
119+
orth = np.array([dir_vec[1], -dir_vec[0]])
120+
# test which direction to go
121+
new_point1 = (self.bubbles[i, :2] + orth *
122+
self.step_dist)
123+
new_point2 = (self.bubbles[i, :2] - orth *
124+
self.step_dist)
125+
dist1 = self.center_distance(
126+
self.com, np.array([new_point1]))
127+
dist2 = self.center_distance(
128+
self.com, np.array([new_point2]))
129+
new_point = new_point1 if dist1 < dist2 else new_point2
130+
new_bubble = np.append(new_point, self.bubbles[i, 2:4])
131+
if not self.check_collisions(new_bubble, rest_bub):
132+
self.bubbles[i, :] = new_bubble
133+
self.com = self.center_of_mass()
134+
135+
if moves / len(self.bubbles) < 0.1:
136+
self.step_dist = self.step_dist / 2
137+
138+
def plot(self, ax, labels, colors):
139+
"""
140+
Draw the bubble plot.
141+
142+
Parameters
143+
----------
144+
ax : matplotlib.axes.Axes
145+
labels : list
146+
Labels of the bubbles.
147+
colors : list
148+
Colors of the bubbles.
149+
"""
150+
for i in range(len(self.bubbles)):
151+
circ = plt.Circle(
152+
self.bubbles[i, :2], self.bubbles[i, 2], color=colors[i])
153+
ax.add_patch(circ)
154+
ax.text(*self.bubbles[i, :2], labels[i],
155+
horizontalalignment='center', verticalalignment='center')
156+
157+
158+
bubble_chart = BubbleChart(area=browser_market_share['market_share'],
159+
bubble_spacing=0.1)
160+
161+
bubble_chart.collapse()
162+
163+
fig, ax = plt.subplots(subplot_kw=dict(aspect="equal"))
164+
bubble_chart.plot(
165+
ax, browser_market_share['browsers'], browser_market_share['color'])
166+
ax.axis("off")
167+
ax.relim()
168+
ax.autoscale_view()
169+
ax.set_title('Browser market share')
170+
171+
plt.show()

0 commit comments

Comments
 (0)
0