JavaScript callbacks¶
While the main goal of Bokeh is to provide a path to create rich interactive visualizations in the browser purely from Python, there will always be specialized use-cases that are outside the capabilities of the core library. For this reason, Bokeh provides different ways for users to supply custom JavaScript when necessary, so that users may add custom or specialized behaviors in response to property changes and other events.
One mechanism is the ability to add entire new custom extension models, as described in Extending Bokeh. However, it is also possible to supply small snippets of JavaScript as callbacks to use, e.g when property values change or when UI or other events occur. This kind of callback can be used to add interesting interactions to Bokeh documents without requiring a Bokeh server (but can also be used in conjunction with a Bokeh server).
Warning
The explicit purpose of these callbacks is to embed raw JavaScript code for a browser to execute. If any part of the code is derived from untrusted user inputs, then you must take appropriate care to sanitize the user input prior to passing it to Bokeh.
CustomJS callbacks¶
To supply a snippet of JavaScript code that should be executed (in the
browser) when some event occurs, use the CustomJS
model:
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 automagically available
xr.start = a;
xr.end = b;
""")
Note that in addition to the code
property, CustomJS
also accepts
an args
property that maps string names to Bokeh models. Any Bokeh
models that are configured in args
(on the “Python side”) will
automatically be available to the JavaScript code by the corresponding name.
Additionally, the model that triggers the callback (that is the model that
the callback is attached to) will be available as cb_obj
.
CustomJS for model property events¶
These CustomJS
callbacks can be attached to property change events on
any Bokeh model, using the js_on_change
method of Bokeh models:
p = figure()
# execute a callback whenever p.x_range.start changes
p.x_range.js_on_change('start', callback)
It should be mentioned that the first parameter to js_on_change
is
actually the name of a BokehJS event. The full format for a property
change event is, for example, "change:start"
, but Bokeh will automatically
convert any property name into one of these BokehJS change events for you.
Additionally, some Bokeh models have additional specialized events. For
example, the ColumnDataSource
also supports "patch"
and "stream"
events, for executing CustomJS
callbacks whenever the data source is
patched or streamed to.
Below is an example that shows how to attach a CustomJS
callback to a
Slider
widget, so that whenever the slider value updates, the callback
is executed to update some data:
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(width=400, height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
callback = CustomJS(args=dict(source=source), code="""
const data = source.data;
const f = cb_obj.value
const x = data['x']
const y = data['y']
for (let 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 for user interaction events¶
In addition to responding to property change events using js_on_change, Bokeh allows CustomJS callbacks to be triggered by specific interaction events with the plot canvas, on button click events, and on LOD events.
These event callbacks are defined on models using the js_on_event method, with the callback receiving the event object as a locally defined cb_obj variable:
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)
The event can be specified as a string such as 'tap'
above, or an event
class import from the bokeh.events
module
(i.e. from bokeh.events import Tap
).
The following code imports bokeh.events
and registers all of the
available event classes using the display_event
function in order to
generate the CustomJS
objects. This function is used to update the Div
with the event name (always accessible from the event_name
attribute) as well as all the other applicable event attributes. The
result is a plot that displays the corresponding event on the right when the
user interacts with it:
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="""
const attrs = %s;
const args = [];
for (let i = 0; i<attrs.length; i++) {
args.push(attrs[i] + '=' + Number(cb_obj[attrs[i]]).toFixed(2));
}
const line = "<span 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");
""" % (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.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)
Examples¶
CustomJS for widgets¶
A common use case for property callbacks is responding to changes to widgets.
The code below shows an example of CustomJS
set on a slider Widget that
changes the source of a plot when the slider is used.
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(width=400, height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
callback = CustomJS(args=dict(source=source), code="""
const data = source.data;
const f = cb_obj.value
const x = data['x']
const y = data['y']
for (let 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 for selections¶
Another common scenario is wanting to specify the same kind of callback to be executed whenever a selection changes. As a simple demonstration, the example below simply copies selected points on the first plot to the second. However, more sophisticated actions and computations are easily constructed in a similar way.
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(width=400, 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(width=400, 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="""
const inds = cb_obj.indices;
const d1 = s1.data;
const d2 = s2.data;
d2['x'] = []
d2['y'] = []
for (let 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)
Another more sophisticated example is shown below. It computes the average y value of any selected points (including multiple disjoint selections) and draws a line through that value.
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(width=400, 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;
let ym = 0
if (inds.length == 0)
return;
for (let i = 0; i < d['color'].length; i++) {
d['color'][i] = "navy"
}
for (let 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 for ranges¶
The properties of range objects may also be connected to CustomJS
callbacks
in order to perform specialized work whenever a range changes:
import numpy as np
from bokeh.layouts import row
from bokeh.models import BoxAnnotation, CustomJS
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)
]
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.scatter(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.scatter(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 for tools¶
Selection tools emit events that can drive useful callbacks. Below, a
callback for SelectionGeometry
uses the BoxSelectTool
geometry (accessed
via the geometry field of the cb_data
callback object), in order to update a
Rect
glyph.
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(width=400, 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 for specialized events¶
In addition to the generic mechanisms described above for adding CustomJS
callbacks to Bokeh models, there are also some Bokeh models that have a
.callback
property specifically for executing CustomJS
in response
to specific events or situations.
Warning
The callbacks described below were added early to Bokeh in an ad-hoc fashion. Many of them can be accomplished with the generic mechanism described above, and as such, may be deprecated in favor of the generic mechanism in the future.
CustomJS for hover tool¶
The HoverTool
has a callback which comes with two pieces of built-in data:
the index
and the geometry
. The index
is the indices of any points
that the hover tool is over.
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(width=400, 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 (let i = 0; i < indices.length; i++) {
const start = indices[i]
for (let 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)
OpenURL¶
Opening an URL when users click on a glyph (for instance a circle marker) is a very popular feature. Bokeh lets users enable this feature by exposing an OpenURL callback object that can be passed to a Tap tool in order to have that action called whenever the user clicks on the glyph.
The following code shows how to use the OpenURL action combined with a TapTool to open an URL whenever the user clicks on a circle.
from bokeh.models import ColumnDataSource, OpenURL, TapTool
from bokeh.plotting import figure, output_file, show
output_file("openurl.html")
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.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)
Please note that OpenURL
callbacks specifically and only work with
TapTool
, and are only invoked when a glyph is hit. That is, they do not
execute on every tap. If you would like to execute a callback on every
mouse tap, please see CustomJS for user interaction events.