Bokeh in Python: send a working chart without a Python server

A standalone Bokeh document can run browser callbacks. A Python callback still needs a running Bokeh server.

A Bokeh chart opens on your computer, but its slider stops responding in the file you send. Check the callback before changing the host. A standalone Bokeh document can run JavaScript callbacks; a Python callback still needs a Bokeh server. Saving the page does not move that server into the file.

For a handoff that only changes the displayed data, keep the interaction in the browser. The worked example has three fictional inspection readings: 8, 14, and 20 minutes. Its threshold control starts at 10, so two readings remain visible. Set it to 15 and only the 20-minute reading remains.

Inspection readings: a native Bokeh threshold filter
Inspection readings: a native Bokeh threshold filter
Open the full example ↗

Open the working example · Download the complete HTML · Download the Python source

Keep the filtering work in CustomJS

The important distinction is where the callback executes. The following Python source creates a browser-side callback, then saves the layout:

from bokeh.embed import file_html
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, CustomJS, Slider
from bokeh.plotting import figure
from bokeh.resources import INLINE

source = ColumnDataSource(data=dict(x=[2, 3], y=[14, 20]))
plot = figure(height=260, sizing_mode="stretch_width",
              x_axis_label="Inspection", y_axis_label="Minutes")
plot.scatter("x", "y", source=source, size=16)
threshold = Slider(start=0, end=25, value=10, step=1,
                   title="Minimum minutes")
threshold.js_on_change("value", CustomJS(args=dict(source=source), code="""
const values = [8, 14, 20];
const x = [], y = [];
values.forEach((v, i) => {
  if (v >= cb_obj.value) { x.push(i + 1); y.push(v); }
});
source.data = {x, y};
source.change.emit();
"""))
page = file_html(column(threshold, plot), INLINE, "Inspection times")
with open("inspection.html", "w", encoding="utf-8") as f:
    f.write(page)

Here the initial source already matches the initial threshold. That matters: a page should not show all three points until the reader touches the slider.

Decide whether the server is part of the result

A callback that queries a database or executes Python cannot be recovered by replacing its event name with js_on_change. Either keep the server and share the deployed application, or prepare a fixed dataset whose allowed interactions can run in JavaScript.

The Bokeh embedding documentation separates standalone documents from server applications. Treat that boundary as a design choice before exporting. The same issue appears in Dash-to-HTML handoffs, though the APIs and callback code differ.

Bundle the resources the page needs

INLINE places Bokeh's resources in the document. That addresses the library dependency; your own callback must also avoid external data requests if the delivered file is meant to work offline. A call to fetch("readings.csv") still depends on another file or service.

Keep the source Python and the resulting HTML together in the handoff archive. The Python is the reproducible authoring file. The HTML is the reading copy, including the data that a reader can inspect through the browser.

Check the delivered interaction

Use threshold values 0, 10, 15, and 25. The expected visible counts are 3, 2, 1, and 0. At 25 the plot should be empty; it should not quietly retain the previous point. Restore 10 before delivery so the first view agrees with the written explanation.

Use a labeled control and leave enough room for the plot on a narrow screen. If the report needs an accessible record of its values, include the three readings as text beside the chart instead of making the plotted points the only source.

For an HTML reading copy in NOS, keep the same distinction: the page contains the prepared dataset and its browser interaction. A workspace page does not replace the Python server required by a different Bokeh application.

Keep reading