如何在Python中绘制圆形布局的打包气泡图?
如何修改Matplotlib打包气泡图实现圆形布局
官方示例的BubbleChart类采用网格初始布局+向质心收缩的逻辑,这种方式会生成紧凑的不规则布局,而非严格的圆形包裹效果。要实现圆形布局,需从初始位置生成、收缩约束两个核心部分修改:
核心修改点
1. 替换初始布局为圆形放射状排列
将原来的网格初始位置,改为围绕中心的圆形分布,让气泡从一开始就具备圆形雏形。修改__init__方法中的初始位置代码:
原代码:
# 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)]
替换为:
# 初始布局改为围绕中心的圆形分布 num_bubbles = len(self.bubbles) self.com = np.array([0.0, 0.0]) # 先固定质心为原点,后续更新 angles = np.linspace(0, 2 * np.pi, num_bubbles, endpoint=False) init_radius = self.maxstep * (num_bubbles // 4 + 1) self.bubbles[:, 0] = self.com[0] + init_radius * np.cos(angles) self.bubbles[:, 1] = self.com[1] + init_radius * np.sin(angles)
2. 增加圆形边界约束
在collapse方法的每次迭代后,限制气泡不能超出以质心为中心的虚拟圆形边界,避免气泡偏移出圆形区域。在迭代循环末尾添加:
# 添加圆形边界约束 max_dist_to_com = np.max(self.center_distance(self.bubbles[:, :2], np.array([self.com])) + self.bubbles[:, 2]) bound_radius = max_dist_to_com * 1.1 for i in range(len(self.bubbles)): dist_to_com = self.center_distance(self.bubbles[i, :2], np.array([self.com])) if dist_to_com + self.bubbles[i, 2] > bound_radius: dir_vec = self.bubbles[i, :2] - self.com dir_vec = dir_vec / dist_to_com new_pos = self.com + dir_vec * (bound_radius - self.bubbles[i, 2]) self.bubbles[i, :2] = new_pos
3. 调整收缩迭代次数
增加迭代次数,让气泡有足够时间调整到紧凑的圆形状态,调用collapse时传入n_iterations=100:
bubble_chart.collapse(n_iterations=100)
完整修改后的代码
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): 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 # 初始布局改为围绕中心的圆形分布 num_bubbles = len(self.bubbles) self.com = np.array([0.0, 0.0]) # 先固定质心为原点,后续更新 angles = np.linspace(0, 2 * np.pi, num_bubbles, endpoint=False) init_radius = self.maxstep * (num_bubbles // 4 + 1) self.bubbles[:, 0] = self.com[0] + init_radius * np.cos(angles) self.bubbles[:, 1] = self.com[1] + init_radius * np.sin(angles) 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=100): for _i in range(n_iterations): moves = 0 for i in range(len(self.bubbles)): rest_bub = np.delete(self.bubbles, i, 0) dir_vec = self.com - self.bubbles[i, :2] if np.linalg.norm(dir_vec) == 0: continue # 避免除以0 dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec)) new_point = self.bubbles[i, :2] + dir_vec * self.step_dist 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() moves += 1 else: for colliding in self.collides_with(new_bubble, rest_bub): dir_vec = rest_bub[colliding, :2] - self.bubbles[i, :2] if np.linalg.norm(dir_vec) == 0: continue dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec)) orth = np.array([dir_vec[1], -dir_vec[0]]) 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() # 添加圆形边界约束 max_dist_to_com = np.max(self.center_distance(self.bubbles[:, :2], np.array([self.com])) + self.bubbles[:, 2]) bound_radius = max_dist_to_com * 1.1 for i in range(len(self.bubbles)): dist_to_com = self.center_distance(self.bubbles[i, :2], np.array([self.com])) if dist_to_com + self.bubbles[i, 2] > bound_radius: dir_vec = self.bubbles[i, :2] - self.com dir_vec = dir_vec / dist_to_com new_pos = self.com + dir_vec * (bound_radius - self.bubbles[i, 2]) self.bubbles[i, :2] = new_pos if moves / len(self.bubbles) < 0.1: self.step_dist = self.step_dist / 2 def plot(self, ax, labels, colors): 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(n_iterations=100) 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()
效果说明
- 初始圆形布局让气泡从一开始就围绕中心排列,避免网格布局带来的不规则偏移
- 圆形边界约束确保所有气泡最终被限制在近似圆形的区域内
- 增加迭代次数让气泡有足够时间调整到紧凑且规则的状态
内容的提问来源于stack exchange,提问作者AlexPy
相关产品推荐
相关产品推荐

