自定义扩展#

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 用于配置读取的文本消息和

  • 一个可以持有SliderInstance

这将创建一个与Python中的Slider相对应的JavaScript Slider对象。

JavaScript 模型和视图#

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

这里是一个关于Custom及其CustomView的带注释的TypeScript实现。对于内置模型,这种类型的代码直接包含在最终的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库

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

添加自定义小部件

在扩展小部件中包含一个第三方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 命令从头开始重新构建您的扩展。