From 0269e67b029025a4a05f8fed52380e75e76fcbdd Mon Sep 17 00:00:00 2001 From: McToel Date: Tue, 11 Aug 2020 19:36:11 +0200 Subject: [PATCH 01/13] Added example on how to make packed bubble charts --- examples/misc/packed_bubbles.py | 149 ++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 examples/misc/packed_bubbles.py diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py new file mode 100644 index 000000000000..859ad9fd043f --- /dev/null +++ b/examples/misc/packed_bubbles.py @@ -0,0 +1,149 @@ +""" +Create a packed bubble / non overlapping bubble chart to represent scalar data. +In this example we plot the market share of different desktop browsers. +""" + +import numpy as np +import matplotlib.pyplot as plt + +browser_market_share = { + 'browsers': ['firefox', 'chrome', 'safari', 'edge', 'ie', 'opera'], + 'market_share': [783, 6881, 371, 704, 587, 124], + 'color': ['b', 'g', 'r', 'c', 'm', 'y'] +} + + +class BubbleChart: + def __init__(self, r=None, a=None, bubble_distance=0): + """setup for bubble collapse + + Args: + r (list, optional): radius of the bubbles. Defaults to None. + a (list, optional): area of the bubbles. Defaults to None. + Note: If r or a is sorted, the results might look weird + bubble_distance (int, optional): minimal distance the bubbles should have after collapsing. Defaults to 0. + """ + if r is None: + r = np.sqrt(a / np.pi) + if a is None: + a = np.power(r, 2) * 2 + + self.bubble_distance = bubble_distance + self.n = len(r) + self.bubbles = np.ones((len(self), 4)) + self.bubbles[:, 2] = r + self.bubbles[:, 3] = a + self.maxstep = 2 * self.bubbles[:, 2].max() + self.bubble_distance + self.step_dist = self.maxstep / 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[:, 3]) + + 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 from bubble to com + 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:4]) + + # 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 which 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: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) < 0.1: + self.step_dist = self.step_dist / 2 + print(self.step_dist) + + def plot(self, ax, labels, colors): + """draw the bubble plot + + Args: + ax (matplotlib.axes._subplots.AxesSubplot) + labels (list): labels of the bubbles + colors (list): colors of the bubbles + """ + for i in range(len(self)): + 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') + + +# set market share of the browsers as area of the bubbles +bubble_plot = BubbleChart(a=np.array( + browser_market_share['market_share']), bubble_distance=1) + +# collapse plot 50 times. In some cases it might be useful to do this more or less often +for i in range(50): + bubble_plot.collapse() + +fig, ax = plt.subplots(subplot_kw=dict(aspect="equal")) +bubble_plot.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() From 0e0b7e132ca9ac3edd29de46f13704fe1c68e490 Mon Sep 17 00:00:00 2001 From: McToel Date: Wed, 12 Aug 2020 11:41:51 +0200 Subject: [PATCH 02/13] fixed style errors --- examples/misc/packed_bubbles.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py index 859ad9fd043f..d79613afd4d4 100644 --- a/examples/misc/packed_bubbles.py +++ b/examples/misc/packed_bubbles.py @@ -1,6 +1,9 @@ """ Create a packed bubble / non overlapping bubble chart to represent scalar data. -In this example we plot the market share of different desktop browsers. +The presented algorithm tries to move all bubbles as close to the center of +mass as possible while avoiding some collisions by moving aroud colliding +objects. In this example we plot the market share of different desktop +browsers. """ import numpy as np @@ -15,13 +18,15 @@ class BubbleChart: def __init__(self, r=None, a=None, bubble_distance=0): - """setup for bubble collapse + """ + setup for bubble collapse Args: r (list, optional): radius of the bubbles. Defaults to None. a (list, optional): area of the bubbles. Defaults to None. Note: If r or a is sorted, the results might look weird - bubble_distance (int, optional): minimal distance the bubbles should have after collapsing. Defaults to 0. + bubble_distance (int, optional): minimal distance the bubbles + should have after collapsing. Defaults to 0. """ if r is None: r = np.sqrt(a / np.pi) @@ -49,15 +54,18 @@ def __len__(self): return self.n def center_of_mass(self): - return np.average(self.bubbles[:, :2], axis=0, weights=self.bubbles[:, 3]) + return np.average( + self.bubbles[:, :2], axis=0, weights=self.bubbles[:, 3] + ) def center_distance(self, bubble, bubbles): - return np.sqrt(np.power(bubble[0] - bubbles[:, 0], 2) - + np.power(bubble[1] - bubbles[:, 1], 2)) + 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 + return center_distance - bubble[2] - \ + bubbles[:, 2] - self.bubble_distance def check_collisions(self, bubble, bubbles): distance = self.outline_distance(bubble, bubbles) @@ -112,10 +120,10 @@ def collapse(self): if moves / len(self) < 0.1: self.step_dist = self.step_dist / 2 - print(self.step_dist) def plot(self, ax, labels, colors): - """draw the bubble plot + """ + draw the bubble plot Args: ax (matplotlib.axes._subplots.AxesSubplot) @@ -134,7 +142,8 @@ def plot(self, ax, labels, colors): bubble_plot = BubbleChart(a=np.array( browser_market_share['market_share']), bubble_distance=1) -# collapse plot 50 times. In some cases it might be useful to do this more or less often +# collapse plot 50 times. In some cases it might be useful +# to do this more or less often for i in range(50): bubble_plot.collapse() From 545b951787aae040d16833a291e25a1dd5c2e2db Mon Sep 17 00:00:00 2001 From: McToel <36071676+McToel@users.noreply.github.com> Date: Wed, 12 Aug 2020 16:05:28 +0200 Subject: [PATCH 03/13] Apply suggestions from code review Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- examples/misc/packed_bubbles.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py index d79613afd4d4..a70078f26845 100644 --- a/examples/misc/packed_bubbles.py +++ b/examples/misc/packed_bubbles.py @@ -1,4 +1,8 @@ """ +=================== +Packed bubble chart +=================== + Create a packed bubble / non overlapping 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 aroud colliding From 919e27e5cf7c496f34d1d7bc9d90532eb22dd879 Mon Sep 17 00:00:00 2001 From: McToel <36071676+McToel@users.noreply.github.com> Date: Wed, 12 Aug 2020 19:01:28 +0200 Subject: [PATCH 04/13] Apply suggestions from code review Co-authored-by: Jody Klymak --- examples/misc/packed_bubbles.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py index a70078f26845..45d73693c12f 100644 --- a/examples/misc/packed_bubbles.py +++ b/examples/misc/packed_bubbles.py @@ -1,11 +1,11 @@ """ =================== -Packed bubble chart +Packed-bubble chart =================== -Create a packed bubble / non overlapping bubble chart to represent scalar data. +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 aroud colliding +mass as possible while avoiding some collisions by moving around colliding objects. In this example we plot the market share of different desktop browsers. """ @@ -69,7 +69,8 @@ def center_distance(self, bubble, bubbles): def outline_distance(self, bubble, bubbles): center_distance = self.center_distance(bubble, bubbles) return center_distance - bubble[2] - \ - bubbles[:, 2] - self.bubble_distance + return (center_distance - bubble[2] - + bubbles[:, 2] - self.bubble_distance) def check_collisions(self, bubble, bubbles): distance = self.outline_distance(bubble, bubbles) From 7e1598de46260e3e810b151bf557fe90b0418c3b Mon Sep 17 00:00:00 2001 From: McToel Date: Thu, 13 Aug 2020 00:26:09 +0200 Subject: [PATCH 05/13] applied rewiews --- examples/misc/packed_bubbles.py | 138 ++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 60 deletions(-) diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py index d79613afd4d4..8b05cf4cf0b3 100644 --- a/examples/misc/packed_bubbles.py +++ b/examples/misc/packed_bubbles.py @@ -21,12 +21,19 @@ def __init__(self, r=None, a=None, bubble_distance=0): """ setup for bubble collapse - Args: - r (list, optional): radius of the bubbles. Defaults to None. - a (list, optional): area of the bubbles. Defaults to None. - Note: If r or a is sorted, the results might look weird - bubble_distance (int, optional): minimal distance the bubbles - should have after collapsing. Defaults to 0. + Parameters + ---------- + r : list, optional + radius of the bubbles. Defaults to None. + a : list, optional + area of the bubbles. Defaults to None. + bubble_distance : int, optional + minimal distance the bubbles should + have after collapsing. Defaults to 0. + + Notes + ----- + If r or a is sorted, the results might look weird """ if r is None: r = np.sqrt(a / np.pi) @@ -59,8 +66,8 @@ def center_of_mass(self): ) def center_distance(self, bubble, bubbles): - return np.sqrt(np.power(bubble[0] - bubbles[:, 0], 2) + - np.power(bubble[1] - bubbles[:, 1], 2)) + return np.hypot(bubble[0] - bubbles[:, 0], + bubble[1] - bubbles[:, 1]) def outline_distance(self, bubble, bubbles): center_distance = self.center_distance(bubble, bubbles) @@ -76,59 +83,73 @@ def collides_with(self, 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 from bubble to com - 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:4]) - - # 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 which 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: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) < 0.1: - self.step_dist = self.step_dist / 2 + def collapse(self, n_iterations=50): + """ + Move bubbles to the center of mass + + Parameters + ---------- + n_iterations :int, optional + number of moves to perform. Defaults to 50. + """ + for _i in range(n_iterations): + 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 from bubble to com + 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: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 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 which 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: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) < 0.1: + self.step_dist = self.step_dist / 2 def plot(self, ax, labels, colors): """ draw the bubble plot - Args: - ax (matplotlib.axes._subplots.AxesSubplot) - labels (list): labels of the bubbles - colors (list): colors of the bubbles + Parameters + ---------- + ax : matplotlib.axes._subplots.AxesSubplot + labels : list + labels of the bubbles + colors : list + colors of the bubbles """ for i in range(len(self)): circ = plt.Circle( @@ -142,10 +163,7 @@ def plot(self, ax, labels, colors): bubble_plot = BubbleChart(a=np.array( browser_market_share['market_share']), bubble_distance=1) -# collapse plot 50 times. In some cases it might be useful -# to do this more or less often -for i in range(50): - bubble_plot.collapse() +bubble_plot.collapse() fig, ax = plt.subplots(subplot_kw=dict(aspect="equal")) bubble_plot.plot( From 0cddb12a80992a510a8b53f8c3935169febf2765 Mon Sep 17 00:00:00 2001 From: McToel Date: Thu, 13 Aug 2020 00:40:26 +0200 Subject: [PATCH 06/13] fixed typo --- examples/misc/packed_bubbles.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py index 3f2ec312479f..8b05cf4cf0b3 100644 --- a/examples/misc/packed_bubbles.py +++ b/examples/misc/packed_bubbles.py @@ -1,11 +1,7 @@ """ -=================== -Packed-bubble chart -=================== - -Create a packed-bubble chart to represent scalar data. +Create a packed bubble / non overlapping 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 +mass as possible while avoiding some collisions by moving aroud colliding objects. In this example we plot the market share of different desktop browsers. """ @@ -76,8 +72,7 @@ def center_distance(self, bubble, bubbles): def outline_distance(self, bubble, bubbles): center_distance = self.center_distance(bubble, bubbles) return center_distance - bubble[2] - \ - return (center_distance - bubble[2] - - bubbles[:, 2] - self.bubble_distance) + bubbles[:, 2] - self.bubble_distance def check_collisions(self, bubble, bubbles): distance = self.outline_distance(bubble, bubbles) From eba983546d8eb78980de7f4e3823cfd2c6bb125f Mon Sep 17 00:00:00 2001 From: McToel Date: Thu, 13 Aug 2020 13:42:39 +0200 Subject: [PATCH 07/13] readded title --- examples/misc/packed_bubbles.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py index 8b05cf4cf0b3..23f3ca895901 100644 --- a/examples/misc/packed_bubbles.py +++ b/examples/misc/packed_bubbles.py @@ -1,4 +1,8 @@ """ +=================== +Packed-bubble chart +=================== + Create a packed bubble / non overlapping 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 aroud colliding From 2c7ef1a920f447c18393b9e7f051dbbb4a6bb6ef Mon Sep 17 00:00:00 2001 From: McToel Date: Thu, 13 Aug 2020 17:19:53 +0200 Subject: [PATCH 08/13] style fix --- examples/misc/packed_bubbles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py index 23f3ca895901..18e0ccf17634 100644 --- a/examples/misc/packed_bubbles.py +++ b/examples/misc/packed_bubbles.py @@ -3,7 +3,7 @@ Packed-bubble chart =================== -Create a packed bubble / non overlapping bubble chart to represent scalar data. +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 aroud colliding objects. In this example we plot the market share of different desktop From 5265a491a529906a0e0f165216f2c2ce8696ffb9 Mon Sep 17 00:00:00 2001 From: McToel Date: Fri, 14 Aug 2020 10:56:19 +0200 Subject: [PATCH 09/13] Apply suggestions from code review --- examples/misc/packed_bubbles.py | 73 +++++++++++++++------------------ 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py index 18e0ccf17634..9c3e46977930 100644 --- a/examples/misc/packed_bubbles.py +++ b/examples/misc/packed_bubbles.py @@ -15,54 +15,45 @@ browser_market_share = { 'browsers': ['firefox', 'chrome', 'safari', 'edge', 'ie', 'opera'], - 'market_share': [783, 6881, 371, 704, 587, 124], - 'color': ['b', 'g', 'r', 'c', 'm', 'y'] + 'market_share': np.array([7.83, 68.81, 3.71, 7.04, 5.87, 1.24]), + 'color': ['#5A69AF', '#579E65', '#F9C784', '#FC944A', '#F24C00', '#00B825'] } class BubbleChart: - def __init__(self, r=None, a=None, bubble_distance=0): + def __init__(self, a, bubble_spacing=0): """ - setup for bubble collapse + Setup for bubble collapse. Parameters ---------- - r : list, optional - radius of the bubbles. Defaults to None. - a : list, optional - area of the bubbles. Defaults to None. - bubble_distance : int, optional - minimal distance the bubbles should - have after collapsing. Defaults to 0. + a : list + Area of the bubbles. + bubble_spacing : float, default: 0 + Minimal spacing between bubbles after collapsing. Notes ----- - If r or a is sorted, the results might look weird + If a is sorted, the results might look weird. """ - if r is None: - r = np.sqrt(a / np.pi) - if a is None: - a = np.power(r, 2) * 2 - - self.bubble_distance = bubble_distance - self.n = len(r) - self.bubbles = np.ones((len(self), 4)) + r = np.sqrt(a / np.pi) + + self.bubble_spacing = bubble_spacing + self.bubbles = np.ones((len(a), 4)) self.bubbles[:, 2] = r self.bubbles[:, 3] = a - self.maxstep = 2 * self.bubbles[:, 2].max() + self.bubble_distance + 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))) + length = np.ceil(np.sqrt(len(self.bubbles))) 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.bubbles[:, 0] = gx.flatten()[:len(self.bubbles)] + self.bubbles[:, 1] = gy.flatten()[:len(self.bubbles)] self.com = self.center_of_mass() - def __len__(self): - return self.n def center_of_mass(self): return np.average( @@ -76,7 +67,7 @@ def center_distance(self, bubble, bubbles): def outline_distance(self, bubble, bubbles): center_distance = self.center_distance(bubble, bubbles) return center_distance - bubble[2] - \ - bubbles[:, 2] - self.bubble_distance + bubbles[:, 2] - self.bubble_spacing def check_collisions(self, bubble, bubbles): distance = self.outline_distance(bubble, bubbles) @@ -89,16 +80,16 @@ def collides_with(self, bubble, bubbles): def collapse(self, n_iterations=50): """ - Move bubbles to the center of mass + Move bubbles to the center of mass. Parameters ---------- - n_iterations :int, optional - number of moves to perform. Defaults to 50. + n_iterations : int, default: 50 + Number of moves to perform. """ for _i in range(n_iterations): moves = 0 - for i in range(len(self)): + for i in range(len(self.bubbles)): rest_bub = np.delete(self.bubbles, i, 0) # try to move directly towoards the center of mass # dir_vec from bubble to com @@ -140,22 +131,22 @@ def collapse(self, n_iterations=50): self.bubbles[i, :] = new_bubble self.com = self.center_of_mass() - if moves / len(self) < 0.1: + if moves / len(self.bubbles) < 0.1: self.step_dist = self.step_dist / 2 def plot(self, ax, labels, colors): """ - draw the bubble plot + Draw the bubble plot. Parameters ---------- - ax : matplotlib.axes._subplots.AxesSubplot + ax : matplotlib.axes.Axes labels : list - labels of the bubbles + Labels of the bubbles. colors : list - colors of the bubbles + Colors of the bubbles. """ - for i in range(len(self)): + 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) @@ -164,13 +155,13 @@ def plot(self, ax, labels, colors): # set market share of the browsers as area of the bubbles -bubble_plot = BubbleChart(a=np.array( - browser_market_share['market_share']), bubble_distance=1) +bubble_chart = BubbleChart(a=browser_market_share['market_share'], + bubble_spacing=0.1) -bubble_plot.collapse() +bubble_chart.collapse() fig, ax = plt.subplots(subplot_kw=dict(aspect="equal")) -bubble_plot.plot( +bubble_chart.plot( ax, browser_market_share['browsers'], browser_market_share['color']) ax.axis("off") ax.relim() From b252677b67c1b987f199b9109445d232c5a6aeac Mon Sep 17 00:00:00 2001 From: McToel Date: Fri, 14 Aug 2020 12:16:08 +0200 Subject: [PATCH 10/13] removed blank line --- examples/misc/packed_bubbles.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py index 9c3e46977930..538ee24a4e0c 100644 --- a/examples/misc/packed_bubbles.py +++ b/examples/misc/packed_bubbles.py @@ -54,7 +54,6 @@ def __init__(self, a, bubble_spacing=0): self.com = self.center_of_mass() - def center_of_mass(self): return np.average( self.bubbles[:, :2], axis=0, weights=self.bubbles[:, 3] From 008f981efc5e4b27e7aa6ce68400f7bafaa4a391 Mon Sep 17 00:00:00 2001 From: McToel Date: Fri, 14 Aug 2020 12:44:10 +0200 Subject: [PATCH 11/13] nicer array casting --- examples/misc/packed_bubbles.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py index 538ee24a4e0c..c3e3aecab4e5 100644 --- a/examples/misc/packed_bubbles.py +++ b/examples/misc/packed_bubbles.py @@ -15,7 +15,7 @@ browser_market_share = { 'browsers': ['firefox', 'chrome', 'safari', 'edge', 'ie', 'opera'], - 'market_share': np.array([7.83, 68.81, 3.71, 7.04, 5.87, 1.24]), + 'market_share': [7.83, 68.81, 3.71, 7.04, 5.87, 1.24], 'color': ['#5A69AF', '#579E65', '#F9C784', '#FC944A', '#F24C00', '#00B825'] } @@ -36,6 +36,7 @@ def __init__(self, a, bubble_spacing=0): ----- If a is sorted, the results might look weird. """ + a = np.asarray(a) r = np.sqrt(a / np.pi) self.bubble_spacing = bubble_spacing From c5c765e20956b58f081e046a58d5ec346882941e Mon Sep 17 00:00:00 2001 From: McToel Date: Sat, 15 Aug 2020 10:12:10 +0200 Subject: [PATCH 12/13] Apply suggestions from code review --- examples/misc/packed_bubbles.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py index c3e3aecab4e5..c52fb58745f6 100644 --- a/examples/misc/packed_bubbles.py +++ b/examples/misc/packed_bubbles.py @@ -5,9 +5,10 @@ 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 aroud colliding +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 @@ -15,13 +16,13 @@ browser_market_share = { 'browsers': ['firefox', 'chrome', 'safari', 'edge', 'ie', 'opera'], - 'market_share': [7.83, 68.81, 3.71, 7.04, 5.87, 1.24], + '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, a, bubble_spacing=0): + def __init__(self, area, bubble_spacing=0): """ Setup for bubble collapse. @@ -36,19 +37,19 @@ def __init__(self, a, bubble_spacing=0): ----- If a is sorted, the results might look weird. """ - a = np.asarray(a) - r = np.sqrt(a / np.pi) + area = np.asarray(area) + r = np.sqrt(area / np.pi) self.bubble_spacing = bubble_spacing - self.bubbles = np.ones((len(a), 4)) + self.bubbles = np.ones((len(area), 4)) self.bubbles[:, 2] = r - self.bubbles[:, 3] = a + 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(0, length * self.maxstep, self.maxstep) + 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)] @@ -91,11 +92,11 @@ def collapse(self, n_iterations=50): moves = 0 for i in range(len(self.bubbles)): rest_bub = np.delete(self.bubbles, i, 0) - # try to move directly towoards the center of mass - # dir_vec from bubble to com + # 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 dir_vec to have length of 1 + # shorten direction vector to have length of 1 dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec)) # calculate new bubble position @@ -111,12 +112,12 @@ def collapse(self, n_iterations=50): # 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 + # 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 vec + # calculate orthagonal vector orth = np.array([dir_vec[1], -dir_vec[0]]) - # test which dir to go + # test which direction to go new_point1 = (self.bubbles[i, :2] + orth * self.step_dist) new_point2 = (self.bubbles[i, :2] - orth * @@ -154,8 +155,7 @@ def plot(self, ax, labels, colors): horizontalalignment='center', verticalalignment='center') -# set market share of the browsers as area of the bubbles -bubble_chart = BubbleChart(a=browser_market_share['market_share'], +bubble_chart = BubbleChart(area=browser_market_share['market_share'], bubble_spacing=0.1) bubble_chart.collapse() From fc6d671c9bd4da614c10124d6484884fe35c858c Mon Sep 17 00:00:00 2001 From: McToel Date: Sun, 16 Aug 2020 10:18:25 +0200 Subject: [PATCH 13/13] fixed variable names in docstring --- examples/misc/packed_bubbles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/misc/packed_bubbles.py b/examples/misc/packed_bubbles.py index c52fb58745f6..d3d9df6895fd 100644 --- a/examples/misc/packed_bubbles.py +++ b/examples/misc/packed_bubbles.py @@ -28,14 +28,14 @@ def __init__(self, area, bubble_spacing=0): Parameters ---------- - a : list + area : array-like Area of the bubbles. bubble_spacing : float, default: 0 Minimal spacing between bubbles after collapsing. Notes ----- - If a is sorted, the results might look weird. + If "area" is sorted, the results might look weird. """ area = np.asarray(area) r = np.sqrt(area / np.pi)