8000 Feature Request: Non overlapping Bubble Plots · Issue #18082 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content
Feature Request: Non overlapping Bubble Plots #18082
Closed
@McToel

Description

@McToel

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()

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0