text: import numpy as np
  import matplotlib.pyplot as plt
  from math import ceil
  
  
  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 orthogonal 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, label, value, color):
          """
          Draw the bubble plot.
  
          Parameters
          ----------
          ax : matplotlib.axes.Axes
          label : list
              Label of the bubbles.
          color : list
              Color of the bubbles.
          """
          for i in range(len(self.bubbles)):
              circ = plt.Circle(
                  self.bubbles[i, :2], self.bubbles[i, 2], color=color[i])
              ax.add_patch(circ)
              if value is not None:
                  va = "center" if label is None else "top"
                  ax.text(*self.bubbles[i, :2], value[i],
                          horizontalalignment='center', verticalalignment=va)
              if label is not None:
                  va = "center" if value is None else "bottom"
                  ax.text(*self.bubbles[i, :2], label[i],
                          horizontalalignment='center', verticalalignment=va)
  
  def plot(
      area,
      bubble_spacing=0.1,
      color=None,
      label=None,
      value=None,
      ax=None
  ):
      if ax is None:
          ax = plt.gca()
          
      if color is None:
          color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']
          if len(area) > len(color_cycle):
              color_cycle = color_cycle * ceil(len(color_cycle)/len(area))
          color = color_cycle[:len(area)]
  
      bubble_chart = BubbleChart(area=area,
                              bubble_spacing=bubble_spacing)
  
      bubble_chart.collapse()
  
      bubble_chart.plot(
          ax, label, value, color)
      ax.axis("off")
      ax.relim()
      ax.autoscale_view()
  
      return ax