嵌套饼图

我们可以用嵌套饼图更直观地展示某些的类型数据, 下面是用一个例子详细介绍如何用python绘制嵌套饼图.

这里要绘制的是一个最内圈 6 个类别, 每个类别再细分为几个方向, 各个方向上再进行细分.

大致的效果如下:

matplotlib16

下面我们来一步一步实现这个嵌套饼图.

绘制饼图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ax.pie( vals_first, radius=1-size-size,
colors=inner_colors, labels=labels_first,
labeldistance=0.5, rotatelabels=True,
textprops={'fontsize': 11}, wedgeprops=dict(width=size, edgecolor='w'))

ax.pie( vals_second.flatten(), radius=1-size,
colors=outer_colors, labels=labels_seocnd,
labeldistance=0.7, rotatelabels=True,
textprops={'fontsize': 11}, wedgeprops=dict(width=size, edgecolor='w'))

ax.pie( vals_third.flatten(), radius=1,
colors=outer_colors, labels=labels_third,
labeldistance=0.8, rotatelabels=True,
textprops={'fontsize': 11}, wedgeprops=dict(width=size, edgecolor='w'))

这里是使用matplotlib绘制饼图的代码, 绘制了三层饼图, 对每一层饼图的半径和宽度进行了设置, 使得三层饼图可以一层一层套在一起.

这些参数中, 第一个参数为绘制饼图的数值, radius为饼图的半径, textprop为标签的属性, 设置rotatelabels为True, 使得标签自动旋转. wedgeprops中可以设置饼图的各项数据, 这里我们设置了饼图的宽度和边颜色, 使得饼图显示成嵌套饼图的样子.

这边数据, 颜色, 标签的设置会在后面慢慢细说.

数据设置

数据方面, 由于这次的数据差别太大, 若按照比例来绘制的话, 必然有一些数据显示不出来, 所以我选择将所有数据添加一个固定的数值, 再按照比例来绘制. 但是还有一个问题, 部分数据过大或过小, 会非常影响图形的观感, 这里我只能在绘制图形时手动设置该部分所占的大小.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 第二圈的数据
vals_b = np.array([
[47.5,11.7,15.2,9.6],
[0,44.8,7.5,0],
[9.2, 68.5 , 0, 0],
[1.2, 7.2, 0, 0],
[80,0, 0, 0],
[1.7, 18.9, 0, 0]
])

# 第三圈的数据
vals_c = np.array([
[47.5,11.7,15.2,9.6],
[0,36.6,8.2,7.5],
[9.2,38.1,30.4, 0],
[1.2, 5.8, 1.4, 0],
[80,0, 0, 0],
[1.7, 18.9, 0, 0]
])

# 第一圈的数据
vals_inner = vals_middle.sum(axis=1)

# 绘图时最内圈使用的数值为内圈各类数据加上base
vals_first = vals_inner + base

’‘’
绘图时第二圈使用的数值, 因为最内圈每个类别都加上了base, 所以为了确保第二圈的数值和内圈相匹配, 第二圈的各类别要按照各自所占的比例分配各类的总数值.
‘’‘
vals_second = np.zeros((6, 4))
for i in range(6):
s_a = vals_first[i]
s_b = vals_a[i].sum()
# 如果第二圈某类总数值为0, 则分配base.
if s_b == 0.0:
vals_second[i][1] = base
continue
for j in range(4):
vals_second[i][j] = (vals_mid[i][j] / s_b) * s_a

# 第三圈使用的数值, 和上方同理
vals_third = np.zeros((6, 4))
for i in range(6):
s_a = vals_first[i]
s_b = vals_outer[i].sum()
if s_b == 0.0:
vals_third[i][1] = base
continue
for j in range(4):
vals_third[i][j] = (vals_outer[i][j] / s_b) * s_a

颜色设置

官方自带的 Colormaps, 这里我需要的是6×4共24种颜色, 于是我选择了colormap tab20c的全部色彩和 tab20b 中 5至8 颜色, 一共24种, 当然你也可以通过 rgba 自己定义颜色而不是选择colormap.

matplotlib17

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
# 获取colormap tab20c和tab20b的颜色
cmap_c = plt.get_cmap("tab20c")
cmap_b = plt.get_cmap("tab20b")

# 使用tab20c的全部颜色和tab20b中的 5至8 颜色
cmap_1 = cmap_c(np.arange(20))
cmap_2 = cmap_b(np.array([4, 5, 6, 7]))

# 内圈的颜色是每4个颜色中色彩最深的那个. vstack是将两类颜色叠加在一起
inner_colors = np.vstack((cmap_1[::4], cmap_2[0]))
# 外圈的颜色是全部24种颜色
outer_colors = np.vstack((cmap_1, cmap_2))

标签设置

标签总共是三圈的标签, 内圈, 中圈和外圈, 这里内圈我为了更美观得显示数据, 更改了某些类别的占比, 所以不能直接从输入数据中获取到值, 只能手动输入数值.

中圈和外圈也是同理, 因为绘制的量不大, 中圈和外圈我全部采取了手动输入的方法, 事实上效率确实是大大下降了, 但是毕竟是为了图案美观, 不得已而为之. 如果数据允许的话, 这三种标签中的数据都可以采用输入的数据, 而非手动填写.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
labels_first=["餐厨废弃物\n{}万吨".format(vals_first[0]), 
"农业秸秆\n{}万吨".format(vals_first[1]),
"水草\n151.2万吨",
"园林绿化\n废弃物\n{}万吨".format(vals_first[3]),
"淤泥\n432.0万吨",
"畜禽粪便\n21.6万吨"
]

labels_seocnd=[
"未分类收集\n67.6万吨",
"生物干化\n3.7万吨",
"厌氧发酵\n10.2万吨",
"油水分离\n2.6万吨",

"",
"粉碎\n46.8万吨",
"好氧发酵\n3.5万吨",
"",

"未处理\n4.2万吨",
"藻水分离\n147.0万吨",
"",
"",

"未处理\n1.2万吨",
"粉碎\n7.2万吨",
"",
"",

"堆放\n432.0万吨",
"",
"",
"",

"未处理\n0.7万吨",
"好氧发酵\n19.9万吨",
"",
"",
]

labels_third=[
"未处理\n67.5万吨",
"肥料化、发电\n3.7万吨",
"沼气、沼渣发电\n10.2万吨",
"焚烧\n2.6万吨",

"",
"还田\n42.6万吨",
"燃料化\n4.2万吨",
"肥料化\n3.5万吨",

"未利用\n4.2万吨",
"燃料化\n80.2万吨",
"肥料化\n66.8万吨",
"",

"未利用\n1.2万吨",
"肥料化\n5.8万吨",
"燃料化\n1.4万吨",
"",

"未利用\n432.0万吨",
"",
"",
"",

"未利用\n0.7万吨",
"肥料化\n19.9万吨",
"",
"",
]

图例

这里因为只需要内圈的图例, 所以我把内圈的handels单独拿了出来

1
2
3
4
handles, labels =  ax.pie(vals_first, radius=1-size-size, 
labels=labels_first,
labeldistance=0.5, rotatelabels=True, textprops={'fontsize': 11},
colors=inner_colors, wedgeprops=dict(width=size, edgecolor='w'))

再绘制图例时不再简单使用plt.legend()而是使用:

1
2
3
4
5
6
7
8
9
plt.legend(handles=handles, labels=[
"餐厨废弃物",
"农业秸秆",
"水草",
"园林绿化废弃物",
"淤泥",
"畜禽粪便"],
loc = 1
)

这样出来的图中就只会显示最内层的图例了.

附全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
from matplotlib import pyplot as plt
import numpy as np

size = 0.25
base = 50


plt.rcParams['font.family'] = 'SimHei'
fig, ax = plt.subplots(figsize = (10, 10))

vals_middle = np.array([
[47.5,11.7,15.2,9.6],
[0,44.8,7.5,0],
[9.2, 68.5 , 0, 0],
[1.2, 7.2, 0, 0],
[80,0, 0, 0],
[1.7, 18.9, 0, 0]
])

vals_outer = np.array([
[47.5,11.7,15.2,9.6],
[0,36.6,8.2,7.5],
[9.2,38.1,30.4, 0],
[1.2, 5.8, 1.4, 0],
[80,0, 0, 0],
[1.7, 18.9, 0, 0]
])

vals_inner = vals_middle.sum(axis=1)

# 最内圈使用的数值为内圈各类数据加上base
vals_first = vals_inner + base

'''
第二圈使用的数值, 因为最内圈每个类别都加上了base, 所以为了确保第二圈的数值和内圈相匹配, 第二圈的各类别要按照各自所占的比例分配各类的总数值.
'''
vals_second = np.zeros((6, 4))
for i in range(6):
s_a = vals_first[i]
s_b = vals_middle[i].sum()
# 如果第二圈某类总数值为0, 则分配base.
if s_b == 0.0:
vals_second[i][1] = base
continue
for j in range(4):
vals_second[i][j] = (vals_middle[i][j] / s_b) * s_a

# 第三圈使用的数值, 和上方同理
vals_third = np.zeros((6, 4))
for i in range(6):
s_a = vals_first[i]
s_b = vals_outer[i].sum()
if s_b == 0.0:
vals_third[i][1] = base
continue
for j in range(4):
vals_third[i][j] = (vals_outer[i][j] / s_b) * s_a


# 获取colormap tab20c和tab20b的颜色
cmap_c = plt.get_cmap("tab20c")
cmap_b = plt.get_cmap("tab20b")

# 使用tab20c的全部颜色和tab20b中的 5至8 颜色
cmap_1 = cmap_c(np.arange(20))
cmap_2 = cmap_b(np.array([4, 5, 6, 7]))

# 内圈的颜色是每4个颜色中色彩最深的那个. vstack是将两类颜色叠加在一起
inner_colors = np.vstack((cmap_1[::4], cmap_2[0]))
# 外圈的颜色是全部24种颜色
outer_colors = np.vstack((cmap_1, cmap_2))



labels_first=["餐厨废弃物\n{}万吨".format(vals_inner[0]),
"农业秸秆\n{}万吨".format(vals_inner[1]),
"水草\n151.2万吨",
"园林绿化\n废弃物\n{}万吨".format(vals_inner[3]),
"淤泥\n432.0万吨",
"畜禽粪便\n21.6万吨"
]

labels_seocnd=[
"未分类收集\n67.6万吨",
"生物干化\n3.7万吨",
"厌氧发酵\n10.2万吨",
"油水分离\n2.6万吨",

"",
"粉碎\n46.8万吨",
"好氧发酵\n3.5万吨",
"",

"未处理\n4.2万吨",
"藻水分离\n147.0万吨",
"",
"",

"未处理\n1.2万吨",
"粉碎\n7.2万吨",
"",
"",

"堆放\n432.0万吨",
"",
"",
"",

"未处理\n0.7万吨",
"好氧发酵\n19.9万吨",
"",
"",
]

labels_third=[
"未处理\n67.5万吨",
"肥料化、发电\n3.7万吨",
"沼气、沼渣发电\n10.2万吨",
"焚烧\n2.6万吨",

"",
"还田\n42.6万吨",
"燃料化\n4.2万吨",
"肥料化\n3.5万吨",

"未利用\n4.2万吨",
"燃料化\n80.2万吨",
"肥料化\n66.8万吨",
"",

"未利用\n1.2万吨",
"肥料化\n5.8万吨",
"燃料化\n1.4万吨",
"",

"未利用\n432.0万吨",
"",
"",
"",

"未利用\n0.7万吨",
"肥料化\n19.9万吨",
"",
"",
]


handles, labels = ax.pie(vals_first, radius=1-size-size,
labels=labels_first,
labeldistance=0.5, rotatelabels=True, textprops={'fontsize': 11},
colors=inner_colors, wedgeprops=dict(width=size, edgecolor='w'))

ax.pie(vals_second.flatten(), radius=1-size, colors=outer_colors,
labels=labels_seocnd,
labeldistance=0.7, rotatelabels=True, textprops={'fontsize': 11},
wedgeprops=dict(width=size, edgecolor='w'))

ax.pie(vals_third.flatten(), radius=1, colors=outer_colors,
labels=labels_third,
labeldistance=0.8, rotatelabels=True, textprops={'fontsize': 11},
wedgeprops=dict(width=size, edgecolor='w'))


plt.title('某市有机废弃物产生、处理、利用情况', fontsize=25)
plt.legend(handles=handles, labels=[
"餐厨废弃物",
"农业秸秆",
"水草",
"园林绿化废弃物",
"淤泥",
"畜禽粪便"],
loc = 1
)
plt.show()