I am new to Bokeh and I am trying to build a plot which can be dynamically updated based on input provided by a widget. However, usage of Python callbacks is not thoroughly documented for most widgets and therefore I'm stuck.
on_event
or on_change
), I still have to figure out its signature and arguments. For instance, if I'm using on_change
, which widget attributes can I monitor?Here is an appropriate example. I am using a notebook-embedded server like in this example. As an exercise, I would like to replace the slider with a DataTable
with arbitrary values. Here is the code I currently have:
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, DataTable
from bokeh.plotting import figure
from bokeh.io import show, output_notebook
from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature
output_notebook()
def modify_doc(doc):
df = sea_surface_temperature.copy()
source = ColumnDataSource(data=df)
source_table = ColumnDataSource(data={"alpha": [s for s in "abcdefgh"],
"num": list(range(8))})
plot = figure(x_axis_type='datetime', y_range=(0, 25),
y_axis_label='Temperature (Celsius)',
title="Sea Surface Temperature at 43.18, -70.43")
plot.line('time', 'temperature', source=source)
def callback(attr, old, new):
# This is the old callback from the example. What is "new" when I use
# a table widget?
if new == 0:
data = df
else:
data = df.rolling('{0}D'.format(new)).mean()
source.data = ColumnDataSource(data=data).data
table = DataTable(source=source_table,
columns=[TableColumn(field="alpha", title="Alpha"),
TableColumn(field="num", title="Num")])
# How can I attach a callback to table so that the plot gets updated
# with the "num" value when I select a row?
# table.on_change("some_attribute", callback)
doc.add_root(column(table, plot))
show(modify_doc)
This answer was given for Bokeh v1.0.4 and may not be compliant with the latest documentation
JavaScript callbacks and Python callbacks, are very powerful tools in Bokeh and can be attached to any Bokeh model element. Additionally you can extend Bokeh functionality by writing your own extensions with TypeScript (eventually compiled to JS)
JS callbacks can be added using either of both methods:
Model.js_on_event('event', callback)
Model.js_on_change('attr', callback)
Python callbacks are mainly used for widgets:
Widget.on_event('event', onevent_handler)
Widget.on_change('attr', onchange_handler)
Widget.on_click(onclick_handler)
The exact function signature for event handlers very per widget and can be:
onevent_handler(event)
onchange_handler(attr, old, new)
onclick_handler(new)
onclick_handler()
The attr
can be any widget class (or it's base class) attribute. Therefore you need always to consult the Bokeh reference pages. Also expanding the JSON Prototype helps to find out which attributes are supported e.g. looking at Div we cannot see directly the id
, name
, style
or text
attributes which come from its base classes. However, all of these attributes are present in the Div's JSON Prototype and hence are supported by Div:
{
"css_classes": [],
"disabled": false,
"height": null,
"id": "32025",
"js_event_callbacks": {},
"js_property_callbacks": {},
"name": null,
"render_as_text": false,
"sizing_mode": "fixed",
"style": {},
"subscribed_events": [],
"tags": [],
"text": "",
"width": null
}
Coming back to your question: Many times you can achieve the same result using different approaches.
To my knowledge, there is no nice method that lists all supported events per widget but reading documentation and digging into the base classes helps a lot.
Using methods described above it is possible to check which widget attributes you can use in your callbacks. When it comes to events I advice you to look at and explore the bokeh.events
class in your IDE. You can find there extended description for every event. In time it will come naturally when using your programmer's intuition to select the right event that your widget supports (so no button_click
for Plot
and no pan
event for Button
but the other way around).
Decision to which widget (model element) attach the callback and which method to choose or to which event bound the callback is yours and depends mainly on: which user action should trigger your callback?
So you can have a JS callback attached to any widget (value change, slider move, etc...), any tool (TapTool, HoverTool, etc...), data_source (clicking on glyph), plot canvas (e.g. for clicks on area outside a glyph) or plot range (zoom or pan events), etc...
Basically you need to know that all Python objects have their equivalents in BokehJS so you can use them the same way in both domains (with some syntax differences, of course).
This documentation shows for example that ColumnDataSource has a "selected" property so for points you can inspect source.selected.indices
and see which point on the plot are selected or like in your case: which table rows are selected. You can set a breakpoint in code in Python and also in the browser and inspect the Python or BokehJS data structures. It helps to set the environment variable BOKEH_MINIFIED
to no
either in you IDE (Run Configuration) or in Terminal (e.g. BOKEH_MINIFIED=no python3 main.py
) when running your code. This will make debugging the BokehJS in the browser much easier.
And here is your code (slightly modified for "pure Bokeh" v1.0.4 as I don't have Jupiter Notebook installed)
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, DataTable, TableColumn
from bokeh.plotting import figure, curdoc
from bokeh.io import show, output_notebook
from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature
# output_notebook()
def modify_doc(doc):
df = sea_surface_temperature.copy()
source = ColumnDataSource(data = df)
source_table = ColumnDataSource(data = {"alpha": [s for s in "abcdefgh"],
"num": list(range(8))})
plot = figure(x_axis_type = 'datetime', y_range = (0, 25),
y_axis_label = 'Temperature (Celsius)',
title = "Sea Surface Temperature at 43.18, -70.43")
plot.line('time', 'temperature', source = source)
def callback(attr, old, new): # here new is an array containing selected rows
if new == 0:
data = df
else:
data = df.rolling('{0}D'.format(new[0])).mean() # asuming one row is selected
source.data = ColumnDataSource(data = data).data
table = DataTable(source = source_table,
columns = [TableColumn(field = "alpha", title = "Alpha"),
TableColumn(field = "num", title = "Num")])
source_table.selected.on_change('indices', callback)
doc().add_root(column(table, plot))
modify_doc(curdoc)
# show(modify_doc)
Result: