JavaScript callbacks (original) (raw)

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:

A JavaScript callback is triggered when certain events occur in the browser. There are two main types of JavaScript callback triggers:

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:

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, 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

} """)

The code snippet must contain a default export, which must be a function defined either using arrow function syntax () => {} or a classical function syntaxfunction() {}. Depending on the context, this function may be an async function, a generator function or an async generator function. Also depending on the context, this function may or may not need to return a value.

The callback function uses four positional arguments:

It may be convenient to the user to use object destructuring syntax to gain immediate access to passed values, for example:

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 } """)

Code snippet is compiled once and the callback function (the default export) can be evaluated multiple times. This way the user can robustly and efficiently import external libraries, define complex classes and data structures and maintain state between calls of the callback function. The code snippet is recompiled only when properties of CustomJS instance change.

Alternatively the user can use the legacy variant of CustomJS, where the code snippet is the body of the implicit callback function:

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 distinguishes both approaches by detecting presence or absence ofimport and export syntax in the code snippet.

In this approach, arguments to the callback function are implicitly defined. Names provided by CustomJS.args are immediately available as positional arguments, whereas obj, data and context are all available withcb_ prefix, i.e. cb_obj, cb_data and cb_context.

Finally the user can create CustomJS from files, which use useful when dealing with large and/or complex code snippets:

from bokeh.models.callbacks import CustomJS

callback = CustomJS.from_file("./my_module.mjs", xr=plot.x_range)

The allowed extensions are:

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)

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_objvariable:

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 Divwith the event name (always accessible from the event_nameattribute) 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 = "" + cb_obj.event_name + "(" + args.join(", ") + ")\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+2x, 30+2y)], 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 callbacks for document events can be registered 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)

Similarly 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.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)

Another more sophisticated example is shown below. It computes the average yvalue 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)] 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 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+2x, 30+2y)], 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 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 aRect 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 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 for topics events#

In addition to the generic mechanisms described above for adding CustomJScallbacks 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(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#

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.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)

Please note that OpenURL callbacks specifically and only work withTapTool, 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.