.. only:: html .. note:: :class: sphx-glr-download-link-note Click :ref:`here ` to download the full example code .. rst-class:: sphx-glr-example-title .. _sphx_glr_gallery_misc_packed_bubbles.py: =================== 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) .. image:: /gallery/misc/images/sphx_glr_packed_bubbles_001.png :alt: Browser market share :class: sphx-glr-single-img .. code-block:: default 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 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, 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() .. _sphx_glr_download_gallery_misc_packed_bubbles.py: .. only :: html .. container:: sphx-glr-footer :class: sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: packed_bubbles.py ` .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: packed_bubbles.ipynb ` .. only:: html .. rst-class:: sphx-glr-signature Keywords: matplotlib code example, codex, python plot, pyplot `Gallery generated by Sphinx-Gallery `_