JavaScript回调

虽然Bokeh的主要目标是提供一条纯粹从Python在浏览器中创建丰富的交互式可视化的路径,但是总会有一些超出核心库功能的专用用例。因此,Bokeh在必要时为用户提供了不同的方式来提供定制JavaScript,这样用户就可以添加自定义或专门的行为来响应属性更改和其他事件。

一种机制是能够添加整个新的自定义扩展模型,如中所述 扩展Bokeh . 但是,也可以提供一些JavaScript片段作为回调使用,例如当属性值更改或UI或其他事件发生时。这种回调可以用来向Bokeh文档添加有趣的交互,而不需要Bokeh服务器(但也可以与Bokeh服务器一起使用)。

警告

这些回调的明确目的是嵌入 原始JavaScript代码 用于执行浏览器。如果代码的任何部分是从不受信任的用户输入派生的,那么在将用户输入传递给Bokeh之前,您必须谨慎处理用户输入。

CustomJS回调

要提供在发生某些事件时(在浏览器中)执行的JavaScript代码片段,请使用 CustomJS 模型:

from bokeh.models.callbacks import CustomJS

callback = CustomJS(args=dict(xr=plot.x_range), code="""

// JavaScript code goes here

var a = 10;

// the model that triggered the callback is cb_obj:
var b = cb_obj.value;

// models passed as args are automagically available
xr.start = a;
xr.end = b;

""")

请注意,除了 code 财产, CustomJS 也接受 args 属性,该属性将字符串名称映射到Bokeh模型。在中配置的任何Bokeh型号 args (在“Python端”)将自动由相应的名称提供给JavaScript代码。此外,触发回调的模型(即回调附加到的模型)将作为 cb_obj .

模型属性事件的CustomJS

这些 CustomJS 回调可以附加到任何Bokeh模型上的属性更改事件,使用 js_on_change Bokeh模型方法:

p = figure()

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

应该提到的是,第一个参数 js_on_change 实际上是BokehJS事件的名称。属性更改事件的完整格式是,例如。, "change:start" ,但Bokeh会自动将任何属性名转换为这些bokehj更改事件之一。此外,一些Bokeh模型有额外的专门活动。例如 ColumnDataSource 也支持 "patch""stream" 事件,用于执行 CustomJS 每当数据源被修补或流式传输到时进行回调。

下面是一个示例,演示如何附加 CustomJS 回调到 Slider 小部件,因此每当滑块值更新时,都会执行回调来更新某些数据:

from bokeh.layouts import column
from bokeh.models import ColumnDataSource, CustomJS, Slider
from bokeh.plotting import Figure, output_file, show

output_file("js_on_change.html")

x = [x*0.005 for x in range(0, 200)]
y = x

source = ColumnDataSource(data=dict(x=x, y=y))

plot = Figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)

callback = CustomJS(args=dict(source=source), code="""
    var data = source.data;
    var f = cb_obj.value
    var x = data['x']
    var y = data['y']
    for (var i = 0; i < x.length; i++) {
        y[i] = Math.pow(x[i], f)
    }
    source.change.emit();
""")

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)

用于用户交互事件的CustomJS

除了使用js_on_change响应属性更改事件外,Bokeh还允许CustomJS回调由与绘图画布、按钮单击事件和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 属性)以及所有其他适用的事件属性。当用户与之交互时,结果是在右侧显示相应事件的绘图:

""" Demonstration of how to register event callbacks using an adaptation
of the color_scatter example from the Bokeh gallery
"""
import numpy as np

from bokeh import events
from bokeh.io import output_file, show
from bokeh.layouts import column, row
from bokeh.models import Button, CustomJS, Div
from bokeh.plotting import figure


def display_event(div, attributes=[], style = 'float:left;clear:left;font_size=13px'):
    "Build a suitable CustomJS to display the current event in the div model."
    return CustomJS(args=dict(div=div), code="""
        var attrs = %s; var args = [];
        for (var i = 0; i<attrs.length; i++) {
            args.push(attrs[i] + '=' + Number(cb_obj[attrs[i]]).toFixed(2));
        }
        var line = "<span style=%r><b>" + cb_obj.event_name + "</b>(" + args.join(", ") + ")</span>\\n";
        var text = div.text.concat(line);
        var lines = text.split("\\n")
        if (lines.length > 35)
            lines.shift();
        div.text = lines.join("\\n");
    """ % (attributes, style))

x = np.random.random(size=4000) * 100
y = np.random.random(size=4000) * 100
radii = np.random.random(size=4000) * 1.5
colors = ["#%02x%02x%02x" % (int(r), int(g), 150) for r, g in zip(50+2*x, 30+2*y)]

p = figure(tools="pan,wheel_zoom,zoom_in,zoom_out,reset")
p.scatter(x, y, radius=np.random.random(size=4000) * 1.5,
          fill_color=colors, fill_alpha=0.6, line_color=None)

div = Div(width=400, height=p.plot_height, height_policy="fixed")
button = Button(label="Button", button_type="success")
layout = column(button, row(p, div))

## Events with no attributes
button.js_on_event(events.ButtonClick, display_event(div)) # Button click
p.js_on_event(events.LODStart, display_event(div))         # Start of LOD display
p.js_on_event(events.LODEnd, display_event(div))           # End of LOD display

## Events with attributes
point_attributes = ['x', 'y', 'sx', 'sy']                  # Point events
wheel_attributes = point_attributes + ['delta']            # Mouse wheel event
pan_attributes = point_attributes + ['delta_x', 'delta_y'] # Pan event
pinch_attributes = point_attributes + ['scale']            # Pinch event

point_events = [
    events.Tap, events.DoubleTap, events.Press, events.PressUp,
    events.MouseMove, events.MouseEnter, events.MouseLeave,
    events.PanStart, events.PanEnd, events.PinchStart, events.PinchEnd,
]

for event in point_events:
    p.js_on_event(event, display_event(div, attributes=point_attributes))

p.js_on_event(events.MouseWheel, display_event(div, attributes=wheel_attributes))
p.js_on_event(events.Pan,        display_event(div, attributes=pan_attributes))
p.js_on_event(events.Pinch,      display_event(div, attributes=pinch_attributes))

output_file("js_events.html", title="JS Events Example")
show(layout)

实例

小部件的CustomJS

属性回调的一个常见用例是响应小部件的更改。下面的代码显示了 CustomJS 设置在一个滑块小部件上,当使用滑块时,它会更改绘图源。

from bokeh.layouts import column
from bokeh.models import ColumnDataSource, CustomJS, Slider
from bokeh.plotting import figure, output_file, show

output_file("callback.html")

x = [x*0.005 for x in range(0, 200)]
y = x

source = ColumnDataSource(data=dict(x=x, y=y))

plot = figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)

callback = CustomJS(args=dict(source=source), code="""
        var data = source.data;
        var f = cb_obj.value
        var x = data['x']
        var y = data['y']
        for (var i = 0; i < x.length; i++) {
            y[i] = Math.pow(x[i], f)
        }
        source.change.emit();
    """)

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

另一个常见的场景是希望指定在选择更改时要执行的相同类型的回调。作为一个简单的演示,下面的示例简单地将第一个图上的选定点复制到第二个图上。然而,更复杂的动作和计算很容易用类似的方式构造。

from random import random

from bokeh.layouts import row
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.plotting import figure, output_file, show

output_file("callback.html")

x = [random() for x in range(500)]
y = [random() for y in range(500)]

s1 = ColumnDataSource(data=dict(x=x, y=y))
p1 = figure(plot_width=400, plot_height=400, tools="lasso_select", title="Select Here")
p1.circle('x', 'y', source=s1, alpha=0.6)

s2 = ColumnDataSource(data=dict(x=[], y=[]))
p2 = figure(plot_width=400, plot_height=400, x_range=(0, 1), y_range=(0, 1),
            tools="", title="Watch Here")
p2.circle('x', 'y', source=s2, alpha=0.6)

s1.selected.js_on_change('indices', CustomJS(args=dict(s1=s1, s2=s2), code="""
        var inds = cb_obj.indices;
        var d1 = s1.data;
        var d2 = s2.data;
        d2['x'] = []
        d2['y'] = []
        for (var i = 0; i < inds.length; i++) {
            d2['x'].push(d1['x'][inds[i]])
            d2['y'].push(d1['y'][inds[i]])
        }
        s2.change.emit();
    """)
)

layout = row(p1, p2)

show(layout)

另一个更复杂的例子如下所示。它计算平均值 y 任何选定点的值(包括多个不相交的选择),并通过该值绘制一条直线。

from random import random

from bokeh.models import ColumnDataSource, CustomJS
from bokeh.plotting import figure, output_file, show

output_file("callback.html")

x = [random() for x in range(500)]
y = [random() for y in range(500)]
color = ["navy"] * len(x)

s = ColumnDataSource(data=dict(x=x, y=y, color=color))
p = figure(plot_width=400, plot_height=400, tools="lasso_select", title="Select Here")
p.circle('x', 'y', color='color', size=8, source=s, alpha=0.4)

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;
    const d = s.data;
    var ym = 0

    if (inds.length == 0)
        return;

    for (var i = 0; i < d['color'].length; i++) {
        d['color'][i] = "navy"
    }
    for (var i = 0; i < inds.length; i++) {
        d['color'][inds[i]] = "firebrick"
        ym += d['y'][inds[i]]
    }

    ym /= inds.length
    s2.data['ym'] = [ym, ym]

    s.change.emit();
    s2.change.emit();
"""))

show(p)

自定义范围

范围对象的属性也可以连接到 CustomJS 回调,以便在范围发生变化时执行专门的工作:

import numpy as np

from bokeh.layouts import row
from bokeh.models import ColumnDataSource, CustomJS, Rect
from bokeh.plotting import figure, output_file, show

output_file('range_update_callback.html')

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 = [
    "#%02x%02x%02x" % (int(r), int(g), 150) for r, g in zip(50+2*x, 30+2*y)
]

source = ColumnDataSource({'x': [], 'y': [], 'width': [], 'height': []})

jscode = """
    const data = source.data
    const start = cb_obj.start
    const end = cb_obj.end
    data[%r] = [start + (end - start) / 2]
    data[%r] = [end - start]
    source.change.emit()
"""

p1 = figure(title='Pan and Zoom Here', x_range=(0, 100), y_range=(0, 100),
            tools='box_zoom,wheel_zoom,pan,reset', plot_width=400, plot_height=400)
p1.scatter(x, y, radius=radii, fill_color=colors, fill_alpha=0.6, line_color=None)

xcb = CustomJS(args=dict(source=source), code=jscode % ('x', 'width'))
ycb = CustomJS(args=dict(source=source), code=jscode % ('y', 'height'))

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='', plot_width=400, plot_height=400)
p2.scatter(x, y, radius=radii, fill_color=colors, fill_alpha=0.6, line_color=None)

rect = Rect(x='x', y='y', width='width', height='height', fill_alpha=0.1,
            line_color='black', fill_color='black')
p2.add_glyph(source, rect)

layout = row(p1, p2)

show(layout)

自定义工具

选择工具发出可以驱动有用回调的事件。下面是 SelectionGeometry 使用 BoxSelectTool 通过几何图形 cb_data 回调对象),以便更新 Rect 字形。

from bokeh.events import SelectionGeometry
from bokeh.models import ColumnDataSource, CustomJS, Rect
from bokeh.plotting import figure, output_file, show

output_file("box_select_tool_callback.html")

source = ColumnDataSource(data=dict(x=[], y=[], width=[], height=[]))

callback = CustomJS(args=dict(source=source), code="""
    const geometry = cb_obj['geometry']
    const data = source.data

    // calculate Rect attributes
    const width = geometry['x1'] - geometry['x0']
    const height = geometry['y1'] - geometry['y0']
    const x = geometry['x0'] + width/2
    const y = geometry['y0'] + height/2

    // update data source with new Rect attributes
    data['x'].push(x)
    data['y'].push(y)
    data['width'].push(width);
    data['height'].push(height)

    // emit update of data source
    source.change.emit()
""")

p = figure(plot_width=400, plot_height=400, tools="box_select",
           title="Select Below", x_range=(0, 1), y_range=(0, 1))

rect = Rect(x='x', y='y', width='width', height='height',
            fill_alpha=0.3, fill_color='#009933')

p.add_glyph(source, rect, selection_glyph=rect, nonselection_glyph=rect)

p.js_on_event(SelectionGeometry, callback)

show(p)

专门活动的CustomJS

除了上述用于添加 CustomJS 对Bokeh模型的回调,也有一些Bokeh模型 .callback 专门用于执行的属性 CustomJS 针对特定事件或情况作出反应。

警告

下面描述的回调是在Bokeh的早期临时添加的。其中许多可以通过上面描述的泛型机制来实现,因此,将来可能会反对使用泛型机制。

用于悬停的自定义JS

这个 HoverTool 有一个回调函数,其中包含两个内置数据: index 以及 geometry . 这个 index 悬停工具所在点的索引。

from bokeh.models import ColumnDataSource, CustomJS, HoverTool
from bokeh.plotting import figure, output_file, show

output_file("hover_callback.html")

# 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(plot_width=400, plot_height=400, tools="", toolbar_location=None, title='Hover over points')

source = ColumnDataSource({'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.circle(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 links = %s
const data = {'x0': [], 'y0': [], 'x1': [], 'y1': []}
const indices = cb_data.index.indices
for (var i = 0; i < indices.length; i++) {
    const start = indices[i]
    for (var j = 0; j < links[start].length; j++) {
        const end = links[start][j]
        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
""" % links

callback = CustomJS(args={'circle': cr.data_source, 'segment': sr.data_source}, code=code)
p.add_tools(HoverTool(tooltips=None, callback=callback, renderers=[cr]))

show(p)

开放URL

当一个用户很流行的一个打开标志符号时,一个非常流行的图标。Bokeh允许用户通过公开OpenURL回调对象来启用此功能,该对象可以传递给Tap工具,以便在用户单击glyph时调用该操作。

下面的代码演示了如何使用OpenURL操作和TapTool相结合,在用户单击圆圈时打开URL。

from bokeh.models import ColumnDataSource, OpenURL, TapTool
from bokeh.plotting import figure, output_file, show

output_file("openurl.html")

p = figure(plot_width=400, plot_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.circle('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 = "http://www.html-color-names.com/@color.php"
taptool = p.select(type=TapTool)
taptool.callback = OpenURL(url=url)

show(p)

请注意 OpenURL 回调专门用于 TapTool ,并且仅在命中字形时调用。也就是说,它们不是每次点击都执行。如果要在每次鼠标点击时执行回调,请参阅 用于用户交互事件的CustomJS .