Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get a list of Bokeh widget events and attributes (which can be used to trigger a Python callback)

Tags:

python

bokeh

The real (general) question

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.

  1. How can I know which widget method I should use to attach my callback? I can guess the available choices by probing the widgets attributes in an interactive console, but that's not elegant and I'm sure it's written somewhere in the documentation.
  2. Provided that I would know about the method to use (e.g. 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?
  3. Once I know which attribute I can monitor, how can I know the data structure which will be yielded by the event?

Some more context and the (not-as-useful) specific question

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)
like image 860
M4urice Avatar asked Mar 28 '19 17:03

M4urice


1 Answers

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:

enter image description here

like image 147
Tony Avatar answered Nov 14 '22 23:11

Tony