在模仿中精进数据可视化08:哪个省份的学子是熬夜冠军?
添加微信号"CNFeffery"加入技术交流群
❝1 简介本文完整代码及数据已上传至我的
❞Github
仓库https://github.com/CNFeffery/FefferyViz
大家好~热衷于钻研复刻优秀数据可视化作品的费老师我🧐,最近的业余时间主要沉迷于撰写「Python+Dash快速web应用开发」系列文章,「在模仿中精进数据可视化」系列文章有两个月没更新了,今天继续捡起来🥳。
我们今天要复刻的数据可视化作品,是前段时间在微博刷屏的下面这张网易数读
的作品,基于作业帮
的用户画像数据对哪个地方的学习是“熬夜冠军”进行了可视化表达:
而下面我们就来基于matplotlib
,复刻出这幅作品~
2.1 拆解主要视觉元素
其实这幅作品有些类似于我们这个系列文章开篇那一期「贝壳研究院」的图,都是以半边扇形为主体构图元素,在极坐标中对数据进行一系列表达,而今天的案例我们构建扇形图表选择的是matplotlib
中的「极坐标系」,非常简单方便。
按照惯例,我们先来“肢解”一下这幅图的主要构图元素:
- 「多子图组合」
这幅作品中主要可以分为「主体扇形」子图和右下角略微“出墙来”的「点缀扇形」子图构成,我们可以使用plt.subplots()
创建底层画板之后,再分别用fig.add_axes(rect, polar=True)
来在不同位置插入不同大小的上述子图;
- 「主体扇形底色交替填充」
首先我们可以观察到在这幅图的「主体扇形」右半圆中,背景色是由颜色交替切换的子扇形区域构成的,且仔细观察可以发现子扇形之间的交界处是有白色边界线的。
这部分我们就可以使用到matplotlib
中的fill_between()
区域色彩填充功能,先生成指定数量的右半圆「等弧度」集合,其作用于「极坐标系」时传入的第一个参数为「角度范围」,第二个参数为「填充起点半径值」,第三个参数为「填充终点半径值」,白色交界线直接使用plot()
绘制直线即可;
- 「极坐标柱状图与中央虚线」
在上述构建的交替底色的基础上,我们继续来将每个地区的数值映射为极坐标柱状图的柱体高度,注意,这里的柱体颜色也是交替切换的,并且需要给每个柱体中央添加虚线点缀;
- 「主体扇形多规则文字标注」
在原作品中的「地区」及「深夜学习活跃指数」在角度旋转上有三种规则方式,我们可以在一开始构建数据时针对不同排名的地区,打上用于区别类型的标签,好在之后的绘图过程中分别控制角度旋转计算方式:
图2图3至于其他的点缀元素,就不详细说了,文章结尾的绘图代码里都有详细的注释。
2.2 完成复刻
在上述拆解的基础上,我们就可以充分运用弧度跟角度之间的转换,配合matplotlib
和numpy
来复刻出下面的效果啦,最后裁剪出的作品如下,是不是相当还原呢~:
再放一张没有拆掉“脚手架”(坐标轴线)的效果,你就会更加清楚我的构图逻辑了:
图5完整代码如下,如有疑问欢迎在评论区与我进行交流:
# 生成每份子扇形区域的两边夹角
# 这里[::-1]是为了迎合matplotlib极坐标默认的角度位置
theta_group = (np.linspace(-0.5, 0.5, 32)*np.pi)[::-1]
# 创建图床和原始axes对象
fig, ax = plt.subplots(figsize=(10, 10))
############################
# 主体部分
############################
# 向原始图床中插入极坐标系新axes对象
ax1 = fig.add_axes([-0.5, 0, 1, 1], polar=True)
# 绘制右半边扇形区域最底层错落的色带填充
for idx, group in enumerate(fc.pairwise(theta_group)):
# 当下标为偶数时,填充#e3effd色
if idx % 2 == 0:
ax1.fill_between(group, 0.75, 3, facecolor='#e3effd')
# 当下标为奇数时,填充#fafbff色
else:
ax1.fill_between(group, 0.75, 3, facecolor='#fafbff')
# 绘制每份子扇形区域的中央虚线
for idx, group in enumerate(fc.pairwise(theta_group)):
theta = (group[0] + group[1]) / 2
ax1.plot([theta, theta], [0.75, 2.68], linestyle='--', color='#9fa0a0', linewidth=0.25)
# 绘制极坐标柱状图,分别占据每份子扇形区域的对应外扩长度
for idx, group in enumerate(fc.pairwise(theta_group)):
theta = (group[0] + group[1]) / 2
ax1.bar([theta], [2.25*data.at[idx, '深夜学习活跃指数']*0.01],
width=[np.pi / 32], bottom=0.75,
# 对下标分别为偶数与奇数的扇形绘制不同颜色
facecolor='#6785f2' if idx % 2 != 0 else '#7171fe',
edgecolor='white', linewidth=0.1, alpha=0.95, zorder=9)
# 绘制子扇形区域之间交界处的白色边界
for theta in theta_group:
ax1.plot([theta, theta], [1, 3], color='white', linewidth=0.2)
def rotate_text(text, group, method):
if method == 1:
return text, ((group[0] + group[1]) * 0.5 / np.pi) * 180 - 90
elif method == 2:
return '\n'.join(list(text)), ((group[0] + group[1]) * 0.5 / np.pi) * 180
elif method == 3:
return text, ((group[0] + group[1]) * 0.5 / np.pi) * 180 - 90 + 180
# 地区+数值文字标注
for idx, group in enumerate(fc.pairwise(theta_group[::-1])):
# 控制向data表的索引不越界
if idx < 31:
# 控制第一名的特殊字体颜色
if data.at[30-idx, '地区'] == '江苏':
text_color, value_color = 'white', 'white'
else:
text_color, value_color = 'black', '#595757'
# 利用前面定义的自编函数生成对应的文字与旋转角度
text, angle = rotate_text(data.at[30-idx, '地区'], group, method=data.at[30-idx, '文字排布'])
# 标注地区名称
ax1.annotate(text, xy=[(group[0]+group[1]) / 2, 2.925],
va='center', ha='center', zorder=10,
color=text_color,
rotation=angle,
fontsize=11)
# 标注深夜学习活跃指数
ax1.annotate(re.sub('\.$', '', str(data.at[30-idx, '深夜学习活跃指数'])[:4]),
xy=[(group[0]+group[1]) / 2, 2.79],
va='center', ha='center', zorder=10,
rotation=angle,
fontsize=10,
color=value_color,
fontproperties='Times New Roman')
# 绘制外围黑色虚线
ax1.plot(np.linspace(-0.38, 0.45, 1000)*np.pi, [3.275]*1000,
linestyle='dashed', color='#595655', linewidth=0.75)
# 添加“0~2点学习活跃指数”标注
ax1.annotate('\n'.join(list('0~2点学习活跃指数')),
xy=[0, 3.21],
va='center',
ha='center',
ma='center',
zorder=10,
rotation=0,
fontsize=11,
color='black',
fontproperties='Microsoft Yahei',
fontweight='bold',
bbox=dict(boxstyle="round", fc="white", ec="white", alpha=1))
############################
# 右下角点缀
############################
# 向原始图床中插入极坐标系新axes对象
ax2 = fig.add_axes([0.25, -0.7, 1, 1], polar=True)
theta_group2 = (np.linspace(-1.5, -0.5, 32)*np.pi)[::-1]
# 绘制左半边扇形区域最底层错落的色带填充
for idx, group in enumerate(fc.pairwise(theta_group2)):
# 当下标为偶数时,填充#e3effd色
if idx % 2 == 0:
ax2.fill_between(group, 0.75, 3, facecolor='#e3effd')
# 当下标为奇数时,填充#fafbff色
else:
ax2.fill_between(group, 0.75, 3, facecolor='#fafbff')
# 紧凑布局
fig.tight_layout(pad=0)
# 关闭所有axes的坐标轴线
ax.axis('off')
ax1.axis('off')
ax2.axis('off')
# 导出为图片
fig.savefig('图4.png', dpi=500, bbox_inches='tight', pad_inches=0, facecolor='white')
以上就是本文的全部内容,欢迎在评论区与我进行交流讨论~
加入知识星球【我们谈论数据科学】
300+小伙伴一起学习!
· 推荐阅读 ·