在模仿中精进数据可视化08:哪个省份的学子是熬夜冠军?

共 5252字,需浏览 11分钟

 ·

2021-04-02 23:25

9e2e8c1ce26af15a18e8f938e7bf2cc2.webp

添加微信号"CNFeffery"加入技术交流群

本文完整代码及数据已上传至我的Github仓库https://github.com/CNFeffery/FefferyViz

1 简介

大家好~热衷于钻研复刻优秀数据可视化作品的费老师我🧐,最近的业余时间主要沉迷于撰写「Python+Dash快速web应用开发」系列文章,「在模仿中精进数据可视化」系列文章有两个月没更新了,今天继续捡起来🥳。

我们今天要复刻的数据可视化作品,是前段时间在微博刷屏的下面这张网易数读的作品,基于作业帮的用户画像数据对哪个地方的学习是“熬夜冠军”进行了可视化表达:

8b83f17ce5522a2a018609daf13c5c23.webp图1

而下面我们就来基于matplotlib,复刻出这幅作品~

2 复刻过程

2.1 拆解主要视觉元素

其实这幅作品有些类似于我们这个系列文章开篇那一期「贝壳研究院」的图,都是以半边扇形为主体构图元素,在极坐标中对数据进行一系列表达,而今天的案例我们构建扇形图表选择的是matplotlib中的「极坐标系」,非常简单方便。

按照惯例,我们先来“肢解”一下这幅图的主要构图元素:

  • 「多子图组合」

这幅作品中主要可以分为「主体扇形」子图和右下角略微“出墙来”的「点缀扇形」子图构成,我们可以使用plt.subplots()创建底层画板之后,再分别用fig.add_axes(rect, polar=True)来在不同位置插入不同大小的上述子图;

  • 「主体扇形底色交替填充」

首先我们可以观察到在这幅图的「主体扇形」右半圆中,背景色是由颜色交替切换的子扇形区域构成的,且仔细观察可以发现子扇形之间的交界处是有白色边界线的。

这部分我们就可以使用到matplotlib中的fill_between()区域色彩填充功能,先生成指定数量的右半圆「等弧度」集合,其作用于「极坐标系」时传入的第一个参数为「角度范围」,第二个参数为「填充起点半径值」,第三个参数为「填充终点半径值」,白色交界线直接使用plot()绘制直线即可;

  • 「极坐标柱状图与中央虚线」

在上述构建的交替底色的基础上,我们继续来将每个地区的数值映射为极坐标柱状图的柱体高度,注意,这里的柱体颜色也是交替切换的,并且需要给每个柱体中央添加虚线点缀;

  • 「主体扇形多规则文字标注」

在原作品中的「地区」「深夜学习活跃指数」在角度旋转上有三种规则方式,我们可以在一开始构建数据时针对不同排名的地区,打上用于区别类型的标签,好在之后的绘图过程中分别控制角度旋转计算方式:

0e7c7b41efd045c3e256ce64ced92b71.webp图2634d1225b690218b531cad3b29c3f8c1.webp图3

至于其他的点缀元素,就不详细说了,文章结尾的绘图代码里都有详细的注释。

2.2 完成复刻

在上述拆解的基础上,我们就可以充分运用弧度跟角度之间的转换,配合matplotlibnumpy来复刻出下面的效果啦,最后裁剪出的作品如下,是不是相当还原呢~:

d8604c2225770299e6c4eb0e504265a9.webp图4

再放一张没有拆掉“脚手架”(坐标轴线)的效果,你就会更加清楚我的构图逻辑了:

f7689c297be02410106e2d9a1a4ed8f6.webp

图5

完整代码如下,如有疑问欢迎在评论区与我进行交流:

# 生成每份子扇形区域的两边夹角
# 这里[::-1]是为了迎合matplotlib极坐标默认的角度位置
theta_group = (np.linspace(-0.50.532)*np.pi)[::-1]

# 创建图床和原始axes对象
fig, ax = plt.subplots(figsize=(1010))

############################
# 主体部分
############################

# 向原始图床中插入极坐标系新axes对象
ax1 = fig.add_axes([-0.5011], polar=True)

# 绘制右半边扇形区域最底层错落的色带填充
for idx, group in enumerate(fc.pairwise(theta_group)):
    
    # 当下标为偶数时,填充#e3effd色
    if idx % 2 == 0:
        ax1.fill_between(group, 0.753, facecolor='#e3effd')
    # 当下标为奇数时,填充#fafbff色
    else:
        ax1.fill_between(group, 0.753, facecolor='#fafbff')

# 绘制每份子扇形区域的中央虚线
for idx, group in enumerate(fc.pairwise(theta_group)):
    theta = (group[0] + group[1]) / 2
    ax1.plot([theta, theta], [0.752.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], [13], 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]) / 22.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]) / 22.79], 
                     va='center', ha='center', zorder=10,
                     rotation=angle,
                     fontsize=10,
                     color=value_color,
                     fontproperties='Times New Roman')

# 绘制外围黑色虚线
ax1.plot(np.linspace(-0.380.451000)*np.pi, [3.275]*1000
         linestyle='dashed', color='#595655', linewidth=0.75)

# 添加“0~2点学习活跃指数”标注
ax1.annotate('\n'.join(list('0~2点学习活跃指数')), 
             xy=[03.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.711], polar=True)

theta_group2 = (np.linspace(-1.5-0.532)*np.pi)[::-1]

# 绘制左半边扇形区域最底层错落的色带填充
for idx, group in enumerate(fc.pairwise(theta_group2)):
    
    # 当下标为偶数时,填充#e3effd色
    if idx % 2 == 0:
        ax2.fill_between(group, 0.753, facecolor='#e3effd')
    # 当下标为奇数时,填充#fafbff色
    else:
        ax2.fill_between(group, 0.753, 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')

以上就是本文的全部内容,欢迎在评论区与我进行交流讨论~

f33b525465fb7e1f006c58e8e62f91c3.webp

加入知识星球【我们谈论数据科学】

300+小伙伴一起学习!








· 推荐阅读 ·

Python处理图像五个有趣场景,很实用!

秀啊,90行Python代码开发个人云盘应用

炫酷!纯Python开发LOL英雄信息查询平台


浏览 28
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报