Lee's Space Station

嵌入 bokeh 绘图到 hexo 博客中

2018/03/15 Share

Bokeh 是一个 Python 交互式可视化库,主要用于绘制面向浏览器的交互式图表,D3.js 的风格,Anaconda 出品。关注这个库很久了,我知道的 Python 中用于创建这种交互式图表的库好用的不是很多,还有个 plotly 也不错,同时支持 Python 和 R,只不过是收费的,免费的有很多限制。

使用 bokeh 你可以绘制很多具有交互效果的图,例如直接拖动图、hover 效果、滚轮缩放、box 缩放、按钮和滑块等其他丰富的组件。这里放一个我写本文的时候发现的以 Stephen Hawking (R.I.P)的论文和书籍成就数据为基础绘制的图,这也是使用 bokeh 绘制的,完整文章可见 Stephen Hawking - An interactive history of his career

sh

话说正题,本文的目的不是让你学会怎么使用 bokeh 画图,而是如何把画好的图嵌入到网页或者博客中,例如 hexo 博客。而这一部分我将分两部分说:带有 widgets 的图(No Widgets)和不带 widgets 的图(With Widgets)。这里说的 widgets 指按钮、复选框、单选框、下拉菜单和滑块等小部件。引用 Bokeh 官方对 widgets 的解释:

User interface elements outside of a Bokeh plot such as sliders, drop down menus, buttons, etc. Events and updates from widgets can inform additional computations, or cause Bokeh plots to update. Widgets can be used in both standalone applications or with the Bokeh server. For examples and information, see Adding Interactions.

根据 Bokeh 官方的描述,我们有三种方法可以将图嵌入到网页中:

  • HTML files
  • Components
  • Autoload Scripts

Note:

  • Bokeh Server 不在此文讨论范围中。
  • 关于如何使用 Bokeh,可能的话以后再写篇来说。

效果

先来看下嵌入后的效果:

No widgets:

With widgets:

HTML files

这是最直接不容易出错的方法,就是直接使用生成的 HTML 文件。

假设我们的博客文章使用 Markdown 编写,我们首先使用下面代码生成所需要的 HTML 文件(来自 slider.py — Bokeh 0.12.14 documentation):

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
43
44
45
46
47
48
49
50
51
52
53
import numpy as np

from bokeh.layouts import row, widgetbox
from bokeh.models import CustomJS, Slider
from bokeh.plotting import figure, output_file, show, ColumnDataSource

x = np.arange(-5, 5, 0.1)
y = np.sin(x)

source = ColumnDataSource(data=dict(x=x, y=y))

plot = figure(y_range=(-10, 10), plot_width=400, plot_height=400)

plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)

callback = CustomJS(args=dict(source=source), code="""
var data = source.data;
var A = amp.value;
var k = freq.value;
var phi = phase.value;
var B = offset.value;
x = data['x']
y = data['y']
for (i = 0; i < x.length; i++) {
y[i] = B + A*Math.sin(k*x[i]+phi);
}
source.change.emit();
""")

amp_slider = Slider(start=0.1, end=10, value=1, step=.1,
title="Amplitude", callback=callback)
callback.args["amp"] = amp_slider

freq_slider = Slider(start=0.1, end=10, value=1, step=.1,
title="Frequency", callback=callback)
callback.args["freq"] = freq_slider

phase_slider = Slider(start=0, end=6.4, value=0, step=.1,
title="Phase", callback=callback)
callback.args["phase"] = phase_slider

offset_slider = Slider(start=-5, end=5, value=0, step=.1,
title="Offset", callback=callback)
callback.args["offset"] = offset_slider

layout = row(
plot,
widgetbox(amp_slider, freq_slider, phase_slider, offset_slider),
)

output_file("slider.html", title="slider.py example")

show(layout)

运行结束后,会在当前目录生成 slider.html,并在浏览器中自动打开这个网页。我们所需要的代码是这个 HTML 文件中 <div class="bk-root"> 这个 div 块以及 <script type="application/json" id="id"><script type="text/javascript"> 中的内容,也是 body 中的内容,如下图,内容太长,我就只圈出了标签名字:

然后我们直接复制这三个标签中的内容到博文的 md 文件中即可。

Components

这个方法是直接生成绘图所需用到的 scriptdiv 块,然后直接复制到博文的 md 文件中,只不过这里得到的是一个 script 和一个 div。但是这种方法极其容易出错,所得到的 scriptdiv 内容不太对,在我处理了一个又一个报错后依然不行,错误率很高,说明还不太完善,错误多地我都不知道怎么去提 issue,从何提起 😆。最后果断放弃了这个方法,有成功的同学可以在下方留言告知,谢谢 😄

简单说下这个方法的程序逻辑:

  1. 使用 Bokeh 绘图。
  2. 使用 bokeh.embed.components 方法得到绘图对象对应的 scriptdiv 标签,例如

    1
    2
    3
    4
    5
    # some other codes
    p = figure()
    p.line(x, y)
    # some other plot codes
    script, div = components(p)
  3. 复制得到的 scriptdiv 到博文的 md 文件中,Done。

Autoload Scripts

这是使用自动加载的脚本,需要把绘图脚本放在自己的服务器上。这里给出一个官方的例子:

1
2
3
4
5
6
7
8
from bokeh.resources import CDN
from bokeh.plotting import figure
from bokeh.embed import autoload_static

plot = figure()
plot.circle([1,2], [3,4])

js, tag = autoload_static(plot, CDN, "some/path")

所以你需要把生成的 js 放到 some/path 里。

Conclusion

总的来说,还是第一种方法靠谱。通用的步骤就是:

  1. 创建绘图对象。
  2. 得到绘图所需的 HTML 代码。
  3. 复制这些代码到 md 文件。

最终的 md 文件大概是下面这个样子:

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
---
title: 你的标题
date: 日期
tags: 你的标签
---
<link
href="https://cdn.pydata.org/bokeh/release/bokeh-0.12.14.min.css"
rel="stylesheet" type="text/css">
<link
href="https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.14.min.css"
rel="stylesheet" type="text/css">
<link
href="https://cdn.pydata.org/bokeh/release/bokeh-tables-0.12.14.min.css"
rel="stylesheet" type="text/css">

<script src="https://cdn.pydata.org/bokeh/release/bokeh-0.12.14.min.js"></script>
<script src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.12.14.min.js"></script>
<script src="https://cdn.pydata.org/bokeh/release/bokeh-tables-0.12.14.min.js"></script>
……
这里是你的一些博客内容
……

在需要显示图的时候,把绘图所需代码放复制到这

……
这里是你的一些博客内容
……

其中 -widgets 在图中有 widgets 的情况使用,-tables 在有 table 的情况下使用。

写的不太好,我也是琢磨了好长时间,有问题欢迎在下面留言交流 😄。

References

CATALOG
  1. 1. 效果
  2. 2. HTML files
  3. 3. Components
  4. 4. Autoload Scripts
  5. 5. Conclusion
  6. 6. References