You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 03:27:02