-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Added example on how to make packed bubble charts #18223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
0269e67
Added example on how to make packed bubble charts
McToel 0e0b7e1
fixed style errors
McToel 545b951
Apply suggestions from code review
McToel 919e27e
Apply suggestions from code review
McToel 7e1598d
applied rewiews
McToel a76a3b8
Merge branch 'master' of https://github.com/McToel/matplotlib into pa…
McToel 0cddb12
fixed typo
McToel eba9835
readded title
McToel 2c7ef1a
style fix
McToel 5265a49
Apply suggestions from code review
McToel b252677
removed blank line
McToel 008f981
nicer array casting
McToel c5c765e
Apply suggestions from code review
McToel fc6d671
fixed variable names in docstring
McToel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
""" | ||
=================== | ||
Packed-bubble chart | ||
=================== | ||
|
||
Create a packed-bubble chart to represent scalar data. | ||
The presented algorithm tries to move all bubbles as close to the center of | ||
mass as possible while avoiding some collisions by moving around colliding | ||
objects. In this example we plot the market share of different desktop | ||
browsers. | ||
(source: https://gs.statcounter.com/browser-market-share/desktop/worldwidev) | ||
""" | ||
|
||
import numpy as np | ||
import matplotlib.pyplot as plt | ||
|
||
browser_market_share = { | ||
'browsers': ['firefox', 'chrome', 'safari', 'edge', 'ie', 'opera'], | ||
'market_share': [8.61, 69.55, 8.36, 4.12, 2.76, 2.43], | ||
'color': ['#5A69AF', '#579E65', '#F9C784', '#FC944A', '#F24C00', '#00B825'] | ||
} | ||
|
||
|
||
class BubbleChart: | ||
def __init__(self, area, bubble_spacing=0): | ||
""" | ||
Setup for bubble collapse. | ||
|
||
Parameters | ||
---------- | ||
area : array-like | ||
Area of the bubbles. | ||
bubble_spacing : float, default: 0 | ||
Minimal spacing between bubbles after collapsing. | ||
|
||
Notes | ||
----- | ||
If "area" is sorted, the results might look weird. | ||
""" | ||
area = np.asarray(area) | ||
r = np.sqrt(area / np.pi) | ||
|
||
self.bubble_spacing = bubble_spacing | ||
self.bubbles = np.ones((len(area), 4)) | ||
self.bubbles[:, 2] = r | ||
self.bubbles[:, 3] = area | ||
self.maxstep = 2 * self.bubbles[:, 2].max() + self.bubble_spacing | ||
self.step_dist = self.maxstep / 2 | ||
|
||
# calculate initial grid layout for bubbles | ||
length = np.ceil(np.sqrt(len(self.bubbles))) | ||
grid = np.arange(length) * self.maxstep | ||
gx, gy = np.meshgrid(grid, grid) | ||
self.bubbles[:, 0] = gx.flatten()[:len(self.bubbles)] | ||
self.bubbles[:, 1] = gy.flatten()[:len(self.bubbles)] | ||
|
||
self.com = self.center_of_mass() | ||
|
||
def center_of_mass(self): | ||
return np.average( | ||
self.bubbles[:, :2], axis=0, weights=self.bubbles[:, 3] | ||
) | ||
|
||
def center_distance(self, bubble, bubbles): | ||
return np.hypot(bubble[0] - bubbles[:, 0], | ||
bubble[1] - bubbles[:, 1]) | ||
|
||
def outline_distance(self, bubble, bubbles): | ||
center_distance = self.center_distance(bubble, bubbles) | ||
return center_distance - bubble[2] - \ | ||
bubbles[:, 2] - self.bubble_spacing | ||
|
||
def check_collisions(self, bubble, bubbles): | ||
distance = self.outline_distance(bubble, bubbles) | ||
return len(distance[distance < 0]) | ||
|
||
def collides_with(self, bubble, bubbles): | ||
distance = self.outline_distance(bubble, bubbles) | ||
idx_min = np.argmin(distance) | ||
return idx_min if type(idx_min) == np.ndarray else [idx_min] | ||
|
||
def collapse(self, n_iterations=50): | ||
""" | ||
Move bubbles to the center of mass. | ||
|
||
Parameters | ||
---------- | ||
n_iterations : int, default: 50 | ||
Number of moves to perform. | ||
""" | ||
for _i in range(n_iterations): | ||
moves = 0 | ||
for i in range(len(self.bubbles)): | ||
rest_bub = np.delete(self.bubbles, i, 0) | ||
# try to move directly towards the center of mass | ||
# direction vector from bubble to the center of mass | ||
dir_vec = self.com - self.bubbles[i, :2] | ||
|
||
# shorten direction vector to have length of 1 | ||
dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec)) | ||
|
||
# calculate new bubble position | ||
new_point = self.bubbles[i, :2] + dir_vec * self.step_dist | ||
new_bubble = np.append(new_point, self.bubbles[i, 2:4]) | ||
|
||
# check whether new bubble collides with other bubbles | ||
if not self.check_collisions(new_bubble, rest_bub): | ||
self.bubbles[i, :] = new_bubble | ||
self.com = self.center_of_mass() | ||
moves += 1 | ||
else: | ||
# try to move around a bubble that you collide with | ||
# find colliding bubble | ||
for colliding in self.collides_with(new_bubble, rest_bub): | ||
# calculate direction vector | ||
dir_vec = rest_bub[colliding, :2] - self.bubbles[i, :2] | ||
dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec)) | ||
# calculate orthagonal vector | ||
orth = np.array([dir_vec[1], -dir_vec[0]]) | ||
# test which direction to go | ||
new_point1 = (self.bubbles[i, :2] + orth * | ||
self.step_dist) | ||
new_point2 = (self.bubbles[i, :2] - orth * | ||
self.step_dist) | ||
dist1 = self.center_distance( | ||
self.com, np.array([new_point1])) | ||
dist2 = self.center_distance( | ||
self.com, np.array([new_point2])) | ||
new_point = new_point1 if dist1 < dist2 else new_point2 | ||
new_bubble = np.append(new_point, self.bubbles[i, 2:4]) | ||
if not self.check_collisions(new_bubble, rest_bub): | ||
self.bubbles[i, :] = new_bubble | ||
self.com = self.center_of_mass() | ||
|
||
if moves / len(self.bubbles) < 0.1: | ||
self.step_dist = self.step_dist / 2 | ||
|
||
def plot(self, ax, labels, colors): | ||
""" | ||
Draw the bubble plot. | ||
|
||
Parameters | ||
---------- | ||
ax : matplotlib.axes.Axes | ||
labels : list | ||
Labels of the bubbles. | ||
colors : list | ||
Colors of the bubbles. | ||
""" | ||
for i in range(len(self.bubbles)): | ||
circ = plt.Circle( | ||
self.bubbles[i, :2], self.bubbles[i, 2], color=colors[i]) | ||
ax.add_patch(circ) | ||
ax.text(*self.bubbles[i, :2], labels[i], | ||
horizontalalignment='center', verticalalignment='center') | ||
|
||
|
||
bubble_chart = BubbleChart(area=browser_market_share['market_share'], | ||
bubble_spacing=0.1) | ||
|
||
bubble_chart.collapse() | ||
|
||
fig, ax = plt.subplots(subplot_kw=dict(aspect="equal")) | ||
bubble_chart.plot( | ||
ax, browser_market_share['browsers'], browser_market_share['color']) | ||
ax.axis("off") | ||
ax.relim() | ||
ax.autoscale_view() | ||
ax.set_title('Browser market share') | ||
|
||
plt.show() |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.