Closed
Description
I really like non overlapping bubble chart to display quantities of types (Similar to this.) The only solution I found was this post, but it was way too slow for my task.
I wondered whether a feature like this would fit into the matplotlib api.
As I needed it for myself, I put together a class to calculate these plots or better said the position of the bubbles for me. If someone wants to play around with it, here is the code:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
np.random.seed(0)
r = np.random.randint(5,100, size=300)
class BubbleChart:
def __init__(self, r, bubble_distance=0):
self.bubble_distance = bubble_distance
np.random.shuffle(r)
self.n = len(r)
self.bubbles = np.ones((len(self), 3))
self.bubbles[:, 2] = r
self.maxstep = 2 * self.bubbles[:,2].max() + self.bubble_distance
self.step_dist = self.maxstep / 2
self.minstep = self.bubbles[:,2].min() / 2
#calculate initial grid layout for bubbles
length = np.ceil(np.sqrt(len(self)))
grid = np.arange(0, length * self.maxstep, self.maxstep)
gx,gy = np.meshgrid(grid, grid)
self.bubbles[:,0] = gx.flatten()[:len(self)]
self.bubbles[:,1] = gy.flatten()[:len(self)]
self.com = self.center_of_mass()
def __len__(self):
return self.n
def center_of_mass(self):
return np.average(self.bubbles[:,:2], axis=0, weights=self.bubbles[:,2]) # np.power(self.bubbles[:,2], 2) * np.pi
def center_distance(self, bubble, bubbles):
return np.sqrt(np.power(bubble[0] - bubbles[:, 0], 2)
+ np.power(bubble[1] - bubbles[:, 1], 2))
def outline_distance(self, bubble, bubbles):
center_distance = self.center_distance(bubble, bubbles)
return center_distance - bubble[2] - bubbles[:, 2] - self.bubble_distance
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):
moves = 0
for i in range(len(self)):
rest_bub = np.delete(self.bubbles, i, 0)
# try to move directly towoards the center of mass
dir_vec = self.com - self.bubbles[i, :2]
# shorten dir_vec 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])
# check whether new bubble collides with oder 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 dir vec
dir_vec = rest_bub[colliding, :2] - self.bubbles[i, :2]
dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec))
# calculate orthagonal vec
orth = np.array([dir_vec[1], -dir_vec[0]])
# test wich dir 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])
if not self.check_collisions(new_bubble, rest_bub):
self.bubbles[i, :] = new_bubble
self.com = self.center_of_mass()
if moves / len(self) < 0.1:
self.step_dist = self.step_dist / 2 # max(self.minstep, self.step_dist / 2)
print(self.step_dist)
def plot(self, ax):
for i in range(len(self)):
circ = plt.Circle(self.bubbles[i,:2], self.bubbles[i,2])
ax.add_patch(circ)
# plot center of mass
# circ = plt.Circle(self.com[:], self.maxstep / 10, color='red')
# ax.add_patch(circ)
bubble_plot = BubbleChart(r)
fig, ax2 = plt.subplots(subplot_kw=dict(aspect="equal"))
ax2.axis("off")
for i in range(100):
bubble_plot.collapse()
bubble_plot.plot(ax2)
ax2.relim()
ax2.autoscale_view()
plt.show()