08. Deploying Bokeh Apps

In the previous sections we discovered how to use a HoloMap to build a Jupyter notebook with interactive visualizations that can be exported to a standalone HTML file, as well as how to use DynamicMap and Streams to set up dynamic interactivity backed by the Jupyter Python kernel. However, frequently we want to package our visualization or dashboard for wider distribution, backed by Python but run outside of the notebook environment. Bokeh Server provides a flexible and scalable architecture to deploy complex interactive visualizations and dashboards, integrating seamlessly with Bokeh and with HoloViews.

For a detailed background on Bokeh Server see the bokeh user guide. In this tutorial we will discover how to deploy the visualizations we have created so far as a standalone bokeh server app, and how to flexibly combine HoloViews and Bokeh APIs to build highly customized apps. We will also reuse a lot of what we have learned so far---loading large, tabular datasets, applying datashader operations to them, and adding linked streams to our app.

A simple bokeh app

The preceding sections of this tutorial focused solely on the Jupyter notebook, but now let's look at a bare Python script that can be deployed using Bokeh Server:


In [ ]:
with open('./apps/server_app.py', 'r') as f:
    print(f.read())

Of the three parts of this app, part 2 should be very familiar by now -- load some taxi dropoff locations, declare a Points object, datashade them, and set some plot options.

Step 1 is new: Instead of loading the bokeh extension using hv.extension('bokeh'), we get a direct handle on a bokeh renderer using the hv.renderer function. This has to be done at the top of the script, to be sure that options declared are passed to the Bokeh renderer.

Step 3 is also new: instead of typing app to see the visualization as we would in the notebook, here we create a Bokeh document from it by passing the HoloViews object to the renderer.server_doc method.

Steps 1 and 3 are essentially boilerplate, so you can now use this simple skeleton to turn any HoloViews object into a fully functional, deployable Bokeh app!

Deploying the app

Assuming that you have a terminal window open with the hvtutorial environment activated, in the notebooks/ directory, you can launch this app using Bokeh Server:

bokeh serve --show apps/server_app.py

If you don't already have a favorite way to get a terminal, one way is to open it from within Jupyter, then make sure you are in the notebooks directory, and activate the environment using source activate hvtutorial (or activate tutorial on Windows). You can also open the app script file in the inbuilt text editor, or you can use your own preferred editor.


In [ ]:
# Exercise: Modify the app to display the pickup locations and add a tilesource, then run the app with bokeh serve
# Tip: Refer to the previous notebook

Iteratively building a bokeh app in the notebook

The above app script can be built entirely without using Jupyter, though we displayed it here using Jupyter for convenience in the tutorial. Jupyter notebooks are also often helpful when initially developing such apps, allowing you to quickly iterate over visualizations in the notebook, deploying it as a standalone app only once we are happy with it.

To illustrate this process, let's quickly go through such a workflow. As before we will set up our imports, load the extension, and load the taxi dataset:


In [ ]:
import holoviews as hv
import geoviews as gv
import dask.dataframe as dd

from holoviews.operation.datashader import datashade, aggregate, shade
from bokeh.models import WMTSTileSource

hv.extension('bokeh', logo=False)

usecols = ['tpep_pickup_datetime', 'dropoff_x', 'dropoff_y']
ddf = dd.read_csv('../data/nyc_taxi.csv', parse_dates=['tpep_pickup_datetime'], usecols=usecols)
ddf['hour'] = ddf.tpep_pickup_datetime.dt.hour
ddf = ddf.persist()

Next we define a Counter stream which we will use to select taxi trips by hour.


In [ ]:
stream = hv.streams.Counter()
points = hv.Points(ddf, kdims=['dropoff_x', 'dropoff_y'])
dmap = hv.DynamicMap(lambda counter: points.select(hour=counter%24).relabel('Hour: %s' % (counter % 24)),
                     streams=[stream])
shaded = datashade(dmap)

hv.opts('RGB [width=800, height=600, xaxis=None, yaxis=None]')

url = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{Z}/{Y}/{X}.jpg'
wmts = gv.WMTS(WMTSTileSource(url=url))

overlay = wmts * shaded

Up to this point, we have a normal HoloViews notebook that we could display using Jupyter's rich display of overlay, as we would with an any notebook. But having come up with the objects we want interactively in this way, we can now display the result as a Bokeh app, without leaving the notebook. To do that, first edit the following cell to change "8888" to whatever port your jupyter session is using, in case your URL bar doesn't say "localhost:8888/".

Then run this cell to launch the Bokeh app within this notebook:


In [ ]:
renderer = hv.renderer('bokeh')
server = renderer.app(overlay, show=True, websocket_origin='localhost:8888')

We could stop here, having launched an app, but so far the app will work just the same as in the normal Jupyter notebook, responding to user inputs as they occur. Having defined a Counter stream above, let's go one step further and add a series of periodic events that will let the visualization play on its own even without any user input:


In [ ]:
dmap.periodic(1)

You can stop this ongoing process by clearing the cell displaying the app.

Now let's open the text editor again and make this edit to a separate app, which we can then launch using Bokeh Server separately from this notebook.


In [ ]:
# Exercise: Copy the example above into periodic_app.py and modify it so it can be run with bokeh serve
# Hint: Use hv.renderer and renderer.server_doc
# Note that you have to run periodic **after** creating the bokeh document

Combining HoloViews with bokeh models

Now for a last hurrah let's put everything we have learned to good use and create a bokeh app with it. This time we will go straight to a Python script containing the app. If you run the app with bokeh serve --show ./apps/player_app.py from your terminal you should see something like this:

This more complex app consists of several components:

  1. A datashaded plot of points for the indicated hour of the daty (in the slider widget)
  2. A linked PointerX stream, to compute a cross-section
  3. A set of custom bokeh widgets linked to the hour-of-day stream

We have already covered 1. and 2. so we will focus on 3., which shows how easily we can combine a HoloViews plot with custom Bokeh models. We will not look at the precise widgets in too much detail, instead let's have a quick look at the callback defined for slider widget updates:

def slider_update(attrname, old, new):
    stream.event(hour=new)

Whenever the slider value changes this will trigger a stream event updating our plots. The second part is how we combine HoloViews objects and Bokeh models into a single layout we can display. Once again we can use the renderer to convert the HoloViews object into something we can display with Bokeh:

renderer = hv.renderer('bokeh')
plot = renderer.get_plot(hvobj, doc=curdoc())

The plot instance here has a state attribute that represents the actual Bokeh model, which means we can combine it into a Bokeh layout just like any other Bokeh model:

layout = layout([[plot.state], [slider, button]], sizing_mode='fixed')
curdoc().add_root(layout)

In [ ]:
# Advanced Exercise: Add a histogram to the bokeh layout next to the datashaded plot
# Hint: Declare the histogram like this: hv.operation.histogram(aggregated, bin_range=(0, 20))
#       then use renderer.get_plot and hist_plot.state and add it to the layout

Onwards

Although the code above is more complex than in previous sections, it's actually providing a huge range of custom types of interactivity, which if implemented in Bokeh alone would have required far more than a notebook cell of code. Hopefully it is clear that arbitrarily complex collections of visualizations and interactive controls can be built from the components provided by HoloViews, allowing you to make simple analyses very easily and making it practical to make even quite complex apps when needed. The user guide, gallery, and reference gallery should have all the information you need to get started with all this power on your own datasets and tasks. Good luck!