JavaScript callbacks#
The main goal of Bokeh is to provide a path to create rich interactive visualizations in the browser purely from Python. However, there will always be use-cases that go beyond the capabilities of the pre-defined core library.
For this reason, Bokeh provides different ways for users to supply custom JavaScript when necessary. This way, you can add custom or topics behaviors in response to property changes and other events in the browser.
Note
As the name implies, JavaScript callbacks are snippets of JavaScript code that are executed in the browser. If you are looking for interactive callbacks that are based solely on Python and can be run with Bokeh Server, see Python callbacks.
There are mainly three options for generating a JavaScript callback:
Use the
js_link
Python convenience method. This method helps you link properties of different models together. Whith this method, Bokeh creates the necessary JavaScript code for you automatically. See Linked behavior for details.Use the
SetValue
Python object to dynamically set the property of one object depending on a specific event of another object. See SetValue callbacks for more information.Write custom JavaScript code with the
CustomJS
object. See CustomJS callbacks for more information.
A JavaScript callback is triggered when certain events occur in the browser. There are two main types of JavaScript callback triggers:
Most Bokeh objects have a
.js_on_change
property (all widgets, for example). The callback assigned to this property will be called whenever the state of the object changes. See js_on_change callback triggers for more information.Some widgets also have a
.js_on_event
property. The callback assigned to this property will be called whenever a specific event occurs in the browser.
Warning
The explicit purpose of the CustomJS
Model 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.
Additionally, you can add entire new custom extension models by writing your own Bokeh extension.
SetValue callbacks#
Use the SetValue
model to dynamically set specific
properties of an object when an event occurs in the browser.
The SetValue
model has the following properties:
obj
: The object to set the value on.attr
: The property of the object to modify.value
: The value to set for the object’s property.
Based on these parameters, Bokeh creates the necessary JavaScript code automatically:
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 callbacks#
Use the CustomJS
model to supply a custom snippet of
JavaScript code to run in the browser when an event occurs.
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
.
js_on_change
callback triggers#
CustomJS
and SetValue
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)
Some Bokeh models have additional, topics events. For example, the
ColumnDataSource
model also supports "patch"
and
"stream"
events. You can use these events to trigger CustomJS
callbacks
whenever the data source is patched or streamed to.
The following example attaches a CustomJS
callback to a Slider
widget.
Whenever the slider value updates, the callback updates the plot data with a
custom formula:
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
callback triggers#
In addition to responding to property change events using js_on_change
,
Bokeh allows CustomJS
and SetValue
callbacks to be triggered by specific
interaction events with the plot canvas, on button click events, on LOD
(Level-of-Detail) events, and document 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:
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");
""")
# Follows the color_scatter gallery example
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.scatter(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"]))
# 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 callbacks for document events can be registred with Document.js_on_event()
method. In the case of the standalone embedding mode, one will use the current
document via curdoc()
to set up such callbacks. For example:
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)
Similarily to model-level JS events, one can also use event classes in place of event names, to register document event callbacks:
from bokeh.events import DocumentReady
curdoc().js_on_event(DocumentReady, callback)
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, 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))
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, 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.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 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)
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, show
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,
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 for ranges#
The properties of range objects may also be connected to CustomJS
callbacks
in order to perform topics 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, 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.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, 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 swappeed
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, nonselection_glyph=quad)
p.js_on_event(SelectionGeometry, callback)
show(p)
CustomJS for topics 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, 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({'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, 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.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 = "https://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 js_on_event callback triggers.