目录

  1. 单张图
  2. 多张图
  3. Reference

Altair 是又一个 Python 绘图库,可交互,基于 VegaVega-Lite,官方称其为 Declarative Visualization in Python,声明式可视化绘图。在我看来这是一个比较轻量级的绘图库,可能和 Bokeh 是一类,相比 Plotly 要轻很多。由于其可交互的特性,当我们需要在博客上分享某些图时,读者阅读时要方便有效很多。

本文主要聚焦于如何在 hexo 这种静态博客中嵌入或者说显示 altair plot,但如何使用 altair 并不在本文讨论范围内。我之前也写过一篇文章讲如何嵌入 bokeh,感兴趣的话可以瞅上两眼。

下面我们就直入主题吧。

单张图

我们先从一个热力图说起。最近电视剧《开端》很热,反炸CP、司锅姨、今麦郎等梗也是非常多。我也是刚看完没多久,觉得确实很不错,国内相关题材上算是最好的一个了,但感觉最后一集实在是有些仓促……

话扯远了,说回正题。

我抓取了《开端》的豆瓣小组上的帖子,总共约 2.7 万篇。其中有一个字段是“最后回应时间”,表示该帖子最后一次被回复的时间。我们可以据此推断出在哪些时间段讨论比较热烈,所以这就是我们今天要绘制的热力图,横轴是一天中的 24 小时,纵轴是以天为单位的日期,日期范围是最早和最晚帖子的被回复日期(截至我抓取时 2 月 3 日)。

原始数据样例如下:

原始数据样例原始数据样例

然后我们先把 last_reply_time 拉出来,获取对应的 hour,按照天进行 resample 并统计每个小时内的贴子数,最终处理成 altair 需要的格式,即 x、y、z 各成一列:

1
2
3
4
5
6
7
8
9
grouped = df.resample('D', on='last_reply_time')
name2counter = {}
for name, g in grouped:
counter = Counter(dict.fromkeys(range(24), 0))
counter.update(g.last_reply_time.dt.hour)
name2counter[datetime.strftime(name, '%Y-%m-%d')] = counter
heatmap_data = pd.DataFrame(name2counter)
heatmap_data = heatmap_data.reset_index().melt(id_vars=['index'])
heatmap_data.columns = ['小时', '日期', '计数']

最终得到的数据样例如下:

小时 日期 计数
0 0 2022-01-02 0
1 1 2022-01-02 0
2 2 2022-01-02 0
3 3 2022-01-02 0
4 4 2022-01-02 0

这就是我们要传给 altair 的数据了,横轴是 小时,纵轴是 日期,颜色使用 计数

1
2
3
4
5
6
chart = alt.Chart(altair_data).mark_rect().encode(
x='小时:O', # 格式为 列名:数据类型,O 表示 ordinal,离散的有序数据
y='日期:O',
color='计数:Q', # Q 表示 quantitative,连续数据
tooltip=['日期', '小时', '计数']
).properties(title='《开端》豆瓣小组讨论热力图')

最终的效果图如下:

开端豆瓣小组讨论热力图开端豆瓣小组讨论热力图

可是我们如何将这个图放到我们的博客里呢?

最简单的方式就是导出为 PNG 或者 SVG,就像上面这样,可是这样的话就丢失了可交互性这个重要的特性了。所以最佳方案就是显示绘图的同时保留可交互性。

根据官方文档上的说法,可选的方案有导出为 JSON 或者 HTML。前者需要配合 vegaEmbed 使用,后者也需要,只不过已经内置在 HTML 中了。由于前者需要将 JSON 文件托管在某个地方,因此我们不选用这种方案。我们将使用 HTML 的方式。

我们可以使用 chart.save('chart.html') 来导出到 HTML 文件,下面是一个该文件的样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
</head>
<body>
<div id="vis"></div>
<script type="text/javascript">
var spec = {
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"config": {
"view": {
"continuousHeight": 300,
"continuousWidth": 400
}
},
"data": {
"url": "https://vega.github.io/vega-datasets/data/cars.json"
},
"encoding": {
"color": {
"field": "Origin",
"type": "nominal"
},
"x": {
"field": "Horsepower",
"type": "quantitative"
},
"y": {
"field": "Miles_per_Gallon",
"type": "quantitative"
}
},
"mark": "point"
};
var opt = {"renderer": "canvas", "actions": false};
vegaEmbed("#vis", spec, opt);
</script>
</body>
</html>

按理说我们需要让有 altair plot 的页面加载 <script> 标签中的文件。但我们不能直接将这个 <script> 标签和 <body> 标签中的内容直接复制到 markdown 文件中,这样是没有效果的。我们需要先找到生成 <head> 标签的地方,这个不同主题位置可能不同。然后修改那里的程序,将我们的 <script> 标签加进去即可。

当然我们希望在有 altair plot 的页面加载这些 js 程序,按需加载,避免拖慢其他页面的加载速度。所以我们可以加一个设置vega。当 vega: true 时才加载这些 js,默认为 false 不加载。

综合来说,步骤如下:

  1. 找到生成 <head> 标签的地方。我目前用的主题是 tranquilpeak,我这个主题生成 <head> 的程序是在 tranquilpeak/layout/_partial/head.ejs 中。
  2. 添加 <script> 标签。我们找到上述文件,在最后的 </head> 上面加入如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <% if (page.vega) { %>
    <style>
    .vega-actions a {
    margin-right: 12px;
    color: #757575;
    font-weight: normal;
    font-size: 13px;
    }
    .error {
    color: red;
    }
    </style>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm//[email protected]"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm//[email protected]"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm//[email protected]"></script>
    <% } %>
  3. 在博文中加入绘图代码。然后写博客时,在上面的 metadata 里加上 vega: true,然后将导出的 HTML 文件中的 <body> 内容直接复制到想要显示绘图的位置,如本文:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    ---
    title: 嵌入 Altair 绘图到 Hexo 博客中
    date: 2022-02-04 14:19:00
    tags:
    - hexo
    - Python
    - DataViz
    vega: true
    ---

    <!-- 其他文本 -->

    <!-- 这里放 HTML 文件中的 <body> 内容,一般是一个 div 和一个 script。-->

    <!-- 其他文本 -->

这样就可以显示出 altair plot 了,并且鼠标 hover 可以显示当前点的信息,保留了可交互性:

多张图

从上面的样例可以看出,绘图其实是显示在 id="vis" 的 div 中。而一个 HTML 中 id 不能重名。所以当我们有多张图需要显示时,我们必须更改第二以及后面的图的 id,比如我们可以直接递增,如 vis2。具体来说,要改的地方有 3 个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="vis2"></div> <!-- 修改 1 -->
<script>
(function(vegaEmbed) {
var spec = ... // 此处太长,暂时省略
var embedOpt = {"mode": "vega-lite"};

function showError(el, error){
el.innerHTML = ('<div class="error" style="color:red;">'
+ '<p>JavaScript Error: ' + error.message + '</p>'
+ "<p>This usually means there's a typo in your chart specification. "
+ "See the javascript console for the full traceback.</p>"
+ '</div>');
throw error;
}
const el = document.getElementById('vis2'); // 修改 2
vegaEmbed("#vis2", spec, embedOpt) // 修改 3
.catch(error => showError(el, error));
})(vegaEmbed);

下面我们将上面的热力图稍微改下,将 计数 视为 O 类型数据,即离散有序数据,此时便会以离散的 colorscale 来显示数据:

Reference