本文主要是介绍【可视化】Python绘制风车玫瑰图动画,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
最终成品
思路
风车玫瑰图等效于极坐标系下的堆积条形图。
- 绘制直角坐标系下的堆积条形图。pyplot本身没有找到堆积条形图的方法,所以使用计算堆积。
- 因为极坐系下,x轴会卷积成一个圆,所以需要提前将x轴的刻度转成弧度。
- 由于x轴变换了,所以不能直接使用.bar()方法绘图,同时需要将数据构造转成 层级 * 柱数 的样式,分别绘制条形。
- 将子图的坐标系改成极坐标系。
- 使用FuncAnimation()方法绘制动画。
环境
Python 3.10
引入包
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as npplt.rcParams["font.sans-serif"] = ["SimHei"] # 指定中文字体
plt.rcParams['axes.unicode_minus'] = False # 指定负号等特殊符号兼容
创建测试数据
创建 (16,3) 的数集,对应16个风车柱子,每个柱子分三层。
data = pd.DataFrame(np.random.randint(70, 100, size=(16, 3)),index=[i for i in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'][:16], columns=["l1", 'l2', 'l3'])data
绘制堆积条形图
pyplot中没有找到堆积条形图的办法,只有 stackplot() 堆积面积图,所以直接用 bar()方法画。
每一层都指定前层的合计值为柱子底部 bottom,实现堆积效果。
因为数据集是随机生成的,所以每张图的柱子高度不一致,勿怪。
fig = plt.figure()
ax = fig.add_subplot()
for i in range(data.shape[1]):plt.bar(x=data.index, height=data.iloc[:, i], bottom=0 if i == 0 else data.iloc[:, 0:i].sum(axis=1))
转极坐标系
在subplot子图中指定 projection=“polar” ,即可使用极坐标系。
fig = plt.figure()
ax = fig.add_subplot(projection="polar") # 指定子图为极坐标系
for i in range(data.shape[1]):plt.bar(x=data.index, height=data.iloc[:, i], bottom=0 if i == 0 else data.iloc[:, 0:i].sum(axis=1))
由于x轴的刻度没有转换成角度,所以外圈的刻度不对,A和G靠太近了。
- 将x轴转换成弧度。共16个柱子,每个柱子所在弧度即是 2 π / 16 2\pi/16 2π/16
- 缩小桩子的宽度,避免挤到一起。
fig = plt.figure()
ax = fig.add_subplot(projection="polar") # 指定子图为极坐标系
x = [(i * np.pi * 2)/data.shape[0] for i in range(data.shape[0])] # 计算X轴的弧度
for i in range(data.shape[1]):plt.bar(x=x, height=data.iloc[:, i], bottom=0 if i == 0 else data.iloc[:, 0:i].sum(axis=1))
比较接近了,把X轴的角度标签换成数据标签 tick_label=data.index
fig = plt.figure()
ax = fig.add_subplot(projection="polar") # 指定子图为极坐标系
x = [(i * np.pi * 2)/data.shape[0] for i in range(data.shape[0])] # 计算X轴
for i in range(data.shape[1]):plt.bar(x=x, height=data.iloc[:, i], bottom=0 if i == 0 else data.iloc[:, 0:i].sum(axis=1), width=np.pi/data.shape[0], # 调整柱子宽度tick_label=data.index # 添加标签)
修饰文本
调整一下
- 把标签加到每个柱子上
- 隐藏坐标轴和边框
为啥plt.text()方法一次只能画一条文本?
fig = plt.figure()
ax = fig.add_subplot(projection="polar") # 指定子图为极坐标系
ax.spines["polar"].set_visible(False) # 隐藏边框
ax.axes.xaxis.set_visible(False) # 隐藏X轴
ax.axes.yaxis.set_visible(False) # 隐藏Y轴
x = [(i * np.pi * 2)/data.shape[0] for i in range(data.shape[0])] # 计算X轴
for i in range(data.shape[1]):ax.bar(x=x, height=data.iloc[:, i], bottom=0 if i == 0 else data.iloc[:, 0:i].sum(axis=1), width=np.pi/data.shape[0], # 调整柱子宽度tick_label=data.index # 添加标签)# 添加文本到柱子上
for i in range(data.shape[0]):plt.text(x=x[i], # 文本的X坐标y=data.iloc[i].sum(), # 文本的Y坐标s=f"{data.iloc[i].name}: {data.iloc[i].sum()}" # 文本内容)
修改文字:
- 旋转文字的角度,与柱子方向一致。
- 文字与柱太近了,远离一点。
- 同时修改对齐方式。O和P有点不齐。
fig = plt.figure()
ax = fig.add_subplot(projection="polar") # 指定子图为极坐标系
ax.spines["polar"].set_visible(False) # 隐藏边框
ax.axes.xaxis.set_visible(False) # 隐藏X轴
ax.axes.yaxis.set_visible(False) # 隐藏Y轴
x = [(i * np.pi * 2)/data.shape[0] for i in range(data.shape[0])] # 计算X轴
for i in range(data.shape[1]):ax.bar(x=x, height=data.iloc[:, i], bottom=0 if i == 0 else data.iloc[:, 0:i].sum(axis=1), width=np.pi/data.shape[0], # 调整柱子宽度tick_label=data.index # 添加标签)# 添加文本到柱子上
for i in range(data.shape[0]):plt.text(x=x[i], # 文本的X坐标y=data.iloc[i].sum() * 1.05, # 文本的Y坐标s=f"{data.iloc[i].name}: {data.iloc[i].sum()}", # 文本内容rotation= (i * 360)/data.shape[0], # 旋转角度rotation_mode="anchor", # 指定旋转模式horizontalalignment="left", # 水平对齐方式verticalalignment="center" # 垂直对齐方式)
修饰颜色
修改柱子的颜色, 并添加柱子边框。因为配色无能,所以直接使用了plt的颜色重采样,好东西啊。
思路是先使用 resampled() 方法从调色盘中取 16 * 3 = 48 个颜色,再给每个柱子用不同的颜色。
fig = plt.figure()
ax = fig.add_subplot(projection="polar") # 指定子图为极坐标系
ax.spines["polar"].set_visible(False) # 隐藏边框
ax.axes.xaxis.set_visible(False) # 隐藏X轴
ax.axes.yaxis.set_visible(False) # 隐藏Y轴
x = [(i * np.pi * 2)/data.shape[0] for i in range(data.shape[0])] # 计算X轴
color_list = plt.colormaps['inferno'].resampled(48).colors # 从调色盘中重采样48个颜色,做为柱子的颜色。for i in range(data.shape[1]):ax.bar(x=x, height=data.iloc[:, i], bottom=0 if i == 0 else data.iloc[:, 0:i].sum(axis=1), width=np.pi/data.shape[0], # 调整柱子宽度tick_label=data.index, # 添加标签color=color_list[i*16:(i+1)*16], # 添加颜色edgecolor="k", # 指定组间边框颜色linewidth=1, # 组间边框宽度)# 添加文本到柱子上
for i in range(data.shape[0]):plt.text(x=x[i], # 文本的X坐标y=data.iloc[i].sum() * 1.05, # 文本的Y坐标s=f"{data.iloc[i].name}: {data.iloc[i].sum()}", # 文本内容rotation= (i * 360)/data.shape[0], # 旋转角度rotation_mode="anchor", # 指定旋转模式horizontalalignment="left", # 水平对齐方式verticalalignment="center", # 垂直对齐方式)plt.show()
制作动画
先作个简单的动画,即按照绘制顺序,逐个实现动画效果:
- 创建自定义函数 update() 保存每一帧的绘图方法。
- for循环的子句移入动画函数,再调用matplotlib的FuncAnimation()方法。
- 第0帧时,先清空画布,再绘制。
- 最后一帧时,把文字画上去。
fig = plt.figure()
ax = fig.add_subplot(projection="polar") # 指定子图为极坐标系
ax.spines["polar"].set_visible(False) # 隐藏边框
ax.axes.xaxis.set_visible(False) # 隐藏X轴
ax.axes.yaxis.set_visible(False) # 隐藏Y轴
ax.set_ylim([0, 300])
x = [(i * np.pi * 2)/data.shape[0] for i in range(data.shape[0])] # 计算X轴
color_list = plt.colormaps['inferno'].resampled(48).colors# 动画函数
def update(i):if i == 0: ax.cla()ax.set_ylim([0, 300])bar = ax.bar(x=x,height=data.iloc[:, i],bottom=0 if i == 0 else data.iloc[:, 0:i].sum(axis=1), width=np.pi/data.shape[0], color=color_list[i*16:(i+1)*16],edgecolor="k",linewidth=1,)if i == 2:for i in range(data.shape[0]):plt.text(x=x[i], # 文本的X坐标y=data.iloc[i].sum() * 1.05, # 文本的Y坐标s=f"{data.iloc[i].name}: {data.iloc[i].sum()}", # 文本内容rotation= (i * 360)/data.shape[0], # 旋转角度rotation_mode="anchor", # 指定旋转模式horizontalalignment="left", # 水平对齐方式verticalalignment="center", # 垂直对齐方式)return bar anim = animation.FuncAnimation(fig=fig, # 指定画布对象func=update, # 指定更新方法frames=3, # 动画帧数,即画几次的意思,和一秒多少帧无关interval=720, # 每次绘画的间隔时间,单位是毫秒)
一层一层出现的方式有点奇怪,改成分柱子出现的比较好。即每次绘制一行数据柱,绘制16次。
fig = plt.figure()
ax = fig.add_subplot(projection="polar") # 指定子图为极坐标系
ax.spines["polar"].set_visible(False) # 隐藏边框
ax.axes.xaxis.set_visible(False) # 隐藏X轴
ax.axes.yaxis.set_visible(False) # 隐藏Y轴
ax.set_ylim([0, 300])
x = [(i * np.pi * 2)/data.shape[0] for i in range(data.shape[0])] # 计算X轴
color_list = plt.colormaps['inferno'].resampled(48).colors
bar = ax.bar(x, [0]*len(x), width=np.pi/data.shape[0])[0]# 动画函数
def update(i):if i == 0: ax.cla() # 清除画布ax.set_ylim([0, 300]) # 重新建立Y轴范围for j in range(3): # 分成三层画柱子bar = ax.bar(x = x[i], # X轴坐标height=data.iloc[i, j].sum(), # 每层柱子的高度bottom=0 if j==0 else data.iloc[i, 0:j].sum(), # 每层柱子的底width=np.pi/data.shape[0], # 柱子宽度color=color_list[j*16 + i], # 柱子颜色edgecolor="k", # 分隔线颜色linewidth=1, # 分隔线宽度)plt.text(x=x[i], # 标签的X轴坐标,与柱子相同y=data.iloc[i].sum()*1.05, # 标签的Y轴坐标,比柱子略高一点,避免重叠s=f"{data.iloc[i].name}: {data.iloc[i].sum()}", # 标签的文本内容rotation= (i * 360)/data.shape[0], # 旋转角度rotation_mode="anchor", # 指定旋转模式horizontalalignment="left", # 水平对齐方式verticalalignment="center", # 垂直对齐方式)return bar anim = animation.FuncAnimation(fig=fig, func=update, frames=16, interval=720)
结束
附件:完整代码
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as npplt.rcParams["font.sans-serif"] = ["SimHei"] # 指定中文字体
plt.rcParams['axes.unicode_minus'] = False # 指定负号等特殊符号兼容data = pd.DataFrame(np.random.randint(70, 100, size=(16, 3)),index=[i for i in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'][:16], columns=["l1", 'l2', 'l3'])fig = plt.figure()
ax = fig.add_subplot(projection="polar") # 指定子图为极坐标系
ax.spines["polar"].set_visible(False) # 隐藏边框
ax.axes.xaxis.set_visible(False) # 隐藏X轴
ax.axes.yaxis.set_visible(False) # 隐藏Y轴
ax.set_ylim([0, 300])
x = [(i * np.pi * 2)/data.shape[0] for i in range(data.shape[0])] # 计算X轴
color_list = plt.colormaps['inferno'].resampled(48).colors
bar = ax.bar(x, [0]*len(x), width=np.pi/data.shape[0])[0]# 动画函数
def update(i):if i == 0: ax.cla() # 清除画布ax.set_ylim([0, 300]) # 重新建立Y轴范围for j in range(3): # 分成三层画柱子bar = ax.bar(x = x[i], # X轴坐标height=data.iloc[i, j].sum(), # 每层柱子的高度bottom=0 if j==0 else data.iloc[i, 0:j].sum(), # 每层柱子的底width=np.pi/data.shape[0], # 柱子宽度color=color_list[j*16 + i], # 柱子颜色edgecolor="k", # 分隔线颜色linewidth=1, # 分隔线宽度)plt.text(x=x[i], # 标签的X轴坐标,与柱子相同y=data.iloc[i].sum()*1.05, # 标签的Y轴坐标,比柱子略高一点,避免重叠s=f"{data.iloc[i].name}: {data.iloc[i].sum()}", # 标签的文本内容rotation= (i * 360)/data.shape[0], # 旋转角度rotation_mode="anchor", # 指定旋转模式horizontalalignment="left", # 水平对齐方式verticalalignment="center", # 垂直对齐方式)return bar anim = animation.FuncAnimation(fig=fig, func=update, frames=16, interval=720)anim.save("./sample_02.gif") # 保存到GIF文件
这篇关于【可视化】Python绘制风车玫瑰图动画的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!