Select callback using bokehjs in a static React site

I’m attempting to learn bokehjs so I can embed interactive plots with widgets in a
static site. Creating a Bokeh app in Python is straight forward, however, when I attempt
to do the same thing in JavaScript I’m not able to update the plot when a user interacts
with the Select widget.

Below is what I would do in Python, and below that is what I have done in JavaScript. The Python app works just fine in Jupyter, and any time I select a new value from the Select widget, the figure is updated. For the JavaScript version, I’m constrained to use the Docusaurus framework, which requires the <BrowserOnly> React component, so all the rendering is done on the client. The figure and widget render just fine in the Docusaurus site, but when I interact with the Select object, I do not get any changes represented in the figure.

I’ve looked at the work done by @stas-sl on observablehq and kaggle, but I must be missing something important as I cannot get my example to work. The official documentation that shows user interactions updating a figure using bokehjs does not use a Bokeh widget, and instead opts for an HTML button, which has the addEventListener method. I suppose I could go this route, but I would like to use the widgets that Bokeh has. I’ve combed through the source code to try and find other examples using widgets with JavaScript, but I’m afraid I’m just at a loss, and need some help debugging why I am unable to register an update to the figure.

from bokeh.io.showing import show
from bokeh.layouts import column
from bokeh.models.sources import ColumnDataSource
from bokeh.models.widgets.inputs import Select
from bokeh.plotting.figure import figure


class BKApp:
    def modify_doc(self, doc):
        # Initialize the widget value.
        init_value = 1

        # Compute data using the initial widget value, and create a Bokeh source.
        x = range(10)
        y = [init_value * pt for pt in x]
        source = ColumnDataSource({"x": x, "y": y})

        # Create the figure, and attach the data to it.
        fig = figure()
        glyph = fig.line(x="x", y="y", source=source)

        # Create the widget.
        select = Select(
            value=str(init_value), options=list(map(str, range(3))), title="slope"
        )

        # Callback for the widget.
        def update(attr, old, new):
            # Calculate new data.
            new_y = [int(new) * pt for pt in x]
            # Update the source with the new data.
            source.data = {"x": x, "y": new_y}

        # Listen for changes from the widget.
        select.on_change("value", update)

        # Add the widget and plot to the doc.
        layout = column([select, fig])
        doc.add_root(layout)

    def show(self):
        # Visualize the widget in Jupyter.
        show(self.modify_doc)


BKApp().show()
import React, { useRef } from "react";
import BrowserOnly from "@docusaurus/BrowserOnly";

export const BkApp = () => {
  const containerRef = useRef(null);

  return (
    <BrowserOnly fallback={<div>loading...</div>}>
      {() => {
        const Bokeh = require("@bokeh/bokehjs");

        // Initialize the widget value.
        const init_value = 1;

        // Compute data using the initial widget value, and create a Bokeh source.
        const x = [...Array(10).keys()];
        let y = [];
        for (let i = 0; i < x.length; i++) {
          y.push(init_value * x[i]);
        }
        const source = new Bokeh.ColumnDataSource({ data: { x: x, y: y } });

        // Create the figure, and attach the data to it.
        const fig = Bokeh.Plotting.figure();
        fig.line({
          x: { field: "x" },
          y: { field: "y" },
          source: source,
        });

        // Create the widget.
        const select = new Bokeh.Widgets.Select({
          value: init_value.toString(),
          options: [...Array(3).keys()].map((value) => {
            return value.toString();
          }),
          title: "slope",
        });

        // Callback for the widget.
        const update = (select, source, fig) => {
          let value = select.value;
          let x = source.data.x;
          // Calculate new data.
          const newY = [];
          for (let i = 0; i < y.length; i++) {
            newY.push(parseInt(value) * x[i]);
          }
          // Update the source with the new data.
          source.data.y = newY;
          // Emit changes to the figure.
          fig.change.emit();
        };

        // Listen for changes from the widget.
        select.js_event_callbacks["menu_item_click"] = [
          new Bokeh.CustomJS({
            args: { select, source, fig },
            code: update(select, source, fig),
          }),
        ];

        // Visualize.
        const layout = new Bokeh.Column({ children: [select, fig] });
        if (containerRef && containerRef.current) {
          containerRef.current.innerHTML = "";
          Bokeh.Plotting.show(layout, containerRef.current);
        }
        return <div ref={containerRef} style={{ width: "100%" }}></div>;
      }}
    </BrowserOnly>
  );
};

select-widget

Way outta my comfort zone on the BokehJS side but having a decent knowledge of embedding JS callbacks and looking at your callback… I’d try source.change.emit() instead of fig.change.emit().

1 Like

Thanks @gmerritt123. I tried that and still no results. Since I’m working in a framework (Docusaurus), I realized that the MRE may not be easy to reproduce, so I created a repo that contains the example. The readme should be enough to get the example running, as long as you are using conda. If there are issues associated with not being able to reproduce the gif above from the repo, create an issue and I will fix it.

CustomJS was originally created as a mechanism to shuttle JS callbacks from Python into Bokeh content in web pages. As such, the value of code is expected to be a string, not an actual JS function. [1] I thought there was a way to supply actual JS functions instead of a CustomJS in case users were using BokehJS directly, but I am not seeing it offhand. I’d suggest starting a GitHub development discussion so that we can more easily ping some other core team members who work on BokehJS.


  1. There is no way to send an actual JS function from Python to a web page, except by sending the code for the function as text. ↩︎

Link to the GitHub discussion; Select callback using BokehJS in a static React site · Discussion #12099 · bokeh/bokeh · GitHub.

1 Like

@ndmlny-qs, I wish there was a similar easy way to add callbacks from JS as it is possible from python. It should be even easier as we are already in JS world.

Reading through bokeh source code, I found that there are several mechanisms for handling events there. One is using js_event_callbacks and creating Bokeh.CustomJS as I was using on Observable, another - is handling property change events. The latter is applicable in your case, when select value changes, you can attach a handler like this:

 select.change.connect(
    (_, sender) => (r.glyph.line_color = r.glyph.fill_color = sender.value)
  );

As you see, it is even easier, as you don’t need to wrap it into CustomJS. I don’t like it, as we are touching bokeh internals, but it works. If someone finds a better way, would be happy to know.

You can try it here Bokehjs - handling value change event / Stas / Observable

Also, I don’t think Select widget has menu_item_click event, I believe it is from Dropdown widget, so if you change widget type, it might work.

There could be probably multiple mistakes in your code, as @gmerritt123 pointed out, usually you should call source.change.emit() instead of fig. So try to localize where it stops working, add console.log(...) in the handler to ensure it is called. I believe you don’t need docusaurus for this. There could be additional issues, when you’re integrating with it, but first, make sure it works without it. And it will be easier for others to reproduce.

Update:
And, yes, as @Bryan pointed out, and as you can see in my raindrops animation on Observable, you should pass code as string, to CustomJS constructor, which is rather inconvenient. Or you can use a trick also found by @Bryan. The idea is that internally CustomJS class has execute method, which evaluates the code passed to it as string in constructor, but instead you can just provide an instance of an object that has single execute property with your native javascript callback.

1 Like

@stas-sl I reviewed your observablehq code, and was able to integrate the select.change.connect... method above. It does indeed work, and the figure updates when the Select widget is changed. For completeness, the working code follows after my responses to you.

reponses

Thank you for stating:

Also, I don’t think Select widget has menu_item_click event…

I was very confused about which events to use for which widgets, and could not figure out how to determine which one to use. I did try the Dropdown widget and the Select widget with all combinations of these events; [menu_item_click, menu_click, click]. None of which worked as I was not implementing the correct methods, nor passing the correct types when using CustomJS. I did try both sending a string and JS code to CustomJS, and neither types seemed to work.

There could be probably multiple mistakes in your code

This is absolutely true, as I am still learning JavaScript and trying my best to create a tool that aligns with the tool I implemented in Jupyter. The working code below has no emit() called on the change of the Select widget.

…you don’t need docusaurus for this.

Alas, I do need Docusaurus as it is a requirement of the task I am working on. The code below does function in the Docusaurus framework, so huzzah to you for teaching me the handler, and huzzah to it functioning in Docusaurus.

The idea is that internally CustomJS class has execute method

I did not fully grok this when @Bryan stated it intially. I’m not entirely sure I fully understand it, but the explanation you gave has made the trick linked by Bryan a little clearer. I will need to study it further for future work as there are several tools I need to mirror in JavaScript that have already been written in Python.

working code

import React, { useRef } from "react";
import BrowserOnly from "@docusaurus/BrowserOnly";

export const BKApp = () => {
  const containerRef = useRef(null);
  return (
    <BrowserOnly fallback={<div>loading...</div>}>
      {() => {
        const Bokeh = require("@bokeh/bokehjs");

        // Initialize the widget value.
        const init_value = 1;

        // Compute data using the initial widget value, and create a Bokeh source.
        const x = [...Array(10).keys()];
        let y = [];
        for (let i = 0; i < x.length; i++) {
          y.push(init_value * x[i]);
        }
        const source = new Bokeh.ColumnDataSource({ data: { x: x, y: y } });

        // Create the figure, and attach the data to it.
        const fig = Bokeh.Plotting.figure();
        const glyph = fig.line({
          x: { field: "x" },
          y: { field: "y" },
          source: source,
        });

        // Create the widget.
        const select = new Bokeh.Widgets.Select({
          value: init_value.toString(),
          options: [...Array(3).keys()].map((value) => {
            return value.toString();
          }),
          title: "slope",
        });
        // Callback for the widget.
        select.change.connect((_, sender) => {
          let newSlope = sender.value;
          let newY = [];
          for (let i = 0; i < x.length; i++) {
            newY.push(newSlope * x[i]);
          }
          glyph.data_source.data = { x: x, y: newY };
        });

        // Visualize.
        const layout = new Bokeh.Column({ children: [select, fig] });
        if (containerRef && containerRef.current) {
          containerRef.current.innerHTML = "";
          Bokeh.Plotting.show(layout, containerRef.current);
        }
        return <div ref={containerRef} style={{ width: "100%" }}></div>;
      }}
    </BrowserOnly>
  );
};
1 Like

I wish there was a similar easy way to add callbacks from JS as it is possible from python.

I think this would be a very good area for new development. My own time for Bokeh is currently very tight and the overall priority is getting to a 3.0 release in the next month or two. But I’d like to invite both of you to keep pushing on this issue after that so we can find a way to prioritize it.

1 Like

I think this is because you are replacing the whole data property, which probably emits change implicitly:

glyph.data_source.data = { x: x, y: newY };

If you’d update only specific columns like glyph.data_source.data.x = x or specific values glyph.data_source.data.x[i] = ..., you’d probably needed to call source.change.emit() explicitly.

Anyway, good to know, it is now working for you.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.