网页#

本章探讨了将独立 Bokeh 文档和 Bokeh 应用程序嵌入网页的多种方法。首先,以下是独立文档与应用程序的不同之处

独立文档

这些文档不需要 Bokeh 服务器来工作。它们可能具有许多工具和交互,例如自定义 JavaScript 回调,但除此之外,它们仅仅是 HTML、CSS 和 JavaScript。这些文档可以作为单个大型文档或一组具有单独模板的子组件嵌入到其他 HTML 页面中。

Bokeh 应用程序

这些应用程序需要 Bokeh 服务器才能工作。拥有 Bokeh 服务器可以让您将事件和工具连接到在服务器上执行的实时 Python 回调。有关创建和运行 Bokeh 应用程序的更多信息,请参见 Bokeh 服务器.

独立文档#

本节介绍发布和嵌入独立 Bokeh 文档的不同方法。

HTML 文件#

Bokeh 可以使用 file_html() 函数为 Bokeh 文档生成完整的 HTML 页面。此函数可以从自己的通用模板或您提供的模板中创建 HTML 文档。这些 HTML 文件包含绘图数据,并且完全可移植,同时仍然为您的绘图提供交互式工具(平移、缩放等)。以下是一个示例

from bokeh.plotting import figure
from bokeh.resources import CDN
from bokeh.embed import file_html

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

html = file_html(plot, CDN, "my plot")

您可以使用标准 Python 文件操作将返回的 HTML 文本保存到文件。您还可以为 HTML 输出提供自己的模板,并传递自定义或附加模板变量。有关更多详细信息,请参见 file_html() 文档。

这是一种生成 HTML 文件的低级、显式方法,对于 Flask 应用程序等 Web 应用程序非常有用。

在使用 bokeh.plotting 接口的脚本和 Jupyter 笔记本中,您可以将 output_file() 函数与 show()save() 函数一起调用。 show() 函数创建 HTML 文档并在 Web 浏览器中显示,而 save() 函数创建 HTML 文档并将其保存到本地。

JSON 项目#

Bokeh 还可以提供 BokehJS 可以用来在指定的 <div> 中呈现独立 Bokeh 文档的 JSON 数据。 json_item() 函数接受 Bokeh 模型(例如,绘图)和目标 <div> 的可选 ID。

p = figure()
p.circle(x, y)

item_text = json.dumps(json_item(p, "myplot"))

然后, embed_item() 函数可以在网页上使用此输出

item = JSON.parse(item_text);
Bokeh.embed.embed_item(item);

这将在 ID 为 “myplot”<div> 中呈现绘图。

您也可以在调用 json_item() 时省略目标 ID

p = figure()
p.circle(x, y)

item_text = json.dumps(json_item(p)) # no target ID given

然后您可以在 JavaScript 中指定 ID

item = JSON.parse(item_text);
Bokeh.embed.embed_item(item, "myplot");

以下是从 /plot 端点提供 Bokeh JSON 项目的 Flask 应用程序的更完整示例

@app.route('/plot')
def plot():
    p = make_plot('petal_width', 'petal_length')
    return json.dumps(json_item(p, "myplot"))

这会生成如下所示的 JavaScript 代码

<script>
fetch('/plot')
    .then(function(response) { return response.json() })
    .then(function(item) { return Bokeh.embed.embed_item(item) })
</script>

或者,使用现代语法,如下所示

<script>
const response = await fetch('/plot')
const item = await response.json()
Bokeh.embed.embed_item(item)
</script>

有关完整示例,请参见 examples/output/apis/json_item.py.

组件#

您还可以让 Bokeh 返回独立文档的单个组件,以便使用 components() 函数逐个嵌入它们。此函数返回一个 <script>,其中包含绘图的数据,并提供目标 <div> 来显示绘图视图。您可以在 HTML 文档中随意使用这些元素。

from bokeh.plotting import figure
from bokeh.embed import components

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

script, div = components(plot)

返回的 <script> 将类似于以下内容

<script type="text/javascript">
    (function() {
  const fn = function() {
    Bokeh.safely(function() {
      const docs_json = { DOCUMENT DATA HERE };
      const render_items = [{
        "docid":"6833819f-9b5b-4904-821e-3f5eec77de9b",
        "elementid":"9574d123-9332-4b5f-96cc-6323bef37f40",
        "modelid":"7b328b27-9b14-4f7b-a5d8-0138bc7b0f59"
      }];

      Bokeh.embed.embed_items(docs_json, render_items);
    });
  };
  if (document.readyState != "loading") fn();
  else document.addEventListener("DOMContentLoaded", fn);
})();

</script>

请注意,Jupyter 笔记本不允许在同一个笔记本单元格中使用 components()show() 函数。

docs_json 包含所有数据以及绘图或小部件对象(此处省略以简明起见)。生成的 <div> 类似于以下内容

<div id="9574d123-9332-4b5f-96cc-6323bef37f40"></div>

您可以在 HTML 文档中插入或模板化此脚本及其配套的 <div>,并且当脚本执行时,您的绘图将替换 <div>

要使其正常工作,您首先需要加载 BokehJS,无论是本地加载还是从内容交付网络 (CDN) 加载。要从 CDN 加载 BokehJS,请将以下行添加到您的 HTML 文档或模板中,并将 x.y.z 替换为相应的版本

<script src="https://cdn.bokeh.org/bokeh/release/bokeh-x.y.z.min.js"
        crossorigin="anonymous"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-x.y.z.min.js"
        crossorigin="anonymous"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-x.y.z.min.js"
        crossorigin="anonymous"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-gl-x.y.z.min.js"
        crossorigin="anonymous"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-x.y.z.min.js"
        crossorigin="anonymous"></script>

只有 Bokeh 核心库 bokeh-x.y.z.min.js 始终是必需的。其他脚本是可选的,只有在您想要使用相应的特性时才需要包含它们

  • 如果使用任何 Bokeh 小部件,则只需要 "bokeh-widgets" 文件。

  • 如果使用 Bokeh 的 数据表,则只需要 "bokeh-tables" 文件。

  • 要启用 WebGL 支持,需要 "bokeh-gl" 文件。

  • 要启用 MathJax 支持,需要 "bokeh-mathjax" 文件。

例如,要使用版本 3.0.0 以及对小部件、表格和数学文本的支持,请在您的 HTML 中包含以下内容

<script src="https://cdn.bokeh.org/bokeh/release/bokeh-3.0.0.min.js"
        crossorigin="anonymous"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.0.0.min.js"
        crossorigin="anonymous"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.0.0.min.js"
        crossorigin="anonymous"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.0.0.min.js"
        crossorigin="anonymous"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.0.0.min.js"
        crossorigin="anonymous"></script>

注意

始终提供结束 </script> 标记。这是所有浏览器都要求的,页面通常在没有此标记的情况下不会呈现。您还应该始终在脚本标记上包含 crossorigin="anonymous" 属性。

如果您希望通过设置 integrity 属性在显式脚本标记中包含 子资源完整性 (SRI) 哈希,则可以通过调用 get_sri_hashes_for_version() 获取所需的哈希。以下是一个示例

In [1]: import bokeh.resources

In [2]: bokeh.resources.get_sri_hashes_for_version("2.2.0")
Out[2]:
{'bokeh-2.2.0.js': 'TQAjsk2/lDn1NHjYoe8HIascd3/Cw4EWdk6GNtYXVVyAiUkbEZiuP7fEgbSwM37Y',

...

'bokeh-widgets-2.2.0.min.js': '2ltAd1cQhavmLeBEZXGgnna8fjbw+FjvDq9m2dig4+8KVS8JcYFUQaALvLT//qHE'}

这些是裸哈希,您必须在使用前以 sha384- 为前缀。例如

<script src="https://cdn.bokeh.org/bokeh/release/bokeh-2.2.0.min.js"
        integrity="sha384-5Y+xuMRAbgBj/2WKUiL8yzV4fBFic1HJPo2hT3pq2IsEzbsJjj8kT2i0b1lZ7C2N"
        crossorigin="anonymous"></script>

您只能为完整发布版本生成 SRI 哈希,不能为开发版本或候选发布版本生成 SRI 哈希。

除了单个 Bokeh 模型(例如绘图)之外, components() 函数还可以接受模型列表或元组,或者键和模型字典。每个函数返回一个元组,其中包含一个脚本和目标 <div> 元素的相应数据结构。

以下是不同输入类型如何与输出相关联的说明

components(plot)
#=> (script, plot_div)

components((plot_1, plot_2))
#=> (script, (plot_1_div, plot_2_div))

components({"Plot 1": plot_1, "Plot 2": plot_2})
#=> (script, {"Plot 1": plot_1_div, "Plot 2": plot_2_div})

以下是如何使用多绘图生成器的示例

# scatter.py

from bokeh.plotting import figure
from bokeh.models import Range1d
from bokeh.embed import components

# create some data
x1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
y1 = [0, 8, 2, 4, 6, 9, 5, 6, 25, 28, 4, 7]
x2 = [2, 5, 7, 15, 18, 19, 25, 28, 9, 10, 4]
y2 = [2, 4, 6, 9, 15, 18, 0, 8, 2, 25, 28]
x3 = [0, 1, 0, 8, 2, 4, 6, 9, 7, 8, 9]
y3 = [0, 8, 4, 6, 9, 15, 18, 19, 19, 25, 28]

# select the tools you want
TOOLS="pan,wheel_zoom,box_zoom,reset,save"

# the red and blue graphs share this data range
xr1 = Range1d(start=0, end=30)
yr1 = Range1d(start=0, end=30)

# only the green graph uses this data range
xr2 = Range1d(start=0, end=30)
yr2 = Range1d(start=0, end=30)

# build the figures
p1 = figure(x_range=xr1, y_range=yr1, tools=TOOLS, width=300, height=300)
p1.scatter(x1, y1, size=12, color="red", alpha=0.5)

p2 = figure(x_range=xr1, y_range=yr1, tools=TOOLS, width=300, height=300)
p2.scatter(x2, y2, size=12, color="blue", alpha=0.5)

p3 = figure(x_range=xr2, y_range=yr2, tools=TOOLS, width=300, height=300)
p3.scatter(x3, y3, size=12, color="green", alpha=0.5)

# plots can be a single Bokeh model, a list/tuple, or even a dictionary
plots = {'Red': p1, 'Blue': p2, 'Green': p3}

script, div = components(plots)
print(script)
print(div)

运行 python scatter.py 会打印出以下内容

<script type="text/javascript">
    const docs_json = { DOCUMENT DATA HERE }
    const render_items = [{
      "docid":"33961aa6-fd96-4055-886f-b2afec7ff193",
      "elementid":"e89297cf-a2dc-4edd-8993-e16f0ca6af04",
      "modelid":"4eff3fdb-80f4-4b4c-a592-f99911e14398"
    },{
      "docid":"33961aa6-fd96-4055-886f-b2afec7ff193",
      "elementid":"eeb9a417-02a1-47e3-ab82-221abe8a1644",
      "modelid":"0e5ccbaf-62af-42cc-98de-7c597d83747a"
    },{
      "docid":"33961aa6-fd96-4055-886f-b2afec7ff193",
      "elementid":"c311f123-368f-43ba-88b6-4e3ecd9aed94",
      "modelid":"57f18497-9598-4c70-a251-6072baf223ff"
    }];

    Bokeh.embed.embed_items(docs_json, render_items);
</script>

    {
        'Green': '\n<div id="e89297cf-a2dc-4edd-8993-e16f0ca6af04"></div>',
        'Blue': '\n<div id="eeb9a417-02a1-47e3-ab82-221abe8a1644"></div>',
        'Red': '\n<div id="c311f123-368f-43ba-88b6-4e3ecd9aed94"></div>'
    }

然后您可以将生成的脚本和 <div> 元素插入到以下示例的模板中

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Bokeh Scatter Plots</title>

        <script src="https://cdn.bokeh.org/bokeh/release/bokeh-2.2.0.min.js"></script>

        <!-- COPY/PASTE SCRIPT HERE -->

    </head>
    <body>
        <!-- INSERT DIVS HERE -->
    </body>
</html>

请注意,这并不包含用于 -widgets" 的 JavaScript 和 CSS 文件,因为文档没有使用任何 Bokeh 小部件。

您可以通过执行以下操作查看多绘图生成的示例

python /bokeh/examples/embed/embed_multiple.py

自动加载脚本#

您也可以使用 autoload_static() 函数嵌入独立的文档。此函数提供一个 <script> 标签,该标签会用 Bokeh 图表替换自身。此脚本还会检查 BokehJS 并根据需要加载它。使用此函数,您只需使用此 <script> 标签即可嵌入图表。

此函数接受要显示的 Bokeh 模型(例如图表)、Resources 对象以及从其加载脚本的路径。然后,autoload_static() 返回一个自包含的 <script> 标签和一段 JavaScript 代码。JavaScript 代码会保存到您提供的路径,而 <script> 会加载并运行该代码以在网页上显示图表。

以下是如何使用 autoload_static() 创建一个简单图表的示例

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")

生成的 <script> 标签如下所示

<script
    src="some/path"
    id="c5339dfd-a354-4e09-bba4-466f58a574f1"
    async="true"
    data-bokeh-modelid="7b226555-8e16-4c29-ba2a-df2d308588dc"
    data-bokeh-loglevel="info"
></script>

将此标签包含在您希望图表显示在 HTML 页面上的任何位置。

将 JavaScript 代码保存到服务器上的 “some/path” 文件中,该文件可以访问包含图表的文档。

注意

The <script> 标签会用 <div> 替换自身,因此它必须放置在文档的 <body> 内。

Bokeh 应用程序#

本节介绍如何嵌入完整的 Bokeh 服务器应用程序。您可以嵌入 Bokeh 应用程序,以便每次页面加载都会创建和显示一个新会话和文档,或者输出一个特定的现有会话。

应用程序文档#

如果应用程序在 Bokeh 服务器上运行,并且该服务器使其在某个 URL 上可用,那么您通常希望将整个应用程序嵌入网页中。这样,每次页面加载时,页面都会创建一个新会话并将其显示给用户。

您可以使用 server_document() 函数来实现此目的。此函数接受 Bokeh 服务器应用程序的 URL,并返回一个脚本,该脚本会每次执行脚本时嵌入来自该服务器的新会话。

以下是如何使用 server_document() 函数的示例

from bokeh.embed import server_document
script = server_document("https://demo.bokeh.org/sliders")

这将返回一个 <script> 标签,它看起来类似于以下代码

<script
    src="https://demo.bokeh.org/sliders/autoload.js?bokeh-autoload-element=1000&bokeh-app-path=/sliders&bokeh-absolute-url=https://demo.bokeh.org/sliders"
    id="1000">
</script>

您可以将此标签添加到 HTML 页面以在该位置包含 Bokeh 应用程序。

应用程序会话#

有时,您可能希望加载特定会话,而不是加载新会话。

以渲染已验证用户的页面的 Flask 应用程序为例。您可能希望它提取一个新会话,为该特定用户进行一些自定义,并提供此自定义的 Bokeh 服务器会话。

您可以使用 server_session() 函数来实现此目的。此函数接受要嵌入的特定模型(或 None 以表示整个会话文档)、会话 ID 以及 Bokeh 应用程序的 URL。

以下是如何使用 server_session() 与 Flask 结合使用的示例

from flask import Flask, render_template

from bokeh.client import pull_session
from bokeh.embed import server_session

app = Flask(__name__)

@app.route('/', methods=['GET'])
def bkapp_page():

    # pull a new session from a running Bokeh server
    with pull_session(url="https://127.0.0.1:5006/sliders") as session:

        # update or customize that session
        session.document.roots[0].children[1].title.text = "Special sliders for a specific user!"

        # generate a script to load the customized session
        script = server_session(session_id=session.id, url='https://127.0.0.1:5006/sliders')

        # use the script in the rendered page
        return render_template("embed.html", script=script, template="Flask")

if __name__ == '__main__':
    app.run(port=8080)

标准模板#

Bokeh 还提供了一个标准的 Jinja 模板,该模板可以帮助您通过扩展“base”模板来快速灵活地嵌入不同的文档根目录。当您需要将 Bokeh 应用程序的各个组件嵌入非 Bokeh 布局(如 Bootstrap)时,这尤其有用。

以下是一个创建两个根目录并将名称属性设置为它们的应用程序的最小示例

p1 = figure(..., name="scatter")

p2 = figure(..., name="line")

curdoc().add_root(p1)
curdoc().add_root(p2)

然后,您可以通过名称引用这些根目录,并将它们传递给 embed 宏以将其放置在模板的任何部分

{% extends base %}

<!-- goes in head -->
{% block preamble %}
<link href="app/static/css/custom.min.css" rel="stylesheet">
{% endblock %}

<!-- goes in body -->
{% block contents %}
<div> {{ embed(roots.scatter) }} </div>
<div> {{ embed(roots.line) }} </div>
{% endblock %}

以下是一个包含所有可覆盖部分的完整模板

<!DOCTYPE html>
<html lang="en">
{% block head %}
<head>
{% block inner_head %}
    <meta charset="utf-8">
    <title>{% block title %}{{ title | e if title else "Bokeh Plot" }}{% endblock %}</title>
{%  block preamble -%}{%- endblock %}
{%  block resources -%}
{%   block css_resources -%}
    {{- bokeh_css if bokeh_css }}
{%-  endblock css_resources %}
{%   block js_resources -%}
    {{  bokeh_js if bokeh_js }}
{%-  endblock js_resources %}
{%  endblock resources %}
{%  block postamble %}{% endblock %}
{% endblock inner_head %}
</head>
{% endblock head%}
{% block body %}
<body>
{%  block inner_body %}
{%    block contents %}
{%      for doc in docs %}
{{        embed(doc) if doc.elementid }}
{%-       for root in doc.roots %}
{%          block root scoped %}
{{            embed(root) }}
{%          endblock %}
{%        endfor %}
{%      endfor %}
{%    endblock contents %}
{{ plot_script | indent(4) }}
{%  endblock inner_body %}
</body>
{% endblock body%}
</html>