JavaScript Callbacks — Anaconda Enterprise 4.4 documentation

Anaconda Enterprise: Documentation

# 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 behaviours 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 the need to use a Bokeh server (but can also be used in conjunction with a Bokeh server).

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

    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;

    """)

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 (i.e. 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 the name of actually the name of a Backbone event. The full format for a property change event is, e.g. `"change:start"` but Bokeh will automatically convert any property name into one of these Backbone 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 CustomJS, ColumnDataSource, 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
        x = data['x']
        y = data['y']
        for (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:
    var event = cb_obj.value;
    // The event type determines the relevant attributes
    console.log('Tap event occured at x-position: ' + event.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 when interacted with, displays the corresponding event on the right:

    """ 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.io import show, output_file
    from bokeh.plotting import figure
    from bokeh import events
    from bokeh.models import CustomJS, Div, Button
    from bokeh.layouts import column, row

    def display_event(div, attributes=[], style = 'float:left;clear:left;font_size=0.5pt'):
        "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=1000)
    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.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 for Specialized Events

In addition to the generic mechanisms described above for adding `CustomJS` callbacks to Bokeh models, there are also a 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 Widgets

Bokeh lets you express even more advanced callbacks that must be called on the Javascript side in order to add custom logic and interactivity when a widget is used. For instance, we may want to change the data of a plot when a user clicks on a button or changes a slider Widget.

Custom callbacks like these can be set using a CustomJS object and passing it as the `callback` argument to a Widget object.

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 CustomJS, ColumnDataSource, 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
            x = data['x']
            y = data['y']
            for (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", callback=callback)

    layout = column(slider, plot)

    show(layout)

### CustomJS for Tools

Bokeh allows for some tool events to trigger custom Javascript callbacks that have access to the tool’s attributes. Below, a callback on the BoxSelectTool uses the selection box dimensions (accessed in the geometry field of the cb_data object that is injected into the Callback code attribute), in order to add a Rect glyph to the plot with identical dimensions.

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

    output_file("boxselecttool_callback.html")

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

    callback = CustomJS(args=dict(source=source), code="""
            // get data source from Callback args
            var data = source.data;

            /// get BoxSelectTool dimensions from cb_data parameter of Callback
            var geometry = cb_data['geometry'];

            /// calculate Rect attributes
            var width = geometry['x1'] - geometry['x0'];
            var height = geometry['y1'] - geometry['y0'];
            var x = geometry['x0'] + width/2;
            var 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();
        """)

    box_select = BoxSelectTool(callback=callback)

    p = figure(plot_width=400,
               plot_height=400,
               tools=[box_select],
               title="Select Below",
               x_range=Range1d(start=0.0, end=1.0),
               y_range=Range1d(start=0.0, end=1.0))

    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)
    show(p)

### CustomJS for Selections

Bokeh also provides the means 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 CustomJS, ColumnDataSource
    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.callback = CustomJS(args=dict(s2=s2), code="""
            var inds = cb_obj.selected['1d'].indices;
            var d1 = cb_obj.data;
            var d2 = s2.data;
            d2['x'] = []
            d2['y'] = []
            for (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 CustomJS, ColumnDataSource
    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.callback = CustomJS(args=dict(s2=s2), code="""
            var inds = cb_obj.selected['1d'].indices;
            var d = cb_obj.data;
            var ym = 0

            if (inds.length == 0) { return; }

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

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

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

    show(p)

### CustomJS for Hover

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.plotting import figure, output_file, show
    from bokeh.models import ColumnDataSource, HoverTool, CustomJS

    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 = """
    var links = %s;
    var data = {'x0': [], 'y0': [], 'x1': [], 'y1': []};
    var cdata = circle.data;
    var indices = cb_data.index['1d'].indices;
    for (i=0; i < indices.length; i++) {
        ind0 = indices[i]
        for (j=0; j < links[ind0].length; j++) {
            ind1 = links[ind0][j];
            data['x0'].push(cdata.x[ind0]);
            data['y0'].push(cdata.y[ind0]);
            data['x1'].push(cdata.x[ind1]);
            data['y1'].push(cdata.y[ind1]);
        }
    }
    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)

### CustomJS for Range Update

With Bokeh, ranges have a callback attribute that accept a Callback instance and execute javascript code on range updates that are triggered by tool interactions such as a box zoom, wheel scroll or pan.

    import numpy as np

    from bokeh.layouts import row
    from bokeh.models import ColumnDataSource, CustomJS, Rect
    from bokeh.plotting import output_file, figure, 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="""
        var data = source.data;
        var start = cb_obj.start;
        var end = cb_obj.end;
        data['%s'] = [start + (end - start) / 2];
        data['%s'] = [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)

    p1.x_range.callback = CustomJS(
            args=dict(source=source), code=jscode % ('x', 'width'))
    p1.y_range.callback = CustomJS(
            args=dict(source=source), code=jscode % ('y', 'height'))

    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)

## CustomJS with CoffeeScript code

It is possible to write the code for `CustomJS` callbacks in CoffeeScript. To accomplish this, use the `from_coffeescript` class method, which accepts the same `args` and `code` parameters:

    callback = CustomJS.from_coffeescript(args=dict(p=plot), code="""
    # coffeescript code here
    """)

## CustomJS with a Python function

A CustomJS callback can also be implemented as a Python function, which is then translated to JavaScript using PyScript. This makes it easier for users to define client-side interactions without having to learn JavaScript. To use this functionality you need the Flexx library (install with `conda install -c bokeh flexx` or `pip install flexx`).

Warning

It is critical to note that no python code is ever executed when a CustomJS callback is used. This is true even when the callback is supplied as python code to be translated to JavaScript as described in this section. A `CustomJS` callback is only executed inside a browser JavaScript interpreter, and can only directly interact JavaScript data and functions (e.g., BokehJS Backbone models).

For more information about the subset of Python that is supported in callbacks, see the PyScript documentation.

We recommend using `window.x` for variables specific to JavaScript to avoid confusion and help static code analysis tools. You can add `window` as an argument to the callback function to help readability (and pyflakes), as in the example below.

    from bokeh.layouts import column
    from bokeh.models import CustomJS, ColumnDataSource, 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)

    def callback(source=source, window=None):
        data = source.data
        f = cb_obj.value
        x, y = data['x'], data['y']
        for i in range(len(x)):
            y[i] = window.Math.pow(x[i], f)
        source.change.emit()

    slider = Slider(start=0.1, end=4, value=1, step=.1, title="power",
                    callback=CustomJS.from_py_func(callback))

    layout = column(slider, plot)

    show(layout)

## 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 users 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(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.colors.commutercreative.com/@color/"
    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.