[python] 圆形嵌套图Circular Packing

圆形嵌套图Circular Packing


圆形嵌套图Circular Packing能够将一组组圆形互相嵌套起来,以显示数据的层次关系。本文主要基于circlify实现圆形嵌套图的绘制。circlify包由Python实现。官方开源地址见: circlify

您可以使用以下代码安装circlify:

pip install circlify

本文主要参考circlifycircular-packing

1 具有一级层次的圆形嵌套图绘制

circlify可以在没有层次结构的情况下工作,即只有一组数字变量,将每个变量都将显示为圆形。请注意,该包仅计算每个圆形的位置和大小。完成后,matplotlib用于制作图表本身。此外circlify库也提供了一个bubbles()函数完成所有绘图的功能。但它没有提供很多定制,所以matplotlib在这里是一个更好的绘图选择。

1.1 绘图数据与circlify计算

基于一级层次结构的基本圆形嵌套图只需要两列数据框。第一列提供每个项目的名称(用于标记)。第二列提供项目的数值。它控制圆形大小。

import pandas as pd
df = pd.DataFrame({
    'Name': ['A', 'B', 'C', 'D', 'E', 'F'],
    'Value': [10, 2, 23, 87, 12, 65]
})
df
NameValue
0A10
1B2
2C23
3D87
4E12
5F65

在具有一级层次结构的基本圆形嵌套图中,数据集的每个实体都由一个圆圈表示。圆圈大小与其代表的值成正比。工作中最难的部分是计算每个圆的位置和大小。幸运的是,该circlify库提供了一个circlify()进行计算的函数。它将作为绘图输入。
计算函数输入参数如下:

  • data :(必需)从大到小排序的正值列表
  • show_enclosure :(可选)一个布尔值,指示是否在所有圆之外添加一个最小包围圆(默认为 False)
  • target_enclosure :(可选)最小包围圆的参数信息(默认为单位圆 (0, 0, 1))
# import the circlify library
# 导入库
import circlify

# compute circle positions:
# 计算圆的位置
# circle是一个列表
circles = circlify.circlify(
    df['Value'].tolist(), 
    show_enclosure=False, 
    target_enclosure=circlify.Circle(x=0, y=0, r=1)
)
# 输出要绘制其中一个圆形信息,x,y为圆心坐标,level为绘制等级
circles[0]
Circle(x=-0.44578608966292743, y=0.537367020215489, r=0.08132507760370634, level=1, ex={'datum': 2})

1.2 图形绘制

1.2.1 基础图形绘制

通过circlify计算数据后,基础的圆形嵌套图绘制如下所示。

# import libraries
import circlify
import matplotlib.pyplot as plt

# Create just a figure and only one subplot
# 设置图像尺寸
fig, ax = plt.subplots(figsize=(7,7))

# Remove axes
# 设置matplotlib不显示坐标轴
ax.axis('off')

# Find axis boundaries
# 查找轴边界
lim = max(
    max(
        abs(circle.x) + circle.r,
        abs(circle.y) + circle.r,
    )
    for circle in circles
)
plt.xlim(-lim, lim)
plt.ylim(-lim, lim)

# print circles
# 画圆
for circle in circles:
    x, y, r = circle
    # fill表示不填充圆
    ax.add_patch(plt.Circle((x, y), r, alpha=0.2, linewidth=2, fill=False))

png

1.2.2 视觉调整

让我们从这里做一些更漂亮、更有洞察力的事情。我们将添加一个标题,给圆形着色并添加标签:

# import libraries
import circlify
import matplotlib.pyplot as plt

# Create just a figure and only one subplot
fig, ax = plt.subplots(figsize=(10,10))

# Title
# 添加标题
ax.set_title('Basic circular packing')

# Remove axes
ax.axis('off')

# Find axis boundaries
lim = max(
    max(
        abs(circle.x) + circle.r,
        abs(circle.y) + circle.r,
    )
    for circle in circles
)
plt.xlim(-lim, lim)
plt.ylim(-lim, lim)

# list of labels
# 添加标签
labels = df['Name']

# print circles
for circle, label in zip(circles, labels):
    x, y, r = circle
    ax.add_patch(plt.Circle((x, y), r, alpha=0.2, linewidth=2))
    # 添加标签
    plt.annotate(
          label, 
          (x,y ) ,
          va='center',
          ha='center'
     )

png

1.2.3 圆形之间的空间设置

可以轻松地在圆形之间添加间距。只需要提供半径参数的百分比给add_patch()(此处为70%)。

# Create just a figure and only one subplot
fig, ax = plt.subplots(figsize=(10,10))

# Title
ax.set_title('Basic circular packing')

# Remove axes
ax.axis('off')

# Find axis boundaries
lim = max(
    max(
        abs(circle.x) + circle.r,
        abs(circle.y) + circle.r,
    )
    for circle in circles
)
plt.xlim(-lim, lim)
plt.ylim(-lim, lim)

# list of labels
labels = df['Name']

# print circles
for circle, label in zip(circles, labels):
    x, y, r = circle
    # facecolor设置圆的填充颜色,edgecolor设置边框颜色
    ax.add_patch(plt.Circle((x, y), r*0.7, alpha=0.9, linewidth=2, facecolor="#69b2a3", edgecolor="black"))
    # boxstyle设置边框形状,pad设置边框填充
    plt.annotate(label, (x,y ) ,va='center', ha='center', bbox=dict(facecolor='white', edgecolor='black', boxstyle='round', pad=.5))

png

2 具有多级层次的圆形嵌套图绘制

下面将解释如何构建具有多个层次结构的圆形嵌套图。它使用circlify库来计算圆形位置,并通过matplotlib用于渲染图形。

2.1 绘图数据与circlify计算

此示例考虑分层数据集。世界被大陆分割。大陆按国家划分。每个国家都有一个值(人口规模)。我们的目标是将每个国家表示为一个圆圈,其大小与其人口成正比。让我们创建这样一个数据集:

data = [{'id': 'World', 'datum': 6964195249, 'children' : [
              {'id' : "North America", 'datum': 450448697,
                   'children' : [
                     {'id' : "United States", 'datum' : 308865000},
                     {'id' : "Mexico", 'datum' : 107550697},
                     {'id' : "Canada", 'datum' : 34033000} 
                   ]},
              {'id' : "South America", 'datum' : 278095425, 
                   'children' : [
                     {'id' : "Brazil", 'datum' : 192612000},
                     {'id' : "Colombia", 'datum' : 45349000},
                     {'id' : "Argentina", 'datum' : 40134425}
                   ]},
              {'id' : "Europe", 'datum' : 209246682,  
                   'children' : [
                     {'id' : "Germany", 'datum' : 81757600},
                     {'id' : "France", 'datum' : 65447374},
                     {'id' : "United Kingdom", 'datum' : 62041708}
                   ]},
              {'id' : "Africa", 'datum' : 311929000,  
                   'children' : [
                     {'id' : "Nigeria", 'datum' : 154729000},
                     {'id' : "Ethiopia", 'datum' : 79221000},
                     {'id' : "Egypt", 'datum' : 77979000}
                   ]},
              {'id' : "Asia", 'datum' : 2745929500,  
                   'children' : [
                     {'id' : "China", 'datum' : 1336335000},
                     {'id' : "India", 'datum' : 1178225000},
                     {'id' : "Indonesia", 'datum' : 231369500}
                   ]}
    ]}]

data
[{'id': 'World',
  'datum': 6964195249,
  'children': [{'id': 'North America',
    'datum': 450448697,
    'children': [{'id': 'United States', 'datum': 308865000},
     {'id': 'Mexico', 'datum': 107550697},
     {'id': 'Canada', 'datum': 34033000}]},
   {'id': 'South America',
    'datum': 278095425,
    'children': [{'id': 'Brazil', 'datum': 192612000},
     {'id': 'Colombia', 'datum': 45349000},
     {'id': 'Argentina', 'datum': 40134425}]},
   {'id': 'Europe',
    'datum': 209246682,
    'children': [{'id': 'Germany', 'datum': 81757600},
     {'id': 'France', 'datum': 65447374},
     {'id': 'United Kingdom', 'datum': 62041708}]},
   {'id': 'Africa',
    'datum': 311929000,
    'children': [{'id': 'Nigeria', 'datum': 154729000},
     {'id': 'Ethiopia', 'datum': 79221000},
     {'id': 'Egypt', 'datum': 77979000}]},
   {'id': 'Asia',
    'datum': 2745929500,
    'children': [{'id': 'China', 'datum': 1336335000},
     {'id': 'India', 'datum': 1178225000},
     {'id': 'Indonesia', 'datum': 231369500}]}]}]

然后我们需要用circlify()来计算表示每个国家和大陆圆形的位置,以及它们的半径。

# import the circlify library
import circlify

# Compute circle positions thanks to the circlify() function
# 计算
circles = circlify.circlify(
    data, 
    show_enclosure=False, 
    target_enclosure=circlify.Circle(x=0, y=0, r=1)
)
circles
[Circle(x=0.0, y=0.0, r=1.0, level=1, ex={'id': 'World', 'datum': 6964195249, 'children': [{'id': 'North America', 'datum': 450448697, 'children': [{'id': 'United States', 'datum': 308865000}, {'id': 'Mexico', 'datum': 107550697}, {'id': 'Canada', 'datum': 34033000}]}, {'id': 'South America', 'datum': 278095425, 'children': [{'id': 'Brazil', 'datum': 192612000}, {'id': 'Colombia', 'datum': 45349000}, {'id': 'Argentina', 'datum': 40134425}]}, {'id': 'Europe', 'datum': 209246682, 'children': [{'id': 'Germany', 'datum': 81757600}, {'id': 'France', 'datum': 65447374}, {'id': 'United Kingdom', 'datum': 62041708}]}, {'id': 'Africa', 'datum': 311929000, 'children': [{'id': 'Nigeria', 'datum': 154729000}, {'id': 'Ethiopia', 'datum': 79221000}, {'id': 'Egypt', 'datum': 77979000}]}, {'id': 'Asia', 'datum': 2745929500, 'children': [{'id': 'China', 'datum': 1336335000}, {'id': 'India', 'datum': 1178225000}, {'id': 'Indonesia', 'datum': 231369500}]}]}),
 Circle(x=-0.1891573044970616, y=0.7725949609994359, r=0.1964724487306323, level=2, ex={'id': 'Europe', 'datum': 209246682, 'children': [{'id': 'Germany', 'datum': 81757600}, {'id': 'France', 'datum': 65447374}, {'id': 'United Kingdom', 'datum': 62041708}]}),
 Circle(x=-0.5193811141243917, y=-0.4774793174718824, r=0.22650056519090414, level=2, ex={'id': 'South America', 'datum': 278095425, 'children': [{'id': 'Brazil', 'datum': 192612000}, {'id': 'Colombia', 'datum': 45349000}, {'id': 'Argentina', 'datum': 40134425}]}),
 Circle(x=-0.5250482991363239, y=0.4940564718994228, r=0.23988342689140008, level=2, ex={'id': 'Africa', 'datum': 311929000, 'children': [{'id': 'Nigeria', 'datum': 154729000}, {'id': 'Ethiopia', 'datum': 79221000}, {'id': 'Egypt', 'datum': 77979000}]}),
 Circle(x=-0.7117329289789401, y=0.0, r=0.28826707102105975, level=2, ex={'id': 'North America', 'datum': 450448697, 'children': [{'id': 'United States', 'datum': 308865000}, {'id': 'Mexico', 'datum': 107550697}, {'id': 'Canada', 'datum': 34033000}]}),
 Circle(x=0.28826707102105975, y=0.0, r=0.7117329289789401, level=2, ex={'id': 'Asia', 'datum': 2745929500, 'children': [{'id': 'China', 'datum': 1336335000}, {'id': 'India', 'datum': 1178225000}, {'id': 'Indonesia', 'datum': 231369500}]}),
 Circle(x=-0.8015572298502232, y=0.13991165332617728, r=0.06017798041665636, level=3, ex={'id': 'Canada', 'datum': 34033000}),
 Circle(x=-0.6218965087862706, y=-0.35827194898537407, r=0.06927524011838612, level=3, ex={'id': 'Argentina', 'datum': 40134425}),
 Circle(x=-0.6715240632168605, y=-0.49229197511777817, r=0.07363823567480635, level=3, ex={'id': 'Colombia', 'datum': 45349000}),
 Circle(x=-0.20484950837730978, y=0.8820383650518233, r=0.08590977893113161, level=3, ex={'id': 'United Kingdom', 'datum': 62041708}),
 Circle(x=-0.2883116431566897, y=0.7291956444670085, r=0.08823620918716854, level=3, ex={'id': 'France', 'datum': 65447374}),
 Circle(x=-0.5807524341097545, y=0.6266527390697123, r=0.09606159016055799, level=3, ex={'id': 'Egypt', 'datum': 77979000}),
 Circle(x=-0.6616477217438194, y=0.4515509451194898, r=0.09682357206293761, level=3, ex={'id': 'Ethiopia', 'datum': 79221000}),
 Circle(x=-0.10145547990466931, y=0.7291956444670085, r=0.09861995406485186, level=3, ex={'id': 'Germany', 'datum': 81757600}),
 Circle(x=-0.8930220906231182, y=0.0, r=0.1069779093768817, level=3, ex={'id': 'Mexico', 'datum': 107550697}),
 Circle(x=-0.42950888228212775, y=0.4515509451194898, r=0.13531526739875396, level=3, ex={'id': 'Nigeria', 'datum': 154729000}),
 Circle(x=-0.44612447532083954, y=-0.49229197511777817, r=0.15176135222121462, level=3, ex={'id': 'Brazil', 'datum': 192612000}),
 Circle(x=0.2610622289123354, y=0.3631857971321717, r=0.15273517341003195, level=3, ex={'id': 'Indonesia', 'datum': 231369500}),
 Circle(x=-0.6047550196020585, y=0.0, r=0.18128916164417805, level=3, ex={'id': 'United States', 'datum': 308865000}),
 Circle(x=-0.07879852383709784, y=0.0, r=0.34466733412078254, level=3, ex={'id': 'India', 'datum': 1178225000}),
 Circle(x=0.6329344051418423, y=0.0, r=0.3670655948581576, level=3, ex={'id': 'China', 'datum': 1336335000})]

2.2 图形绘制


# import libraries
import circlify
import matplotlib.pyplot as plt

# Create just a figure and only one subplot
fig, ax = plt.subplots(figsize=(14,14))

# Title
ax.set_title('Repartition of the world population')

# Remove axes
ax.axis('off')

# Find axis boundaries
lim = max(
    max(
        abs(circle.x) + circle.r,
        abs(circle.y) + circle.r,
    )
    for circle in circles
)
plt.xlim(-lim, lim)
plt.ylim(-lim, lim)

# Print circle the highest level (continents):
# 打印最高级别的圆形也就是数据中的大陆,这一部分circle的level为3
for circle in circles:
    if circle.level != 2:
      continue
    x, y, r = circle
    ax.add_patch( plt.Circle((x, y), r, alpha=0.5, linewidth=2, color="lightblue"))

# 打印次高级别的圆形也就是数据中的国家,这一部分circle的level为2
for circle in circles:
    if circle.level != 3:
      continue
    x, y, r = circle
    label = circle.ex["id"]
    ax.add_patch( plt.Circle((x, y), r, alpha=0.5, linewidth=2, color="#69b3a2"))
    # 画出国家的名字
    plt.annotate(label, (x,y ), ha='center', color="white")

# Print labels for the continents
# 画出各大洲的标签
for circle in circles:
    if circle.level != 2:
      continue
    x, y, r = circle
    label = circle.ex["id"]
    plt.annotate(label, (x,y ) ,va='center', ha='center', bbox=dict(facecolor='white', edgecolor='black', boxstyle='round', pad=.5))

png

3 circlify自带绘图函数

如果只是想看看数据如何,可以用circlify的自带绘图函数bubbles,但是不能定制图形。circlify的bubbles函数好处就是不需要用matplotlib一层一层的画圆。

一级层次绘图

from pprint import pprint as pp
import circlify
# 定义圆
# show_enclosure=True表示显示外圈大圆,也就是结果中的#0
circles = circlify.circlify([19, 17, 13, 11, 7, 5, 3, 2, 1], show_enclosure=True)
# 美化输出
pp(circles)

# 展示结果
circlify.bubbles(circles)
[Circle(x=0.0, y=0.0, r=1.0, level=0, ex=None),
 Circle(x=-0.633232604611031, y=-0.47732413442115296, r=0.09460444572843042, level=1, ex={'datum': 1}),
 Circle(x=-0.7720311587589236, y=0.19946176418549022, r=0.13379089020993573, level=1, ex={'datum': 2}),
 Circle(x=-0.43168871955473165, y=-0.6391381648617572, r=0.16385970662353394, level=1, ex={'datum': 3}),
 Circle(x=0.595447603036083, y=0.5168251295666467, r=0.21154197162246005, level=1, ex={'datum': 5}),
 Circle(x=-0.5480911056188739, y=0.5115139053491098, r=0.2502998363185337, level=1, ex={'datum': 7}),
 Circle(x=0.043747233552068686, y=-0.6848366902134195, r=0.31376744998074435, level=1, ex={'datum': 11}),
 Circle(x=0.04298737651230445, y=0.5310431146935967, r=0.34110117996070605, level=1, ex={'datum': 13}),
 Circle(x=-0.3375943908160698, y=-0.09326467617622711, r=0.39006412239133215, level=1, ex={'datum': 17}),
 Circle(x=0.46484095011516874, y=-0.09326467617622711, r=0.4123712185399064, level=1, ex={'datum': 19})]

png

多级层次绘图

from pprint import pprint as pp
import circlify
data = [
        0.05, {'id': 'a2', 'datum': 0.05},
        {'id': 'a0', 'datum': 0.8, 'children': [0.3, 0.2, 0.2, 0.1], },
        {'id': 'a1', 'datum': 0.1, 'children': [
            {'id': 'a1_1', 'datum': 0.05}, {'datum': 0.04}, 0.01],
        },
    ]
circles = circlify.circlify(data, show_enclosure=True)
pp(circles)

# 展示结果
circlify.bubbles(circles)
[Circle(x=0.0, y=0.0, r=1.0, level=0, ex=None),
 Circle(x=-0.5658030759977484, y=0.4109778665114514, r=0.18469903125906464, level=1, ex={'datum': 0.05}),
 Circle(x=-0.5658030759977484, y=-0.4109778665114514, r=0.18469903125906464, level=1, ex={'id': 'a2', 'datum': 0.05}),
 Circle(x=-0.7387961250362587, y=0.0, r=0.2612038749637415, level=1, ex={'id': 'a1', 'datum': 0.1, 'children': [{'id': 'a1_1', 'datum': 0.05}, {'datum': 0.04}, 0.01]}),
 Circle(x=0.2612038749637414, y=0.0, r=0.7387961250362586, level=1, ex={'id': 'a0', 'datum': 0.8, 'children': [0.3, 0.2, 0.2, 0.1]}),
 Circle(x=-0.7567888163564135, y=0.1408782365133844, r=0.0616618704777984, level=2, ex={'datum': 0.01}),
 Circle(x=-0.8766762590444033, y=0.0, r=0.1233237409555968, level=2, ex={'datum': 0.04}),
 Circle(x=-0.6154723840806618, y=0.0, r=0.13788013400814464, level=2, ex={'id': 'a1_1', 'datum': 0.05}),
 Circle(x=0.6664952237042414, y=0.33692908734605553, r=0.21174557028487648, level=2, ex={'datum': 0.1}),
 Circle(x=-0.1128831469183017, y=-0.23039288135707192, r=0.29945345726929773, level=2, ex={'datum': 0.2}),
 Circle(x=0.1563193680487183, y=0.304601976765483, r=0.29945345726929773, level=2, ex={'datum': 0.2}),
 Circle(x=0.5533243963620487, y=-0.23039288135707192, r=0.3667540860110527, level=2, ex={'datum': 0.3})]

png

4 参考

posted @ 2021-07-23 12:09  落痕的寒假  阅读(193)  评论(0编辑  收藏  举报