Bokeh JavaScript 回调(JS Callbacks)完全指南:从 CustomJS、SetValue 到 js_on_change 与 js_on_event
2026/9/14 13:02:51 网站建设 项目流程

Bokeh JavaScript 回调(JS Callbacks)完全指南:从 CustomJS、SetValue 到 js_on_change 与 js_on_event

【免费下载链接】bokehInteractive Data Visualization in the browser, from Python项目地址: https://gitcode.com/GitHub_Trending/bo/bokeh

本文是 Bokeh 交互编程中"JavaScript 回调"体系的系统性实战指南,主题源于仓库中的 docs/bokeh/source/docs/user_guide/interaction/js_callbacks.rst。Bokeh 的核心目标是"仅用 Python 即可在浏览器中产出丰富的交互可视化",但任何预定义核心库都无法覆盖全部需求,为此 Bokeh 提供了在浏览器端注入自定义 JavaScript 的多条路径。读完本文,你将掌握三种回调生成方式(js_link便捷方法、SetValue对象、CustomJS对象)、两种触发机制(.js_on_change属性变更、.js_on_event交互事件),并能结合仓库中的真实示例直接落地到自己的图表中。

为什么需要 JavaScript 回调

Bokeh 的哲学是:从 Python 到浏览器、纯 Python 构建交互,但总有一些用例超出预定义核心库的能力边界。为此,Bokeh 允许用户在必要时自行提供 JavaScript,以便在属性变更(property changes)和其他浏览器事件发生时执行自定义或特定主题的行为。

需要特别区分的是:本文讨论的 JavaScript 回调是在浏览器中执行的 JavaScript 代码片段。如果你的目标是"完全基于 Python 且需要 Bokeh Server 支持"的交互回调,请参见 Python 回调指南,两者适用场景不同。

生成 JavaScript 回调的三种方式

原文档明确给出了三个选项:

  1. js_link便捷方法:用于把不同模型的属性相互链接,Bokeh 会自动生成所需的 JavaScript 代码,详见 链接指南。
  2. SetValuePython 对象:根据另一个对象的特定事件,动态设置某个对象的属性,详见后文"SetValue 回调"。
  3. CustomJS对象:直接编写自定义 JavaScript 代码片段,这是最灵活、最常用的方式,详见后文"CustomJS 回调"。

两种回调触发类型

  • 属性变更触发(.js_on_change:大多数 Bokeh 对象(例如所有控件)都带有.js_on_change属性,当对象状态改变时调用绑定在其上的回调。
  • 事件触发(.js_on_event:部分控件还带有.js_on_event属性,当浏览器中发生特定事件时调用回调。

安全警告(务必阅读)CustomJS模型的明确用途就是嵌入一段供浏览器执行的原始 JavaScript。如果代码的任何部分来源于不可信的用户输入,你必须在传给 Bokeh 之前对用户输入做适当的清理(sanitize)。这一点在 src/bokeh/models/callbacks.py 的CustomJS类文档字符串中同样被反复强调。

此外,你还可以通过编写自定义扩展模型(Bokeh extensions)来添加全新的功能,这属于扩展开发的高级话题。

SetValue 回调:最简单的"事件 → 属性赋值"

使用 SetValue 模型,可以在浏览器中某个事件发生时,动态地设置特定对象的属性。它的三个核心属性:

属性类型含义
objHasProps(必填)要设置值的对象
attrstr(必填)要修改的对象属性名
valueAnyRef(必填)要为该属性设置的值

从源码看,SetValue还内置了两条校验逻辑(见 callbacks.py):

  • NOT_A_PROPERTY_OF校验:如果attr不是obj的属性,会抛出"{attr} is not a property of {obj}"的验证错误;
  • INVALID_PROPERTY_VALUE校验:如果value不符合该属性的合法类型,会抛出"{value!r} is not a valid value for {obj}.{attr}"

因此SetValue在运行前就能帮你发现拼写错误和类型错误,这也是它比手写CustomJS更"安全"的地方。

基于这三个参数,Bokeh 会自动生成所需的 JavaScript 代码。仓库示例 examples/interaction/js_callbacks/setvalue.py 演示了点击按钮后修改按钮自身文案的场景:

from bokeh.io import show from bokeh.models import Button, SetValue button = Button(label="Foo", button_type="primary") callback = SetValue(obj=button, attr="label", value="Bar") button.js_on_event("button_click", callback) show(button)

运行后,点击 "Foo" 按钮,其标签即变为 "Bar"——整个过程无需编写一行 JavaScript。

CustomJS 回调:完整掌握模块化与现代写法

CustomJS是功能最强大的回调方式,它允许你提供一段自定义 JavaScript 代码片段,在事件发生时于浏览器中执行。入口为bokeh.models.callbacks.CustomJS(同时也可从bokeh.models导入)。

现代写法:ES 模块 + 默认导出函数

推荐的新式写法将代码片段视为一个 ES 模块(ESM),必须包含一个默认导出,且该导出必须是一个函数,既可以是箭头函数() => {},也可以是经典函数function() {};根据上下文,它还可能是 async 函数、生成器函数或 async 生成器函数,并且可能被要求返回值:

from bokeh.models.callbacks import CustomJS callback = CustomJS(args=dict(xr=plot.x_range, yr=plot.y_range, slider=slider), code=""" // imports import {some_function, SOME_VALUE} from "https://cdn.jsdelivr.net/npm/package@version/file" // constants, definitions and state const MY_VALUE = 3.14 function my_function(value) { return MY_VALUE*value } class MyClass { constructor(value) { this.value = value } } let count = 0 // the callback function export default (args, obj, data, context) => { count += 1 console.log(`CustomJS was called ${count} times`) const a = args.slider.value const b = obj.value const {xr, yr} = args xr.start = my_function(a) xr.end = b } """)

这段示例揭示了模块化写法的关键优势:代码片段只编译一次,而默认导出的回调函数可以被求值多次。你可以在顶层稳健高效地导入外部库、定义复杂的类和数据结构,并在多次回调调用之间维持状态(如例子中的count)。只有当CustomJS实例的某个属性发生变化时,代码片段才会被重新编译。

回调函数的四个位置参数

默认导出函数固定接收四个位置参数:

参数说明
args映射到CustomJS.args属性,将名字映射到可序列化值,通常用于把 Bokeh 模型传入代码片段
obj触发回调的模型(即回调所依附的那个模型)
data由回调发射方提供的名字-值映射,取决于调用方、事件及事件发生的上下文。例如选择类工具会用data提供选择几何信息等
contextbokehjs 提供的更宽泛的上下文,同样是名字-值映射。目前仅提供index,可用来访问 bokehjs 的视图索引

借助对象解构(destructuring)语法,可以立即拿到所需值:

from bokeh.models.callbacks import CustomJS callback = CustomJS(args=dict(xr=plot.x_range, yr=plot.y_range, slider=slider), code=""" export default ({xr, yr, slider}, obj, {geometry}, {index}) => { // use xr, yr, slider, geometry and index } """)

旧式写法:隐式函数体

CustomJS也兼容旧式变体:此时代码片段是隐式回调函数的函数体,CustomJS.args中的名字会直接成为作用域内可用的变量,而objdatacontext则以cb_前缀暴露,即cb_objcb_datacb_context

from bokeh.models.callbacks import CustomJS callback = CustomJS(args=dict(xr=plot.x_range), code=""" // JavaScript code goes here const a = 10 // the model that triggered the callback is cb_obj: const b = cb_obj.value // models passed as args are auto-magically available xr.start = a xr.end = b """)

Bokeh 通过检测代码片段中是否存在import/export语法来区分新旧两种写法。在 callbacks.py 中,这一逻辑体现为module属性(默认"auto"):显式设置为True/False可强制按 ES 模块或 JS 函数解释,设为"auto"则从代码自动推断。

从文件加载 CustomJS:from_file

处理大型/复杂代码片段时,推荐把 JavaScript 单独存放为文件,再用CustomJS.from_file加载(实现见 callbacks.py):

from bokeh.models.callbacks import CustomJS callback = CustomJS.from_file("./my_module.mjs", xr=plot.x_range)

允许的扩展名与语义为:

  • .mjs:新式export default () => {}模块变体;
  • .js:旧式CustomJS函数体变体。

源码中,from_file会根据后缀自动设置module.mjsTrue.jsFalse),其他后缀会抛出RuntimeError

触发方式一:js_on_change(属性变更触发)

CustomJSSetValue回调都可以通过任意 Bokeh 模型的js_on_change方法,挂接到属性变更事件上:

p = figure() # execute a callback whenever p.x_range.start changes p.x_range.js_on_change('start', callback)

仓库示例 examples/interaction/js_callbacks/js_on_change.py 将CustomJS回调挂到Slider上:滑块值一更新,回调就用自定义公式重算并更新绘图数据:

from bokeh.layouts import column from bokeh.models import ColumnDataSource, CustomJS, Slider from bokeh.plotting import figure, show x = [x*0.005 for x in range(0, 200)] y = x source = ColumnDataSource(data=dict(x=x, y=y)) plot = figure(width=400, height=400, x_range=(0, 1), y_range=(0, 1)) plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6) callback = CustomJS(args=dict(source=source), code=""" const f = cb_obj.value const x = source.data.x const y = Array.from(x, (x) => Math.pow(x, f)) source.data = { x, y } """) slider = Slider(start=0.1, end=4, value=1, step=.1, title="power") slider.js_on_change('value', callback) layout = column(slider, plot) show(layout)

这里的核心技巧是把ColumnDataSource通过args传入CustomJS,回调内部直接改写source.data——由于 Bokeh 的数据源是响应式的,改数据即可驱动图形重绘,无需手动刷新画布。

触发方式二:js_on_event(浏览器事件触发)

除了用js_on_change响应属性变更,Bokeh 还允许CustomJSSetValue回调由特定的交互事件触发,包括:绘图画布上的交互事件、按钮点击事件、LOD(Level-of-Detail,细节层次)事件以及文档事件。

这些事件回调通过模型的js_on_event方法注册,回调内部通过局部变量cb_obj拿到事件对象:

from bokeh.models.callbacks import CustomJS callback = CustomJS(code=""" // the event that triggered the callback is cb_obj: // The event type determines the relevant attributes console.log('Tap event occurred at x-position: ' + cb_obj.x) """) p = figure() # execute a callback whenever the plot canvas is tapped p.js_on_event('tap', callback)

事件既可以用字符串指定(如上文的'tap'),也可以使用bokeh.events模块中的事件类(如from bokeh.events import Tap)。可用的常见事件类在 src/bokeh/events.py 中有完整定义,本文提到的包括:

事件类说明常用属性
ButtonClick按钮点击(第 315 行)
ValueSubmit文本输入提交(第 372 行)value
AxisClick坐标轴点击(第 291 行)value
LODStart/LODEnd细节层次开始/结束(第 399 行)
TapDoubleTapPressPressUp指针类事件(第 508 行起)x, y, sx, sy
MouseWheel滚轮事件(第 604 行)x, y, sx, sy, delta
MouseEnter/MouseLeave鼠标进入/离开x, y, sx, sy
PanPanStartPanEnd平移事件(第 635 行)x, y, sx, sy, delta_x, delta_y
PinchPinchStartPinchEnd捏合缩放x, y, sx, sy, scale
RangesUpdate范围更新(第 423 行)x0, x1, y0, y1
SelectionGeometry选择几何(第 452 行)geometry, final
DocumentReady文档渲染完成(第 218 行)

仓库示例 examples/interaction/js_callbacks/js_on_event.py 导入bokeh.events并注册了几乎所有事件类,通过一个display_event工厂函数生成对应的CustomJS,把事件名(始终可从event_name属性获得)及所有适用的事件属性更新到右侧的Div中——与画布交互时,右侧会实时列出触发的事件及参数:

def display_event(div: Div, attributes: list[str] = []) -> CustomJS: style = 'float: left; clear: left; font-size: 13px' return CustomJS(args=dict(div=div), code=f""" const attrs = {attributes}; const args = []; for (let i = 0; i < attrs.length; i++) {{ const val = JSON.stringify(cb_obj[attrs[i]], function(key, val) {{ return val.toFixed ? Number(val.toFixed(2)) : val; }}) args.push(attrs[i] + '=' + val) }} const line = "<span style={style!r}><b>" + cb_obj.event_name + "</b>(" + args.join(", ") + ")</span>\\n"; const text = div.text.concat(line); const lines = text.split("\\n") if (lines.length > 35) lines.shift(); div.text = lines.join("\\n"); """) # 部分注册示例 button.js_on_event(events.ButtonClick, display_event(div)) text_input.js_on_event(events.ValueSubmit, display_event(div, ["value"])) p.js_on_event(events.Tap, display_event(div, attributes=['x','y','sx','sy'])) p.js_on_event(events.Pan, display_event(div, attributes=[*point_attributes, 'delta_x', 'delta_y'])) p.js_on_event(events.SelectionGeometry, display_event(div, attributes=['geometry', 'final']))

注意:事件没有节流(throttling)。像MouseMove这类事件可能以非常高的频率触发,注册此类回调时需考虑性能影响(见 events.py 的模块说明)。

文档级事件:Document.js_on_event

文档级(document)事件回调通过Document.js_on_event()方法注册。在独立嵌入(standalone embedding)模式下,需要通过curdoc()获取当前文档来设置:

from bokeh.models import Div from bokeh.models.callbacks import CustomJS from bokeh.io import curdoc, show div = Div() # execute a callback when the document is fully rendered callback = CustomJS(args=dict(div=div, code="""div.text = "READY!"""") curdoc().js_on_event("document_ready", callback) show(div)

与模型级事件类似,文档事件也可用事件类代替事件名注册:

from bokeh.events import DocumentReady curdoc().js_on_event(DocumentReady, callback)

仓库示例 examples/interaction/js_callbacks/doc_js_events.py 进一步展示了文档事件的完整用法:document_ready(文档渲染完成)、connection_lost(连接丢失)以及按钮点击,均同时注册了 Python 回调(on_event)与 JS 回调(js_on_event),并将事件信息插入页面:

js_ready = CustomJS(code=""" const html = "<div>READY!</div>" document.body.insertAdjacentHTML("beforeend", html) """) curdoc().on_event("document_ready", py_ready) curdoc().js_on_event("document_ready", js_ready)

实战示例:各类模型的 CustomJS 应用

原文档提供了六个覆盖面极广的实战示例,全部位于 examples/interaction/js_callbacks/ 目录,可直接运行验证。

控件回调(CustomJS for widgets)

属性回调最常见的用途就是响应控件变化。customjs_for_widgets.py 展示了滑块驱动折线数据更新的完整代码(与上文js_on_change.py逻辑一致,此处不再重复粘贴):Slider的值作为指数f,回调用Math.pow(x, f)重算整条曲线,实现"指数可调"的幂函数可视化。

选择回调(CustomJS for selections)

一种常见需求是:每当选择(selection)变化时执行同一类回调。customjs_for_selection.py 用两个图演示:在左侧用lasso_select框选散点,右侧图立即显示被选中的点。关键在于监听s1.selected.js_on_change('indices', ...),并在回调中读取cb_obj.indices

s1.selected.js_on_change('indices', CustomJS(args=dict(s1=s1, s2=s2), code=""" const inds = cb_obj.indices const d1 = s1.data const x = Array.from(inds, (i) => d1.x[i]) const y = Array.from(inds, (i) => d1.y[i]) s2.data = {x, y} """), )

更进阶的 customjs_lasso_mean.py 计算所有被选点(包括多段不相交选择)的y平均值,并在图上绘制一条经过该值的水平线:

s.selected.js_on_change('indices', CustomJS(args=dict(s=s, s2=s2), code=""" const inds = s.selected.indices if (inds.length > 0) { const ym = inds.reduce((a, b) => a + s.data.y[b], 0) / inds.length s2.data = { x: s2.data.x, ym: [ym, ym] } } """))

范围回调(CustomJS for ranges)

范围(range)对象的属性同样可以挂接CustomJS,以便在范围变化时执行特定工作。customjs_for_range_update.py 在左图平移/缩放时,把x_rangey_rangestart/end同步到右图上的BoxAnnotation,从而"镜像"出当前的缩放窗口:

p1.x_range.js_on_change('start', xcb) p1.x_range.js_on_change('end', xcb) p1.y_range.js_on_change('start', ycb) p1.y_range.js_on_change('end', ycb)

其中xcb/ycb通过字符串格式化生成两份CustomJS,分别更新box.left/rightbox.bottom/top

工具回调(CustomJS for tools)

选择类工具会发射事件,可用来驱动有价值的回调。customjs_for_tools.py 中,SelectionGeometry回调通过cb_obj.geometryBoxSelectTool的框选几何)把每次框选的矩形追加到Quad字形上:

from bokeh.events import SelectionGeometry from bokeh.models import ColumnDataSource, CustomJS, Quad callback = CustomJS(args=dict(source=source), code=""" const geometry = cb_obj.geometry const data = source.data // quad is forgiving if left/right or top/bottom are swapped source.data = { left: data.left.concat([geometry.x0]), right: data.right.concat([geometry.x1]), top: data.top.concat([geometry.y0]), bottom: data.bottom.concat([geometry.y1]) } """) p.js_on_event(SelectionGeometry, callback)

Hover 工具专用回调(CustomJS for hover tool)

除通用机制外,还有部分 Bokeh 模型带有专门用于执行CustomJS.callback属性。需要留意的是:这些回调在 Bokeh 早期以临时方式加入,其中多数可以用上文所述的通用机制实现,未来可能被通用机制取代(原文档对此有明确警告)。

HoverTool的回调自带两块内建数据:index(悬停所覆盖点的索引)与geometry。customjs_for_hover.py 利用悬停索引动态绘制"悬停点到其邻接点"的连线:

code = """ const data = {x0: [], y0: [], x1: [], y1: []} const {indices} = cb_data.index for (const start of indices) { for (const end of links.get(start)) { data.x0.push(circle.data.x[start]) data.y0.push(circle.data.y[start]) data.x1.push(circle.data.x[end]) data.y1.push(circle.data.y[end]) } } segment.data = data """ callback = CustomJS(args=dict(circle=cr.data_source, segment=sr.data_source, links=links), code=code) p.add_tools(HoverTool(tooltips=None, callback=callback, renderers=[cr]))

注意这里index/geometry通过cb_data传入,印证了前文所述"选择类工具/悬停工具用data提供索引与几何"的行为。

OpenURL:点击字形打开链接

点击字形(如圆形标记)打开 URL 是非常受欢迎的功能。Bokeh 通过OpenURL回调对象实现:把它传给TapTool,即可在用户点击字形时执行打开链接的动作。模型定义见 callbacks.py,支持两个属性:

  • url:浏览器要跳转的地址,可以是模板字符串,会用数据源中的字段格式化(如@color会被替换为当前行的color列值);
  • same_tabFalse(默认)在新标签页/窗口打开,True在当前标签页打开;注意same_tab=False时究竟开新标签还是新窗口由浏览器决定。

仓库示例 examples/interaction/js_callbacks/open_url.py:

from bokeh.models import ColumnDataSource, OpenURL, TapTool from bokeh.plotting import figure, show p = figure(width=400, height=400, tools="tap", title="Click the Dots") source = ColumnDataSource(data=dict( x=[1, 2, 3, 4, 5], y=[2, 5, 8, 2, 7], color=["navy", "orange", "olive", "firebrick", "gold"], )) p.scatter('x', 'y', color='color', size=20, source=source) # use the "color" column of the CDS to complete the URL # e.g. if the glyph at index 10 is selected, then @color # will be replaced with source.data['color'][10] url = "https://www.html-color-names.com/@color.php" taptool = p.select(type=TapTool) taptool.callback = OpenURL(url=url) show(p)

需要特别说明两点限制:OpenURL只与TapTool配合使用,且仅在字形被命中(hit)时才触发——它不会在每次鼠标点击时执行。如果你想在每次点击时都执行回调,请使用前文的js_on_event机制(如p.js_on_event('tap', ...))。

小结:如何选择回调方案

综合原文档与源码,可按以下思路快速决策:

  • 只想把"事件"绑定到"简单属性赋值"(如按钮点击改文案)→ 用SetValue,自带属性/类型校验,代码最少;
  • 想链接两个模型之间的属性(如滑块 ↔ 绘图范围)→ 优先用js_link,完全零代码;
  • 需要复杂计算、外部库导入、状态保持或多步操作 → 用CustomJS,推荐新式 ES 模块写法(export default (args, obj, data, context) => {}),复杂逻辑可拆到.mjs文件用from_file加载;
  • 响应对象状态变化 → 用js_on_change;响应浏览器交互事件(点击、平移、缩放、LOD、文档就绪等)→ 用js_on_event,事件名与事件类两种写法等价;
  • 最后,无论采用哪种方式,凡涉及不可信输入拼接进代码,都必须先 sanitize,这是CustomJS模型反复强调的安全底线。

【免费下载链接】bokehInteractive Data Visualization in the browser, from Python项目地址: https://gitcode.com/GitHub_Trending/bo/bokeh

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询