BokehGarden is a Bokeh based web application framework focused on plotting heavy interactive applications.
Additionally, it provides several improved widgets for Bokeh, that can be used both within the framework and in a standalone bokeh app:
- Interactive colorbar
- That allows adjusting the color map by pan and zoom
- File download button
- That goes directly over http (not websocket)
- File upload field
- That goes directly over http (not websocket), meaning that it does not have a size limit, nor does it encode the file as base64.
- Autocomplete input field
- That fixes the text entry vs drop down selection problem
- Drop down menu
- That encodes the entry id:s as json, allowing e.g. integers to be used as id:s, not just strings.
bokeh serve examples/weather_data_explorer.py
The BokehGarden application framework consists of a very old idea,
applied to Bokeh, combined with a small tool. The simple idea is to
create complex UI element from simpler ones by subclassing Bokeh
widget classes. In the subclass, the __init__()
method is used to
populate the widget with parts and bind callbacks.
class MyForm(bokeh_garden.application.AppWidget, bokeh.layouts.Column):
def __init__(self, app=None, **kw):
self._app = app
self._foo = bokeh.models.Slider(title="Select a value for foo", value=0, start=0, end=100, step=1)
self._bar = bokeh.models.NumericInput(title="Bar value", mode="float", value=10)
self._submit_button = bokeh.models.Button(label="Submit", button_type="primary")
self._submit_button.on_event(bokeh.events.ButtonClick, self.on_submit)
bokeh.layouts.Column.__init__(
self, self._foo, self._bar, self._submit_button, **kw)
def on_submit(self, event):
print("Submitted values:", self._foo.value, self._bar.value)
The above widget could be used as any other Bokeh widget in your plots.
Additionally, it could be used inside another widget of the same type, to recursively build up complex interfaces. But there is a better way: Layouts and overlays.
A layout is a simple dictionary based data structure that defines the layout of an application declaratively. It is equivalent to instantiating widgets, sending them as parameters when instantiating other widgets to gradually build up the application layout, just like you normally do in bokeh. However, since it's data, not code, it's easy to store and modify the structure. Example layout data as python code:
layout = {"widget": "bokeh_garden.tabs.Tabs",
"Data": {"widget": "weather.DataLoaderForm"},
"Sea": {"widget": "bokeh.models.layouts.Column",
"children": [{"widget": "weather.WavesPlot", "unit": "kn"},
{"widget": "weather.CurentsPlot", "unit": "kn"}]},
"Air": {"widget": "bokeh.models.layouts.Row",
"children": [{"widget": "weather.PressurePlot", "tags": ["wind_map"]},
{"widget": "weather.WindPlot", "tags": ["wind_map"]}]}}
At each level, the layout data structure consists of a dictionary
{"widget": "modulename.submodulename.classname", "initparamname1": "initparamvalue1", "initparamname2": "initparamvalue2", ... }
with the key widget
holding the name (and module path) of a widget class, and other keys being used to generate the
parameters to __init__()
for that class. For scalar values, these are just passed as-is, but for dictionaries and lists, they are first instantiated the same way recursively.
__init__()
will also receive an instance of
bokeh_garden.application.Application
as a first argument. This instance is shared among all
widgets created using the layout. The layout above would translate into
layout_instance = bokeh_garden.tabs.Tabs(
app,
Data=weather.DataLoaderForm(app),
Sea=bokeh.models.layouts.Column(
*[weather.WavesPlot(app, unit="kn"),
weather.CurentsPlot(app, unit="kn")]),
Air=bokeh.models.layouts.Row(
*[weather.PressurePlot(app, tags=["wind_map"]),
weather.WindPlot(app, tags=["wind_map"])]))
For larger applications is recomended to store this data structure as a separate yaml file. This makes getting a quick overview over the layout easier, as well as eases merging layout changes in git. Here's the same data as above but in yaml syntax:
layout:
widget: bokeh_garden.tabs.Tabs
Data:
widget: weather.DataLoaderForm
Sea:
widget: bokeh.models.layouts.Column
children:
- widget: weather.WavesPlot
unit: kn
- widget: weather.CurentsPlot
unit: kn
Air:
widget: bokeh.models.layouts.Row
children:
- widget: weather.PressurePlot
tags: ["wind_map"]
- widget: weather.WindPlot
tags: ["wind_map"]
The layout structure above lets you build complex applications from highly independent widgets. But it has one drawback: What if you need to make widgets in different parts of the application interact? Overlays solve this problem.
Overlays are ordinary python classes that has access to the document and the widget tree, can query the widget tree and modify the widgets in it by e.g. binding callbacks or adding drawing operations (layers) to plots.
class WindMapLinkedMapExtentsOverlay(object):
def __init__(self, app):
self.app = app
x_range = None
y_range = None
for figure in self.app.doc.select({"tags": ["wind_map"]}):
if x_range is None:
x_range = figure.x_range
y_range = figure.y_range
else:
figure.x_range = x_range
figure.y_range = y_range
overlays = [{"widget": WindMapLinkedMapExtentsOverlay},
...]
Once you have built a layout and defined your overlays, you can instantiate them as a Bokeh application using the following code (notebook version):
bokeh.io.output_notebook()
application = bokeh_garden.application.Application(layout, overlays)
bokeh.io.show(application)
or (application file for bokeh serve):
application = bokeh_garden.application.Application(layout, overlays)
application(bokeh.plotting.curdoc())