Skip to content

Instantly share code, notes, and snippets.

@mrocklin
Created June 29, 2017 00:42
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save mrocklin/e014f11aab7eb3fd12d83a746d8c87df to your computer and use it in GitHub Desktop.
Save mrocklin/e014f11aab7eb3fd12d83a746d8c87df to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src=\"http://bokeh.pydata.org/en/latest/_static/images/logo.png\" align=\"right\">\n",
"\n",
"The Bokeh Server, a minimal programmatic example\n",
"================\n",
"\n",
"This notebook show how to start a *very simple* bokeh server application *programmatically*. For more complex examples, or for the more standard command line interface, see the [Bokeh documentation](http://bokeh.pydata.org/en/latest/docs/user_guide/server.html).\n",
"\n",
"Motivation\n",
"---------\n",
"\n",
"Many people know Bokeh as a tool for building web visualizations from languages like Python. However I find that Bokeh's true value is in serving live-streaming, interactive visualizations that update with real-time data. I personally use Bokeh to serve real-time diagnostics for a distributed computing system. In this case I embed Bokeh directly into my library. I've found it incredibly useful and easy to deploy sophisticated and beaufitul visualizations that help me understand the deep inner-workings of my system.\n",
"\n",
"Most of the (excellent) documentation focuses on stand-alone applications using the Bokeh server\n",
"\n",
" $ bokeh serve myapp.py\n",
" \n",
"However I mostly deal with programmers who would prefer to execute things programmatically. I thought I'd provide some examples on how to do this within a Jupyter notebook."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Launch Bokeh Servers from a Notebook\n",
"------------------------------------\n",
"\n",
"The code below starts a Bokeh server running on port 5000 that provides a single route to `/` that serves a single figure with a line-plot. The imports are a bit wonky, but the amount of code necessary here is relatively small."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"from bokeh.server.server import Server\n",
"from bokeh.application import Application\n",
"from bokeh.application.handlers.function import FunctionHandler\n",
"from bokeh.plotting import figure, ColumnDataSource\n",
"\n",
"def make_document(doc):\n",
" fig = figure(title='Line plot!', sizing_mode='scale_width')\n",
" fig.line(x=[1, 2, 3], y=[1, 4, 9])\n",
"\n",
" doc.title = \"Hello, world!\"\n",
" doc.add_root(fig)\n",
" \n",
"apps = {'/': Application(FunctionHandler(make_document))}\n",
"\n",
"server = Server(apps, port=5000)\n",
"server.start()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We make a function `make_document` which is called every time someone visits our website. This function can create plots, call functions, and generally do whatever it wants. Here we make a simple line plot and register that plot with the document with the `doc.add_root(...)` method. \n",
"\n",
"This starts a Tornado web server and creates a new image whenever someone connects, similar to libraries like Tornado, or Flask\n",
"\n",
"image to bokeh-server-line-plot.png"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Live Updates\n",
"--------------\n",
"\n",
"I find that Bokeh's real strength comes when you want to stream live data into the browser. Doing this by hand generally means serializing your data on the server, figuring out how web sockets work, sending the data to the client/browser and then updating plots in the browser.\n",
"\n",
"Bokeh handles this by keeping a synchronized table of data on the client and the server. This is the `ColumnDataSource`. If you define plots around the column data source and then push more data into the source then Bokeh will handle the rest, synchronizing the plots on the screen with the data that you provide.\n",
"\n",
"In the example below every time someone connects to our server we make a new `ColumnDataSource`, make a function that will add a new record into it, and set up a calllback to call that function every 100ms. We then make a plot around that data source to render the data as colored circles.\n",
"\n",
"Because this is a new Bokeh server we start this on a new port, though in practice if we had multiple pages we would just add them as multiple routes in the `apps` variable."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"import random\n",
"\n",
"def make_document(doc):\n",
" source = ColumnDataSource({'x': [], 'y': [], 'color': []})\n",
"\n",
" def update():\n",
" new = {'x': [random.random()],\n",
" 'y': [random.random()],\n",
" 'color': [random.choice(['red', 'blue', 'green'])]}\n",
" source.stream(new)\n",
"\n",
" doc.add_periodic_callback(update, 100)\n",
"\n",
" fig = figure(title='Streaming Circle Plot!', sizing_mode='scale_width',\n",
" x_range=[0, 1], y_range=[0, 1])\n",
" fig.circle(source=source, x='x', y='y', color='color', size=10)\n",
"\n",
" doc.title = \"Now with live updating!\"\n",
" doc.add_root(fig)\n",
" \n",
"apps = {'/': Application(FunctionHandler(make_document))}\n",
"\n",
"server = Server(apps, port=5001)\n",
"server.start()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"By changing around the figures (or combining multiple figures, text, other visual elements, etc.) you have full freedom over the visual styling of your web service. By changing around the update function you can pull data from sensors, shove in more interesting data, etc.. This toy example is meant to provide the skeleton of a simple application; hopefully you can fill in details from your application.\n",
"\n",
"Here is a simple example taken from Dask's dashboard that maintains a streaming time series plot with the number of idle and saturated workers in a Dask cluster."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```python\n",
"def make_document(doc):\n",
" source = ColumnDataSource({'time': [time(), time() + 1],\n",
" 'idle': [0, 0.1],\n",
" 'saturated': [0, 0.1]})\n",
"\n",
" x_range = DataRange1d(follow='end', follow_interval=20000, range_padding=0)\n",
"\n",
" fig = figure(title=\"Idle and Saturated Workers Over Time\",\n",
" x_axis_type='datetime', y_range=[-0.1, len(scheduler.workers) + 0.1],\n",
" height=150, tools='', x_range=x_range, **kwargs)\n",
" fig.line(source=source, x='time', y='idle', color='red')\n",
" fig.line(source=source, x='time', y='saturated', color='green')\n",
" fig.yaxis.minor_tick_line_color = None\n",
"\n",
" fig.add_tools(\n",
" ResetTool(reset_size=False),\n",
" PanTool(dimensions=\"width\"),\n",
" WheelZoomTool(dimensions=\"width\")\n",
" )\n",
"\n",
" doc.add_root(fig)\n",
"\n",
" def update():\n",
" result = {'time': [time() * 1000],\n",
" 'idle': [len(scheduler.idle)],\n",
" 'saturated': [len(scheduler.saturated)]}\n",
" source.stream(result, 10000)\n",
" \n",
" doc.add_periodic_callback(update, 100)\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Final Thoughts\n",
"--------------\n",
"\n",
"You can also have buttons, sliders, widgets, etc.. I rarely use these \n",
"personally though so they don't interest me as much. I've found the Bokeh\n",
"server to be incredibly helpful in my work and also very approachable once you\n",
"understand how to set one up (as you now do). I hope that this post serves\n",
"people well. "
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.0"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment