自定义扩展#

Bokeh 提供了丰富的内置功能,可让您在浏览器中创建复杂的交互式可视化和数据应用程序。但是,某些有用的功能和特性可能不会包含在核心库中,要么是因为它们过于专业,要么是因为资源不足。幸运的是,您可以使用自定义扩展来扩展 Bokeh 的功能,这些扩展可以让您

  • 修改现有 Bokeh 模型的行为

  • 添加新模型以将第三方 JavaScript 库连接到 Python

  • 创建高度专业化的模型以用于特定领域的用例

您可以使用标准发布版本创建和使用自定义扩展,无需设置开发环境或从源代码构建任何内容。这是参与 Bokeh 开发最简单的方法。您可以尝试新功能和改进的功能,而无需等待核心团队将其实现到 Bokeh 本身中。

注意

扩展 Bokeh 是一项高级功能。创建和使用扩展的某些方面仍在积极开发中,应视为实验性。

Bokeh 模型结构#

Python 模型#

在大多数情况下,Python Bokeh 模型是完全声明式的类。您可以通过从 Model 创建子类并包含特殊类属性来声明要在 JavaScript 端镜像的属性,从而创建自定义扩展。有关所有可用属性类型的详细信息,请参见 bokeh.core.properties 部分的 参考指南

以下是一个简单的示例,它为滑块创建自定义读数

from bokeh.core.properties import String, Instance
from bokeh.models import UIElement, Slider

class Custom(UIElement):

    text = String(default="Custom text")

    slider = Instance(Slider)

此示例从 UIElement 创建子类,以允许扩展集成到 DOM 布局中。它还添加了两个属性

  • 一个 String 用于配置读数的文本消息,以及

  • 一个 Instance 可以保存 Slider

这会创建一个 JavaScript Slider 对象,它对应于 Python 中的 Slider

JavaScript 模型和视图#

虽然 Python 端几乎没有代码,但 JavaScript 端需要代码来实现模型。您还需要提供必要的对应视图的代码。

以下是一个带有注释的 TypeScript 实现,用于 Custom 及其 CustomView。对于内置模型,这种类型的代码直接包含在最终的 BokehJS 脚本中。

import {UIElement, UIElementView} from "models/ui/ui_element"
import {Slider} from "models/widgets/sliders/slider"
import {div} from "core/dom"
import * as p from "core/properties"

export class CustomView extends UIElementView {
  declare model: Custom

  private content_el: HTMLElement

  override connect_signals(): void {
    super.connect_signals()
    this.connect(this.model.slider.change, () => this._update_text())
  }

  override render(): void {
    // BokehJS views create <div> elements by default. These are accessible
    // as ``this.el``. Many Bokeh views ignore the default <div> and
    // instead do things like draw to the HTML canvas. In this case though,
    // the program changes the contents of the <div> based on the current
    // slider value.
    super.render()

    this.content_el = div({style: {
      textAlign: "center",
      fontSize: "1.2em",
      padding: "2px",
      color: "#b88d8e",
      backgroundColor: "#2a3153",
    }})
    this.shadow_el.append(this.content_el)

    this._update_text()
  }

  private _update_text(): void {
    this.content_el.textContent = `${this.model.text}: ${this.model.slider.value}`
  }
}

export namespace Custom {
  export type Attrs = p.AttrsOf<Props>

  export type Props = UIElement.Props & {
    text: p.Property<string>
    slider: p.Property<Slider>
  }
}

export interface Custom extends Custom.Attrs {}

export class Custom extends UIElement {
  declare properties: Custom.Props
  declare __view_type__: CustomView

  constructor(attrs?: Partial<Custom.Attrs>) {
    super(attrs)
  }

  static {
    // If there is an associated view, this is typically boilerplate.
    this.prototype.default_view = CustomView

    // The this.define() block adds corresponding "properties" to the JS
    // model. These should normally line up 1-1 with the Python model
    // class. Most property types have counterparts. For example,
    // bokeh.core.properties.String will correspond to ``String`` in the
    // JS implementation. Where JS lacks a given type, you can use
    // ``p.Any`` as a "wildcard" property type.
    this.define<Custom.Props>(({Str, Ref}) => ({
      text:   [ Str, "Custom text" ],
      slider: [ Ref(Slider) ],
    }))
  }
}

组合在一起#

对于内置的 Bokeh 模型,构建过程会自动将 BokehJS 中的实现与相应的 Python 模型匹配。Python 类还应该有一个名为 __implementation__ 的类属性,其值为定义客户端模型的 JavaScript(或 TypeScript)代码以及任何可选视图。

假设您将前一个示例中的 TypeScript 代码保存在名为 custom.ts 的文件中,完整的 Python 类可能如下所示

from bokeh.core.properties import String, Instance
from bokeh.models import UIElement, Slider

class Custom(UIElement):

    __implementation__ = "custom.ts"

    text = String(default="Custom text")

    slider = Instance(Slider)

假设 Python 模块 custom.py 定义了此类,您现在可以使用自定义扩展,就像使用任何内置 Bokeh 模型一样。

from bokeh.io import show, output_file
from bokeh.layouts import column
from bokeh.models import Slider

slider = Slider(start=0, end=10, step=0.1, value=0, title="value")

custom = Custom(text="Special Slider Display", slider=slider)

layout = column(slider, custom)

show(layout)

这会产生以下输出

渲染的文档会自动包含实现的 JavaScript 代码。移动滑块以查看滑块移动时特殊标题的更新。

指定实现语言#

如果 __implementation__ 的值为以 .js.ts 结尾的单行,Bokeh 会将其解释为文件名,打开文件,并根据文件的扩展名编译其内容。

对于倾斜的实现,请使用类 JavaScriptTypeScript 为源代码指定语言。以下是一个示例

class Custom(Model):

    __implementation__ = JavaScript(" <JS code here> ")

指定默认值#

如果您的属性具有默认值,您必须在 Python 端和 JavaScript 端提供默认值。您提供的值在两端应相同。出于效率考虑,Bokeh 只会传输用户明确更改其默认值的属性值。

作为一个具体的示例,一个默认值为 True 的布尔属性 flag 在 Python 端应该如下所示

flag = Bool(default=True)

它在 Bokeh 端应该如下所示

flag: [ Boolean, true ]

提供外部资源#

您可能需要第三方 JavaScript 库或 CSS 资源来在 Bokeh 中实现自定义模型。您可以通过自定义模型的 __javascript____css__ Python 类属性来提供外部资源。

包含指向外部资源的 URL 路径会将它们添加到 HTML 文档的头部,从而使 JavaScript 库在全局命名空间中可用,并应用自定义 CSS 样式。

以下是一个示例,它包括 KaTeX(一个具有 LaTeX 支持的 JS 库)的 JS 和 CSS 文件,以创建 LatexLabel 自定义模型。

class LatexLabel(Label):
    """A subclass of the built-in Bokeh model `Label` that supports
    rendering LaTeX with the KaTeX typesetting library.
    """
    __javascript__ = "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.js"
    __css__ = "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.css"
    __implementation__ = """
    # do something here
    """

有关完整实现及其输出,请参见以下扩展画廊中的 LaTeX 示例。

与 Bokeh 服务器集成#

您无需执行任何额外操作即可将自定义扩展与 Bokeh 服务器集成。与独立文档一样,渲染的应用程序会自动包含 JavaScript 实现。此外,Bokeh 模型属性的标准同步对于自定义用户扩展是透明的,与内置模型相同。

示例#

本节旨在为您提供一些基本示例,以帮助您开始创建自定义扩展。但是,这是一个相当高级的主题,您通常需要研究 bokehjs/src/lib/models 中基类的源代码才能取得进展。

专门的轴刻度

对内置的 Bokeh 模型进行子类化,以自定义轴刻度行为。

一个新的自定义工具

创建一个可以绘制在绘图画布上的全新工具。

包装 JavaScript 库

通过将第三方 JavaScript 库包装在 Bokeh 自定义扩展中,将 Python 连接到该库。

添加自定义部件

在扩展部件中包含第三方 JavaScript 库。

预构建扩展#

到目前为止,本章介绍了简单的、通常是内联扩展。这些对于 Bokeh 的临时添加非常有用,但这种方法在涉及到严肃开发时并不十分方便。

例如,某些配置文件(如 package.jsontsconfig.json)的隐式性质不允许您在编写 TypeScript 或 JavaScript 用于扩展时充分利用 IDE 的功能。

使用预构建扩展。

要创建预构建扩展,请使用 bokeh init 命令。这会创建所有必要的文件,包括 bokeh.ext.jsonpackage.jsontsconfig.json

要逐步创建和自定义扩展,请运行 bokeh init --interactive

要构建扩展,请使用 bokeh build 命令。这会在必要时运行 npm install,编译 TypeScript 文件,转译 JavaScript 文件,解析模块,并将它们链接到可分发的捆绑包中。

Bokeh 会缓存编译产品以提高性能。如果这会导致问题,请使用 bokeh build --rebuild 命令从头开始重建扩展。