点击关注我的Telegram群组和微信公众号

MENU

基于 Plotly Graph Objects 库制作含下拉菜单的可视化图表

2023 年 05 月 31 日 • 阅读: 916 • 技术,Python,分析

前言

对于用 Python 做可视化的用户而言,Plotly Express 是一个不错的选择:处理好数据后,几行代码就可以生成出常见的图形;JavaScript + HTML 网页的输出形式也允许图形在高分辨率下有良好的阅读体验。

最近在制作一批与胡桃工具箱项目相关的可视化图表,其中一张图是分析上传深渊数据的所有用户的 ID 段信息。它的数据来源于数据库,结构如下:

PrimaryId (int)Uid (int)Uploader (text)UploadTime (timestamp)
218618104xxxx62Snap Hutao1681597317
218619244xxxx47Snap Hutao1681599869
218620500xxxx51miao-plugin1681601283

使用 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()

Plotly Express 效果图

运行上述代码,我们就会得到这样一张散点图,但是它有一个明显的缺陷:虽然我们能通过这张图来获得一个大致的用户 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()方法来增加图形。在这一步我们需要做几件事:

  1. 由于没有 Plotly Express 中 data_frame 一样的整体数据源参数,我们首先需要将 dict[] 格式的数据重新序列化。
  2. 在 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, updateanimate 四种方法。

在这里,我们使用 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"
        ),
    ]
)

其它可行的方法

更换数据源其实是一个可行且思路更加清晰的方案,但是在本文所使用的案例中,更换数据源会储存重复的数据,在数据较多的情况下会占用更多不必要的储存空间。如果使用这种方法,则在对应的按钮上对 xy 参数重新指定相对应的数据源即可。

最终效果

最终效果图

返回文章列表 文章二维码 打赏
本页链接的二维码
打赏二维码