构建应用程序#

迄今为止,使用 Bokeh 服务器创建交互式数据可视化的最灵活方法是创建 Bokeh 应用程序并使用 bokeh serve 命令提供服务。然后,Bokeh 服务器使用应用程序代码为所有连接的浏览器创建会话和文档。

../../../_images/bokeh_serve.svg

Bokeh 服务器(左)使用应用程序代码创建 Bokeh 文档。来自浏览器的每个新连接(右)都会导致服务器为该会话创建一个新文档。#

Bokeh 服务器在每次新连接时执行应用程序代码并创建一个新的 Bokeh 文档,将其同步到浏览器。应用程序代码还设置了在属性(例如部件值)更改时应运行的回调。

您可以通过多种方式提供应用程序代码。

单模块格式#

考虑以下完整示例。

# myapp.py

from random import random

from bokeh.layouts import column
from bokeh.models import Button
from bokeh.palettes import RdYlBu3
from bokeh.plotting import figure, curdoc

# create a plot and style its properties
p = figure(x_range=(0, 100), y_range=(0, 100), toolbar_location=None)
p.border_fill_color = 'black'
p.background_fill_color = 'black'
p.outline_line_color = None
p.grid.grid_line_color = None

# add a text renderer to the plot (no data yet)
r = p.text(x=[], y=[], text=[], text_color=[], text_font_size="26px",
           text_baseline="middle", text_align="center")

i = 0

ds = r.data_source

# create a callback that adds a number in a random location
def callback():
    global i

    # BEST PRACTICE --- update .data in one step with a new dict
    new_data = dict()
    new_data['x'] = ds.data['x'] + [random()*70 + 15]
    new_data['y'] = ds.data['y'] + [random()*70 + 15]
    new_data['text_color'] = ds.data['text_color'] + [RdYlBu3[i%3]]
    new_data['text'] = ds.data['text'] + [str(i)]
    ds.data = new_data

    i = i + 1

# add a button widget and configure with the call back
button = Button(label="Press Me")
button.on_event('button_click', callback)

# put the button and plot in a layout and add to the document
curdoc().add_root(column(button, p))

以上代码未指定任何输出或连接方法。它是一个简单的脚本,用于创建和更新对象。 bokeh 命令行工具允许您在处理数据后指定输出选项。例如,您可以运行 bokeh json myapp.py 以获取应用程序的 JSON 序列化版本。但是,要在 Bokeh 服务器上运行应用程序,请使用以下命令

bokeh serve --show myapp.py

--show 选项将导致您的默认浏览器在新选项卡中打开正在运行的应用程序的地址,在本例中为

https://127.0.0.1:5006/myapp

如果您只有一个应用程序,则服务器根目录将重定向到它。否则,您将看到服务器根目录上所有正在运行的应用程序的索引

https://127.0.0.1:5006/

您可以使用 --disable-index 选项禁用此索引。同样,您可以使用 --disable-index-redirect 选项禁用重定向。

除了从单个 Python 文件创建 Bokeh 应用程序外,您还可以从目录创建应用程序。

目录格式#

您可以通过创建并使用应用程序文件填充文件系统目录来创建 Bokeh 应用程序。要启动名为 myapp 的目录中的应用程序,您可以执行 bokeh serve,如下所示

bokeh serve --show myapp

此目录必须包含一个 main.py 文件,该文件为 Bokeh 服务器要提供的服务构建文档

myapp
   |
   +---main.py

以下是 Bokeh 服务器熟悉的目录应用程序结构

myapp
   |
   +---__init__.py
   +---app_hooks.py
   +---main.py
   +---request_handler.py
   +---static
   +---theme.yaml
   +---templates
        +---index.html

上面的一些文件和子目录是可选的。

  • 一个 __init__.py 文件,将此目录标记为包。您可以进行相对于包的导入,例如 from . import mymodfrom .mymod import func

  • 一个 request_handler.py 文件,允许您声明一个可选函数来处理 HTTP 请求并返回一个字典,其中包含会话令牌中包含的项目,如 请求处理程序钩子 中所述。

  • 一个 app_hooks.py 文件,允许您在应用程序执行的不同阶段触发可选回调,如 生命周期钩子请求处理程序钩子 中所述。

  • 一个 static 子目录,您可以使用它来提供与该应用程序关联的静态资源。

  • 一个 theme.yaml 文件,您可以在其中声明 Bokeh 要应用于模型类型的默认属性。

  • 一个 templates 子目录,其中包含一个 index.html Jinja 模板文件。该目录可能包含 index.html 要引用的其他 Jinja 模板。该模板应具有与 FILE 模板相同的参数。有关更多信息,请参见 自定义应用程序的 Jinja 模板

在执行 main.py 时,Bokeh 服务器确保标准 __file__ 模块属性按预期工作。因此,您可以根据需要在目录中包含数据文件或自定义用户定义的模型。

Bokeh 还将应用程序目录 sys.path 添加到应用程序目录中以方便导入 Python 模块。但是,如果目录中存在 __init__.py,则您可以将应用程序用作包,并进行标准的包相对导入。

以下是一个更发达的目录树示例

myapp
   |
   +---__init__.py
   |
   +---app_hooks.py
   +---data
   |    +---things.csv
   |
   +---helpers.py
   +---main.py
   |---models
   |    +---custom.js
   |
   +---request_handler.py
   +---static
   |    +---css
   |    |    +---special.css
   |    |
   |    +---images
   |    |    +---foo.png
   |    |    +---bar.png
   |    |
   |    +---js
   |        +---special.js
   |
   |---templates
   |    +---index.html
   |
   +---theme.yaml

在这种情况下,您的代码可能类似于以下内容

from os.path import dirname, join
from .helpers import load_data

load_data(join(dirname(__file__), 'data', 'things.csv'))

models/custom.js 加载自定义模型的 JavaScript 实现的代码也类似。

自定义应用程序的 Jinja 模板#

目录格式 部分提到您可以覆盖默认的 Jinja 模板,Bokeh 服务器使用该模板生成面向用户的 HTML。

这使您可以使用 CSS 和 JavaScript 来调整应用程序在浏览器中的显示方式。

有关 Jinja 模板工作原理的更多详细信息,请参见 Jinja 项目文档

在模板中嵌入图形#

要在模板化代码中引用 Bokeh 图形,您需要设置其 name 属性,并在 Bokeh 应用程序的主线程(即 main.py)中将图形添加到当前文档根目录。

from bokeh.plotting import curdoc

# templates can refer to a configured name value
plot = figure(name="bokeh_jinja_figure")

curdoc().add_root(plot)

然后,您可以使用该名称在相应的 Jinja 模板中通过 roots 模板参数引用图形,如下所示

{% extends base %}

{% block contents %}
<div>
    {{ embed(roots.bokeh_jinja_figure) }}
</div>
{% endblock %}

定义自定义变量#

您可以使用 curdoc().template_variables 字典将自定义变量传递给模板,如下所示

# set a new single key/value pair
curdoc().template_variables["user_id"] = user_id

# or update multiple pairs at once
curdoc().template_variables.update(first_name="Mary", last_name="Jones")

然后,您可以在相应的 Jinja 模板中引用这些变量。

{% extends base %}

{% block contents %}
<div>
    <p> Hello {{ user_id }}, AKA '{{ last_name }}, {{ first_name }}'! </p>
</div>
{% endblock %}

访问 HTTP 请求#

在为应用程序创建会话时,Bokeh 会将会话上下文作为 curdoc().session_context 提供。会话上下文最有用的功能是将 Tornado HTTP 请求对象作为 session_context.request 提供给应用程序。HTTP 请求无法直接使用,因为与 --num-procs 不兼容。相反,只有 arguments 属性是完整可用的,并且只有 cookiesheaders 的子集(允许 --include-headers--exclude-headers--include-cookies--exclude-cookies 参数)是可用的。尝试访问 request 上的任何其他属性会导致错误。

您可以启用其他请求属性,如 请求处理程序钩子 中所述。

以下代码访问请求 arguments 以提供变量 N 的值,该变量可以例如控制绘图点的数量。

# request.arguments is a dict that maps argument names to lists of strings,
# for example, the query string ?N=10 results in {'N': [b'10']}

args = curdoc().session_context.request.arguments

try:
  N = int(args.get('N')[0])
except:
  N = 200

警告

请求对象使检查值(如 arguments)变得容易。但是,调用任何 Tornado 方法(例如 finish())或直接写入 request.connection 均不受支持,并且会导致未定义的行为。

请求处理程序钩子#

为了提供其他信息,在可能无法使用完整的 Tornado HTTP 请求的地方,您可以定义一个自定义处理程序钩子。

为此,请在 目录格式 中创建一个应用程序,并在目录中包含一个名为 request_handler.py 的文件。此文件必须包含一个 process_request 函数。

def process_request(request):
    '''If present, this function executes when an HTTP request arrives.'''
    return {}

然后,该过程将 Tornado HTTP 请求传递给处理程序,该处理程序返回 curdoc().session_context.token_payload 的字典。这使您可以解决一些 --num-procs 问题并提供其他信息。

回调和事件#

在专门讨论 Bokeh 服务器上下文中回调和事件之前,值得讨论一下回调的一般不同用例。

浏览器中的 JavaScript 回调#

无论您是否使用 Bokeh 服务器,您都可以使用CustomJS和其他方法创建在浏览器中执行的回调。有关更多信息和示例,请参见JavaScript 回调

CustomJS回调**绝不会**执行 Python 代码,即使您将 Python 回调转换为 JavaScript 也是如此。CustomJS回调仅在浏览器的 JavaScript 解释器中执行,这意味着它们只能与 JavaScript 数据和函数(例如 BokehJS 模型)交互。

使用 Jupyter 交互器的 Python 回调#

在使用 Jupyter Notebook 时,您可以使用 Jupyter 交互器快速创建简单的 GUI 表单。GUI 小部件的更新会触发在 Jupyter 的 Python 内核中执行的 Python 回调。这些回调通常会调用push_notebook()以将更新推送到显示的绘图中。有关更多信息,请参见Jupyter 交互器

注意

您可以使用push_notebook()将绘图更新从 Python 推送到 BokehJS。对于双向通信,请将 Bokeh 服务器嵌入到 Notebook 中。例如,这允许范围和选择更新触发 Python 回调。有关更多详细信息,请参见examples/server/api/notebook_embed.ipynb

从线程更新#

您可以在单独的线程中进行阻塞计算。但是,您**必须**通过下一个 tick 回调安排文档更新。此回调将在 Tornado 事件循环的下次迭代中尽快执行,并自动获取必要的锁以安全地更新文档状态。

警告

从另一个线程对文档执行的唯一安全操作是add_next_tick_callback()remove_next_tick_callback()

请记住,直接从另一个线程更新文档状态,无论是通过其他文档方法还是设置 Bokeh 模型属性,都会存在数据和协议损坏的风险。

要允许所有线程访问同一个文档,请保存curdoc()的本地副本。以下示例说明了此过程。

import time
from functools import partial
from random import random
from threading import Thread

from bokeh.models import ColumnDataSource
from bokeh.plotting import curdoc, figure

# only modify from a Bokeh session callback
source = ColumnDataSource(data=dict(x=[0], y=[0]))

# This is important! Save curdoc() to make sure all threads
# see the same document.
doc = curdoc()

async def update(x, y):
    source.stream(dict(x=[x], y=[y]))

def blocking_task():
    while True:
        # do some blocking computation
        time.sleep(0.1)
        x, y = random(), random()

        # but update the document from a callback
        doc.add_next_tick_callback(partial(update, x=x, y=y))

p = figure(x_range=[0, 1], y_range=[0,1])
l = p.circle(x='x', y='y', source=source)

doc.add_root(p)

thread = Thread(target=blocking_task)
thread.start()

要查看此示例的实际操作,请将以上代码保存到 Python 文件中,例如testapp.py,然后执行以下命令

bokeh serve --show testapp.py

警告

目前,在向文档添加下一个 tick 回调时没有锁定。Bokeh 未来应该对回调方法进行更细粒度的锁定,但目前最好让每个线程向文档添加不超过一个回调。

从未锁定的回调更新#

通常,Bokeh 会话回调会递归锁定文档,直到他们启动的所有未来工作完成。但是,您可能希望使用 Tornado 的ThreadPoolExecutor在异步回调中从回调驱动阻塞计算。这要求您使用without_document_lock()装饰器来抑制正常的锁定行为。

与上面的线程示例一样,**所有更新文档状态的操作都必须通过下一个 tick 回调进行**。

以下示例演示了一个应用程序,该应用程序从一个未锁定的 Bokeh 会话回调驱动阻塞计算。它会让位于在线程池执行器上运行的阻塞函数,然后使用下一个 tick 回调进行更新。该示例还以不同的更新速率从标准锁定的会话回调中简单地更新状态。

import asyncio
import time
from concurrent.futures import ThreadPoolExecutor
from functools import partial

from bokeh.document import without_document_lock
from bokeh.models import ColumnDataSource
from bokeh.plotting import curdoc, figure

source = ColumnDataSource(data=dict(x=[0], y=[0], color=["blue"]))

i = 0

doc = curdoc()

executor = ThreadPoolExecutor(max_workers=2)

def blocking_task(i):
    time.sleep(1)
    return i

# the unlocked callback uses this locked callback to safely update
async def locked_update(i):
    source.stream(dict(x=[source.data['x'][-1]+1], y=[i], color=["blue"]))

# this unlocked callback will not prevent other session callbacks from
# executing while it is running
@without_document_lock
async def unlocked_task():
    global i
    i += 1
    res = await asyncio.wrap_future(executor.submit(blocking_task, i), loop=None)
    doc.add_next_tick_callback(partial(locked_update, i=res))

async def update():
    source.stream(dict(x=[source.data['x'][-1]+1], y=[i], color=["red"]))

p = figure(x_range=[0, 100], y_range=[0, 20])
l = p.circle(x='x', y='y', color='color', source=source)

doc.add_periodic_callback(unlocked_task, 1000)
doc.add_periodic_callback(update, 200)
doc.add_root(p)

和以前一样,您可以通过保存到 Python 文件并在其上运行bokeh serve来运行此示例。

生命周期钩子#

您可能希望在服务器或会话运行时的特定时间点执行代码。Bokeh 通过一组生命周期钩子实现了这一点。要使用这些钩子,请在目录格式中创建您的应用程序,并在目录中包含一个名为app_hooks.py的指定文件。在此文件中,您可以包含以下任何或所有约定命名函数

def on_server_loaded(server_context):
    # If present, this function executes when the server starts.
    pass

def on_server_unloaded(server_context):
    # If present, this function executes when the server shuts down.
    pass

def on_session_created(session_context):
    # If present, this function executes when the server creates a session.
    pass

def on_session_destroyed(session_context):
    # If present, this function executes when the server closes a session.
    pass

您还可以直接在正在服务的Document上定义on_session_destroyed生命周期钩子。这使得在用户关闭会话后执行数据库连接关闭等操作变得很容易,而无需捆绑单独的文件。要声明这样的回调,请定义一个函数并将其注册到Document.on_session_destroyed方法中

doc = Document()

def cleanup_session(session_context):
    # This function executes when the user closes the session.
    pass

doc.on_session_destroyed(cleanup_session)

除了上述生命周期钩子之外,您还可以定义请求钩子来访问用户发出的 HTTP 请求。有关更多信息,请参见请求处理程序钩子