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