import numpy as np
from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Slider, TextInput
from bokeh.plotting import figure
# Set up data
N = 200
x = np.linspace(0, 4*np.pi, N)
y = np.sin(x)
source = ColumnDataSource(data=dict(x=x, y=y))
# Set up plot
plot = figure(height=400, width=400, title="my sine wave",
tools="crosshair,pan,reset,save,wheel_zoom",
x_range=[0, 4*np.pi], y_range=[-2.5, 2.5])
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
# Set up widgets
amplitude = Slider(title="amplitude", value=1.0, start=-5.0, end=5.0, step=0.1)
phase = Slider(title="phase", value=0.0, start=0.0, end=2*np.pi)
freq = Slider(title="frequency", value=1.0, start=0.1, end=5.1, step=0.1)
def update_data(attrname, old, new):
# Get the current slider values
a = amplitude.value
w = phase.value
k = freq.value
# Generate the new curve
x = np.linspace(0, 4*np.pi, N)
y = a*np.sin(k*x + w)
source.data = dict(x=x, y=y)
for w in [amplitude, phase, freq]:
w.on_change('value', update_data)
# Set up layouts and add to document
inputs = column(amplitude, phase, freq)
curdoc().add_root(row(inputs, plot, width=800))A Bokeh app is a Bokeh plot with widgets, such as
sliders, dropdowns and buttons, whose changes run Python callbacks. The
callbacks change the data, and the plot redraws. People build apps for
dashboards and tools where a chart has to respond to input. Normally an
app runs with bokeh serve app.py, which starts a server to run the
Python. This page is an online Bokeh app compiler: the code runs in your
browser, so you can try it without installing anything.
Run the example first, then paste any snippet below into a new cell to try it.
ColumnDataSource holds the data as named columns: 200 x values from 0
to 4π and their sines. plot.line("x", "y", source=source) draws the
line from those columns, so replacing source.data redraws it.
x_range and y_range fix the axes, so an amplitude above 2.5 or below
-2.5 runs past the edge of the plot. The three Slider widgets set the
amplitude, phase and frequency. phase has no step, so it uses
Bokeh's default of 1. update_data(attrname, old, new) has the signature
Bokeh requires for on_change callbacks. It reads all three slider
values, computes a*np.sin(k*x + w) and replaces source.data, and the
loop registers it on each slider's value. column() stacks the
sliders, row() puts them beside the plot, and curdoc().add_root()
adds the layout to the document, which makes it an app. It appears below
the cell when the cell runs. TextInput is imported but not used.
A Select widget's value is the chosen option. The callback swaps in
that product's numbers and renames the plot:
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Select
from bokeh.plotting import figure
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
sales = {"Tea": [120, 135, 150, 140, 160, 175], "Coffee": [200, 190, 210, 230, 220, 240]}
source = ColumnDataSource(data=dict(month=months, units=sales["Tea"]))
plot = figure(x_range=months, height=300, title="Tea sales")
plot.vbar(x="month", top="units", width=0.8, source=source)
product = Select(title="Product", value="Tea", options=list(sales))
def switch(attr, old, new):
source.data = dict(month=months, units=sales[new])
plot.title.text = f"{new} sales"
product.on_change("value", switch)
curdoc().add_root(column(product, plot))
Button.on_click() runs a function on every click. source.stream()
appends rows to a ColumnDataSource instead of replacing its data:
import numpy as np
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import Button, ColumnDataSource, Div
from bokeh.plotting import figure
rng = np.random.default_rng(seed=1)
source = ColumnDataSource(data=dict(x=[], y=[]))
plot = figure(height=300, width=400, x_range=(0, 1), y_range=(0, 1))
plot.scatter("x", "y", source=source, size=10)
button = Button(label="Add 10 points", button_type="primary")
count = Div(text="0 points")
def add_points():
source.stream(dict(x=rng.random(10), y=rng.random(10)))
count.text = f"{len(source.data['x'])} points"
button.on_click(add_points)
curdoc().add_root(column(button, count, plot))
The selection is stored on the data source as source.selected, so a
plot and a DataTable built on one source share it. A callback on its
indices runs whenever the selection changes:
from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, DataTable, Div, TableColumn
from bokeh.plotting import figure
source = ColumnDataSource(data=dict(
fruit=["Apple", "Pear", "Plum", "Grape", "Cherry", "Mango"],
price=[1.2, 1.5, 2.0, 3.1, 4.5, 2.8],
sold=[340, 210, 150, 90, 60, 120],
))
plot = figure(height=300, width=350, tools="tap,box_select,reset",
x_axis_label="Price", y_axis_label="Units sold")
plot.scatter("price", "sold", source=source, size=12)
table = DataTable(source=source, width=300, height=250, columns=[
TableColumn(field=name, title=name.title()) for name in ["fruit", "price", "sold"]])
summary = Div(text="Select points or rows")
def selected(attr, old, new):
sold = sum(source.data["sold"][i] for i in new)
summary.text = f"{len(new)} selected, {sold} units sold"
source.selected.on_change("indices", selected)
curdoc().add_root(column(row(plot, table), summary))
curdoc().add_root(). A plot displayed with show(), as on the
Bokeh page, is standalone HTML, and Bokeh warns that
Python callbacks cannot work there. For a standalone plot, use
js_on_change() with a CustomJS callback.on_change callback must take three arguments, (attr, old, new).
With any other signature, on_change() raises a ValueError. An
on_click callback takes no argument or one event.