JavaScript 回调函数#
Bokeh的主要目标是为在浏览器中仅使用Python创建丰富的交互式可视化提供途径。然而,总会有一些用例超出了预定义核心库的能力范围。
因此,Bokeh 提供了不同的方式,让用户在必要时提供自定义的 JavaScript。这样,您可以在浏览器中响应属性变化和其他事件时,添加自定义或主题行为。
注意
顾名思义,JavaScript回调是在浏览器中执行的JavaScript代码片段。如果您正在寻找仅基于Python并且可以在Bokeh服务器上运行的交互式回调,请参阅Python回调。
主要有三种生成JavaScript回调的选项:
使用
js_linkPython便捷方法。此方法帮助你将不同模型的属性链接在一起。使用此方法,Bokeh会自动为你创建必要的JavaScript代码。详情请参见Linked behavior。使用
SetValuePython 对象根据另一个对象的特定事件动态设置一个对象的属性。有关更多信息,请参阅 SetValue 回调。使用
CustomJS对象编写自定义JavaScript代码。更多信息请参见 CustomJS回调。
当浏览器中发生某些事件时,会触发 JavaScript 回调。 主要有两种 JavaScript 回调触发类型:
大多数Bokeh对象都有一个
.js_on_change属性(例如,所有的widgets)。每当对象的状态发生变化时,分配给此属性的回调函数将被调用。有关更多信息,请参见js_on_change回调触发器。一些widgets还具有
.js_on_event属性。分配给此属性的回调函数将在浏览器中发生特定事件时被调用。
警告
CustomJS 模型的明确目的是嵌入原始 JavaScript 代码供浏览器执行。如果代码的任何部分来自不受信任的用户输入,那么在将其传递给 Bokeh 之前,必须采取适当的措施来清理用户输入。
此外,您可以通过编写自己的Bokeh扩展来添加全新的自定义扩展模型。
SetValue 回调函数#
使用 SetValue 模型在浏览器中发生事件时动态设置对象的特定属性。
SetValue 模型具有以下属性:
obj: 要设置值的对象。attr: 要修改的对象的属性。value: 为对象的属性设置的值。
基于这些参数,Bokeh 自动生成必要的 JavaScript 代码:
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)
CustomJS回调#
使用CustomJS模型来提供一段自定义的JavaScript代码片段,以便在事件发生时在浏览器中运行。
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
}
""")
代码片段必须包含一个默认导出,该导出必须是一个使用箭头函数语法 () => {} 或经典函数语法 function() {} 定义的函数。根据上下文,此函数可能是异步函数、生成器函数或异步生成器函数。同样根据上下文,此函数可能需要也可能不需要返回值。
回调函数使用四个位置参数:
args这映射到
CustomJS.args属性,允许将名称映射到可序列化的值,通常从代码片段中提供对Bokeh模型的访问。
obj这指的是发出回调的模型(这是回调所附加的模型)。
data这是由回调的发射器提供的名称和值之间的映射。这取决于调用者、事件以及事件发生的上下文。例如,选择工具将使用
data来提供选择几何等信息。
context这是由bokehjs提供的额外更广泛的上下文,它是名称和值之间的映射,类似于
data。目前只提供了index,它允许用户访问bokehjs的视图索引。
用户可能会发现使用对象解构语法来立即访问传递的值很方便,例如:
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的旧版变体,其中代码片段是隐式回调函数的主体:
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 语法来区分这两种方法。
在这种方法中,回调函数的参数是隐式定义的。
由CustomJS.args提供的名称立即作为位置参数可用,
而obj、data和context都可以通过
cb_前缀使用,即cb_obj、cb_data和cb_context。
最后,用户可以从文件中创建CustomJS,这在处理大型和/或复杂的代码片段时非常有用:
from bokeh.models.callbacks import CustomJS
callback = CustomJS.from_file("./my_module.mjs", xr=plot.x_range)
允许的扩展名有:
.mjs用于新的export default () => {}变体.js用于遗留的CustomJS
js_on_change 回调触发器#
CustomJS 和 SetValue 回调可以附加到任何 Bokeh 模型的属性更改事件上,使用 Bokeh 模型的 js_on_change 方法:
p = figure()
# execute a callback whenever p.x_range.start changes
p.x_range.js_on_change('start', callback)
以下示例将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)
js_on_event 回调触发器#
除了使用js_on_change响应属性更改事件外,Bokeh还允许通过特定的交互事件触发CustomJS和SetValue回调,这些事件包括与绘图画布的交互、按钮点击事件、LOD(细节级别)事件以及文档事件。
这些事件回调是通过使用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)。
以下代码导入了 bokeh.events 并使用 display_event 函数注册所有可用的事件类,以生成 CustomJS 对象。此函数用于更新 Div,显示事件名称(始终可从 event_name 属性访问)以及所有其他适用的事件属性。结果是当用户与之交互时,右侧会显示相应事件的图表:
from __future__ import annotations
import numpy as np
from bokeh import events
from bokeh.io import curdoc, show
from bokeh.layouts import column, row
from bokeh.models import Button, CustomJS, Div, TextInput
from bokeh.plotting import figure
def display_event(div: Div, attributes: list[str] = []) -> CustomJS:
"""
Function to build a suitable CustomJS to display the current event
in the div model.
"""
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");
""")
N = 4000
x = np.random.random(size=N) * 100
y = np.random.random(size=N) * 100
radii = np.random.random(size=N) * 1.5
colors = np.array([(r, g, 150) for r, g in zip(50+2*x, 30+2*y)], dtype="uint8")
p = figure(tools="pan,wheel_zoom,zoom_in,zoom_out,reset,tap,lasso_select,box_select,box_zoom,undo,redo")
p.circle(x, y, radius=radii, fill_color=colors, fill_alpha=0.6, line_color=None)
# Add a div to display events and a button to trigger button click events
div = Div(width=1000)
button = Button(label="Button", button_type="success", width=300)
text_input = TextInput(placeholder="Input a value and press Enter ...", width=300)
layout = column(button, text_input, row(p, div))
# Register event callbacks
# Button events
button.js_on_event(events.ButtonClick, display_event(div))
# TextInput events
text_input.js_on_event(events.ValueSubmit, display_event(div, ["value"]))
# AxisClick events
p.xaxis[0].js_on_event(events.AxisClick, display_event(div, ["value"]))
p.yaxis[0].js_on_event(events.AxisClick, display_event(div, ["value"]))
# LOD events
p.js_on_event(events.LODStart, display_event(div))
p.js_on_event(events.LODEnd, display_event(div))
# Point events
point_attributes = ['x','y','sx','sy']
p.js_on_event(events.Tap, display_event(div, attributes=point_attributes))
p.js_on_event(events.DoubleTap, display_event(div, attributes=point_attributes))
p.js_on_event(events.Press, display_event(div, attributes=point_attributes))
p.js_on_event(events.PressUp, display_event(div, attributes=point_attributes))
# Mouse wheel event
p.js_on_event(events.MouseWheel, display_event(div,attributes=[*point_attributes, 'delta']))
# Mouse move, enter and leave
# p.js_on_event(events.MouseMove, display_event(div, attributes=point_attributes))
p.js_on_event(events.MouseEnter, display_event(div, attributes=point_attributes))
p.js_on_event(events.MouseLeave, display_event(div, attributes=point_attributes))
# Pan events
pan_attributes = [*point_attributes, 'delta_x', 'delta_y']
p.js_on_event(events.Pan, display_event(div, attributes=pan_attributes))
p.js_on_event(events.PanStart, display_event(div, attributes=point_attributes))
p.js_on_event(events.PanEnd, display_event(div, attributes=point_attributes))
# Pinch events
pinch_attributes = [*point_attributes, 'scale']
p.js_on_event(events.Pinch, display_event(div, attributes=pinch_attributes))
p.js_on_event(events.PinchStart, display_event(div, attributes=point_attributes))
p.js_on_event(events.PinchEnd, display_event(div, attributes=point_attributes))
# Ranges Update events
p.js_on_event(events.RangesUpdate, display_event(div, attributes=['x0','x1','y0','y1']))
# Selection events
p.js_on_event(events.SelectionGeometry, display_event(div, attributes=['geometry', 'final']))
curdoc().on_event(events.DocumentReady, display_event(div))
show(layout)
文档事件的JS回调可以通过Document.js_on_event()方法注册。在独立嵌入模式的情况下,将通过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)
类似于模型级别的JS事件,也可以使用事件类代替事件名称来注册文档事件回调:
from bokeh.events import DocumentReady
curdoc().js_on_event(DocumentReady, callback)
示例#
小部件的CustomJS#
属性回调的一个常见用例是响应小部件的更改。
下面的代码展示了在滑块小部件上设置的CustomJS的示例,当使用滑块时,它会更改绘图的源。
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)
show(column(slider, plot))
选择的自定义JS#
另一个常见的场景是希望在每次选择更改时执行相同类型的回调。作为一个简单的演示,下面的示例只是将第一个图表上选中的点复制到第二个图表上。然而,更复杂的操作和计算可以以类似的方式轻松构建。
from random import random
from bokeh.layouts import row
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.plotting import figure, show
x = [random() for x in range(500)]
y = [random() for y in range(500)]
s1 = ColumnDataSource(data=dict(x=x, y=y))
p1 = figure(width=400, height=400, tools="lasso_select", title="Select Here")
p1.scatter('x', 'y', source=s1, alpha=0.6)
s2 = ColumnDataSource(data=dict(x=[], y=[]))
p2 = figure(width=400, height=400, x_range=(0, 1), y_range=(0, 1),
tools="", title="Watch Here")
p2.scatter('x', 'y', source=s2, alpha=0.6)
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}
"""),
)
layout = row(p1, p2)
show(layout)
下面展示了一个更复杂的例子。它计算任何选定点(包括多个不相交的选择)的平均y值,并在该值处绘制一条线。
from random import random
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.plotting import figure, show
x = [random() for x in range(500)]
y = [random() for y in range(500)]
s = ColumnDataSource(data=dict(x=x, y=y))
p = figure(width=400, height=400, tools="lasso_select", title="Select Here")
p.scatter('x', 'y', color='navy', size=8, source=s, alpha=0.4,
selection_color="firebrick")
s2 = ColumnDataSource(data=dict(x=[0, 1], ym=[0.5, 0.5]))
p.line(x='x', y='ym', color="orange", line_width=5, alpha=0.6, source=s2)
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] }
}
"""))
show(p)
范围的CustomJS#
范围对象的属性也可以连接到CustomJS回调,以便在范围发生变化时执行主题工作:
import numpy as np
from bokeh.layouts import row
from bokeh.models import BoxAnnotation, CustomJS
from bokeh.plotting import figure, show
N = 4000
x = np.random.random(size=N) * 100
y = np.random.random(size=N) * 100
radii = np.random.random(size=N) * 1.5
colors = np.array([(r, g, 150) for r, g in zip(50+2*x, 30+2*y)], dtype="uint8")
box = BoxAnnotation(left=0, right=0, bottom=0, top=0,
fill_alpha=0.1, line_color='black', fill_color='black')
jscode = """
box[%r] = cb_obj.start
box[%r] = cb_obj.end
"""
p1 = figure(title='Pan and Zoom Here', x_range=(0, 100), y_range=(0, 100),
tools='box_zoom,wheel_zoom,pan,reset', width=400, height=400)
p1.circle(x, y, radius=radii, fill_color=colors, fill_alpha=0.6, line_color=None)
xcb = CustomJS(args=dict(box=box), code=jscode % ('left', 'right'))
ycb = CustomJS(args=dict(box=box), code=jscode % ('bottom', 'top'))
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)
p2 = figure(title='See Zoom Window Here', x_range=(0, 100), y_range=(0, 100),
tools='', width=400, height=400)
p2.circle(x, y, radius=radii, fill_color=colors, fill_alpha=0.6, line_color=None)
p2.add_layout(box)
layout = row(p1, p2)
show(layout)
工具的CustomJS#
选择工具发出的事件可以驱动有用的回调。下面,一个用于SelectionGeometry的回调使用BoxSelectTool的几何形状(通过cb_data回调对象的geometry字段访问),以更新Rect字形。
from bokeh.events import SelectionGeometry
from bokeh.models import ColumnDataSource, CustomJS, Quad
from bokeh.plotting import figure, show
source = ColumnDataSource(data=dict(left=[], right=[], top=[], bottom=[]))
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 = figure(width=400, height=400, title="Select below to draw rectangles",
tools="box_select", x_range=(0, 1), y_range=(0, 1))
# using Quad model directly to control (non)selection glyphs more carefully
quad = Quad(left='left', right='right', top='top', bottom='bottom',
fill_alpha=0.3, fill_color='#009933')
p.add_glyph(
source,
quad,
selection_glyph=quad.clone(fill_color='blue'),
nonselection_glyph=quad.clone(fill_color='gray'),
)
p.js_on_event(SelectionGeometry, callback)
show(p)
主题事件的CustomJS#
除了上述描述的通用机制用于向Bokeh模型添加CustomJS回调外,还有一些Bokeh模型具有.callback属性,专门用于在响应特定事件或情况时执行CustomJS。
警告
下面描述的回调函数早期以临时方式添加到Bokeh中。其中许多可以通过上述通用机制实现,因此,未来可能会弃用这些回调函数,转而使用通用机制。
悬停工具的CustomJS#
HoverTool 有一个回调函数,它带有两个内置数据:
index 和 geometry。index 是悬停工具所覆盖的任何点的索引。
from bokeh.models import ColumnDataSource, CustomJS, HoverTool
from bokeh.plotting import figure, show
# define some points and a little graph between them
x = [2, 3, 5, 6, 8, 7]
y = [6, 4, 3, 8, 7, 5]
links = {
0: [1, 2],
1: [0, 3, 4],
2: [0, 5],
3: [1, 4],
4: [1, 3],
5: [2, 3, 4],
}
p = figure(width=400, height=400, tools="", toolbar_location=None, title='Hover over points')
source = ColumnDataSource(dict(x0=[], y0=[], x1=[], y1=[]))
sr = p.segment(x0='x0', y0='y0', x1='x1', y1='y1', color='olive', alpha=0.6, line_width=3, source=source )
cr = p.scatter(x, y, color='olive', size=30, alpha=0.4, hover_color='olive', hover_alpha=1.0)
# add a hover tool that sets the link data for a hovered circle
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]))
show(p)
OpenURL#
当用户点击一个字形(例如一个圆形标记)时打开一个URL是一个非常受欢迎的功能。Bokeh允许用户通过暴露一个OpenURL回调对象来启用此功能,该对象可以传递给Tap工具,以便在用户点击字形时调用该操作。
以下代码展示了如何将OpenURL操作与TapTool结合使用,以便在用户点击圆圈时打开一个URL。
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 一起工作,并且仅在点击到图形时被调用。也就是说,它们不会在每次点击时执行。如果您希望在每次鼠标点击时执行回调,请参阅 js_on_event 回调触发器。