前言
对于用 Python 做可视化的用户而言,Plotly Express 是一个不错的选择:处理好数据后,几行代码就可以生成出常见的图形;JavaScript + HTML 网页的输出形式也允许图形在高分辨率下有良好的阅读体验。
最近在制作一批与胡桃工具箱项目相关的可视化图表,其中一张图是分析上传深渊数据的所有用户的 ID 段信息。它的数据来源于数据库,结构如下:
PrimaryId (int) | Uid (int) | Uploader (text) | UploadTime (timestamp) |
---|---|---|---|
218618 | 104xxxx62 | Snap Hutao | 1681597317 |
218619 | 244xxxx47 | Snap Hutao | 1681599869 |
218620 | 500xxxx51 | miao-plugin | 1681601283 |
使用 Plotly Express
使用 Plotly Express,我们可以使用以下代码来快速实现这张图表
# Generate data source
data = [{"UID": int(item[0][:3]), "Time": datetime.fromtimestamp(item[1]), "Uploader": item[2]} for item in list(sql_result)]
# Draw figure
fig = px.scatter(data_data, y="Time", x="UID", color="Uploader", color_discrete_map=uploader_color,
labels={
"UID": "Starting Number of UID",
"Time": "Datetime"
})
fig.show()
运行上述代码,我们就会得到这样一张散点图,但是它有一个明显的缺陷:虽然我们能通过这张图来获得一个大致的用户 UID 分布,但是由于国服、国际服、渠道服的 UID 构成结构,这张图的X轴有一个界限很大。即使 Plotly 通过 JavaScript 的方式实现了无损的自由缩放,但这仍然为读者带来不便并降低了故事性。
使用 Plotly Graph Objects
针对上面的问题,一个优化这张图的方法是将这幅散点图按服务器拆分成多张图。一个很直接了当的做法是生成多张图表,引入 Plotly Dash 库,然后在仪表盘设计中增加切换图表的功能。但在这个项目中,不同主题的图表位置是独立的,因此我们希望在这个图表中就实现切换功能。对此,Plotly 在 Figure.layout
中提供了一个 updatemenus
参数,实现了 dropdown 功能。
Plotly Go 对比 Plotly Ex
根据官方文档,下拉菜单功能只能和 Plotly Graph Objects (Plotly go
) 库相搭配。平时更常用的 Plotly Express (Plotly ex
) 库是基于 Plotly go 实现的快速制图库,其通过预先设置的流程、调用 Plotly go 方法将复杂的制图代码简化至数行之内。
简而言之, Plotly Go 是一个基础库,它更复杂、抽象但有更高的自定义性,它生成的对象是图表中具体的可视化组件; Plotly Ex 是一个简化的制图库,能快速地生成常见的可视化图表,它生成的对象是不同组件组成的一整个图表。对于下拉菜单功能而言,它实现的功能是对图表中各种组件进行操作,这也就不得不使用 Plotly Go 库了。
创建图表
使用 Graph Objects 创建图表的流程是首先初始化一个 go.Figure()
对象,然后通过 add_trace()
方法来增加图形。在这一步我们需要做几件事:
- 由于没有 Plotly Express 中
data_frame
一样的整体数据源参数,我们首先需要将dict[]
格式的数据重新序列化。 - 在 Plotly Express 中,
color="Uploader"
会指定 Plotly 将Uploader
列作为创建数据组(也就是图例组/ Legend Group)。在 Plotly Go 中,我们必须将每一组数据拆分开来,并分别添加进go.Figure
对象中,通过name
参数来命名对应的图例名称
数据处理
# 将 dict[] 转化为一个 DataFrame
df = pd.DataFrame(list(sql_result))
df.rename(columns={0: "UID", 1: "Time", 2: "Uploader"}, inplace=True)
df["Time"] = [datetime.fromtimestamp(x) for x in df["Time"]]
df.UID = df.UID.str.slice(stop=3).astype(int)
# 这里由于图例组并不多,直接写死,如果数据较多应通过遍历来实现
df_sh = df[df.Uploader == "Snap Hutao"]
df_miao = df[df.Uploader == "miao-plugin"]
df_sh_bm = df[df.Uploader == "Snap Hutao Bookmark"]
# 将 Legend Name 映射到对应的数据集上
df_dict = {
"Snap Hutao": df_sh,
"Snap Hutao Bookmark": df_sh_bm,
"miao-plugin": df_miao
}
构建图像
在这里,我们首先写好 uid_group
作为每个数据集的 UID 分组规则;创建对象;之后通过两层 for loop 来将所有相对应的数据添加进该对象中。
fig = go.Figure()
# CN/bilibili/EU/NA/Asia/(HK/MO/TW)
uid_group = [("1", "2"), "5", "6", "7", "8", "9"]
for uid in uid_group:
for k, v in df_dict.items():
fig.add_trace(
go.Scatter(
x=v[v.UID.astype(str).str.startswith(uid)]["UID"],
y=v[v.UID.astype(str).str.startswith(uid)]["Time"],
name=k,
legendgroup=k,
mode="markers",
marker=dict(color=v[v.UID.astype(str).str.startswith(uid)]["Uploader"].apply(lambda x: uploader_color[x])))
)
这一段代码在图表中创建了18个散点图形(6个服务器 * 3个 Uploader),实现了和 Plotly Express 一样的图表效果
创建下拉菜单
Plotly 下拉菜单是由 updatemenu
方法负责,由 method
参数具体定义下拉菜单中按钮对图表更新的方式,包括 restyle
, relayout
, update
和 animate
四种方法。
在这里,我们使用 update
方法,visible
参数来定义图表中18个图形是否显示。除此以外,更新 title_text
来更新图表的标题以适应变化的图像。
这一段代码实际就是 UI 设计,省略了重复的部分
button_layer_1_height = 1.08
fig.update_layout(showlegend=True, title="Uploader UID Information by Time",
title_x=0.5)
fig.update_layout(
updatemenus=[
dict(
buttons=list([
dict(
args=[{"visible": [True, True, True,
True, True, True,
True, True, True,
True, True, True,
True, True, True,
True, True, True
]},
dict(text="Uploader UID Information by Time (All Regions)")],
label="All",
method="update"
),
dict(
args=[{"visible": [True, True, True,
False, False, False,
False, False, False,
False, False, False,
False, False, False,
False, False, False
]},
dict(text="Uploader UID Information by Time (Mainland China)")],
label="China",
method="update"
),
dict(
args=[{"visible": [False, False, False,
True, True, True,
False, False, False,
False, False, False,
False, False, False,
False, False, False
]},
{"title_text": "Uploader UID Information by Time (bilibili @ Mainland China)"}],
label="bilibili",
method="update"
)
]),
direction="down",
pad={"r": 10, "t": 10},
showactive=True,
x=0.1,
xanchor="left",
y=button_layer_1_height,
yanchor="top"
),
]
)
其它可行的方法
更换数据源其实是一个可行且思路更加清晰的方案,但是在本文所使用的案例中,更换数据源会储存重复的数据,在数据较多的情况下会占用更多不必要的储存空间。如果使用这种方法,则在对应的按钮上对 x
和 y
参数重新指定相对应的数据源即可。
最终效果
转载请标注来源