构建应用程序#

到目前为止,使用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 选项将导致您的默认浏览器在运行应用程序的地址处打开一个新标签页,在这种情况下是:

http://localhost:5006/myapp

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

http://localhost: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应用于模型类型的默认属性。

  • 一个包含index.html Jinja模板文件的templates子目录。该目录可能包含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 提供给应用程序。由于与 --num-procs 不兼容,HTTP 请求不能直接使用。相反,只有 arguments 属性完全可用,并且只有由 --include-headers--exclude-headers--include-cookies--exclude-cookies 参数允许的 cookiesheaders 子集可用。尝试访问 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 callbacks

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

使用Jupyter交互器的Python回调#

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

注意

你可以使用 push_notebook() 从 Python 向 BokehJS 推送绘图更新。为了实现双向通信,可以在笔记本中嵌入 Bokeh 服务器。例如,这可以让范围和选择的更新触发 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', radius=0.02, 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请求。更多信息,请参见请求处理钩子