本文完整代碼及數據已上傳至我的
Github
倉庫https://github.com/CNFeffery/FefferyViz
1 簡介
大家好~熱衷於鑽研復刻優秀數據可視化作品的費老師我🧐,最近的業余時間主要沉迷於撰寫Python+Dash快速web應用開發系列文章,在模仿中精進數據可視化系列文章有兩個月沒更新了,今天繼續撿起來🥳。
我們今天要復刻的數據可視化作品,是前段時間在微博刷屏的下面這張網易數讀
的作品,基於作業幫
的用戶畫像數據對哪個地方的學習是“熬夜冠軍”進行了可視化表達:

而下面我們就來基於matplotlib
,復刻出這幅作品~
2 復刻過程
2.1 拆解主要視覺元素
其實這幅作品有些類似於我們這個系列文章開篇那一期貝殼研究院的圖,都是以半邊扇形為主體構圖元素,在極坐標中對數據進行一系列表達,而今天的案例我們構建扇形圖表選擇的是matplotlib
中的極坐標系,非常簡單方便。
按照慣例,我們先來“肢解”一下這幅圖的主要構圖元素:
- 多子圖組合
這幅作品中主要可以分為主體扇形子圖和右下角略微“出牆來”的點綴扇形子圖構成,我們可以使用plt.subplots()
創建底層畫板之后,再分別用fig.add_axes(rect, polar=True)
來在不同位置插入不同大小的上述子圖;
- 主體扇形底色交替填充
首先我們可以觀察到在這幅圖的主體扇形右半圓中,背景色是由顏色交替切換的子扇形區域構成的,且仔細觀察可以發現子扇形之間的交界處是有白色邊界線的。
這部分我們就可以使用到matplotlib
中的fill_between()
區域色彩填充功能,先生成指定數量的右半圓等弧度集合,其作用於極坐標系時傳入的第一個參數為角度范圍,第二個參數為填充起點半徑值,第三個參數為填充終點半徑值,白色交界線直接使用plot()
繪制直線即可;
- 極坐標柱狀圖與中央虛線
在上述構建的交替底色的基礎上,我們繼續來將每個地區的數值映射為極坐標柱狀圖的柱體高度,注意,這里的柱體顏色也是交替切換的,並且需要給每個柱體中央添加虛線點綴;
- 主體扇形多規則文字標注
在原作品中的地區及深夜學習活躍指數在角度旋轉上有三種規則方式,我們可以在一開始構建數據時針對不同排名的地區,打上用於區別類型的標簽,好在之后的繪圖過程中分別控制角度旋轉計算方式:


至於其他的點綴元素,就不詳細說了,文章結尾的繪圖代碼里都有詳細的注釋。
2.2 完成復刻
在上述拆解的基礎上,我們就可以充分運用弧度跟角度之間的轉換,配合matplotlib
和numpy
來復刻出下面的效果啦,最后裁剪出的作品如下,是不是相當還原呢~:

再放一張沒有拆掉“腳手架”(坐標軸線)的效果,你就會更加清楚我的構圖邏輯了:

完整代碼如下,如有疑問歡迎在評論區與我進行交流:
# 生成每份子扇形區域的兩邊夾角
# 這里[::-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')
以上就是本文的全部內容,歡迎在評論區與我進行交流討論~