Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bokeh server support #959

merged 30 commits into from Apr 7, 2017
Changes from 29 commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
Added BokehRenderer server mode
Oct 31, 2016
Added initial bokeh server stream callback handling
Oct 31, 2016
Small fixes for bokeh server implementation
Oct 31, 2016
Added initial BokehServerWidgets implementation
Oct 31, 2016
Fixes for BokehServerWidgets
Oct 31, 2016
Ensure all subplots have the same plotting classes
Nov 4, 2016
Defined bokeh widget parameters
Nov 4, 2016
Added bokeh app examples
Feb 3, 2017
Small fix for bokeh widget import
Mar 26, 2017
Improved handling of boomeranging events in bokeh backend
Mar 26, 2017
Improved bokeh server event queue
Mar 26, 2017
Improved range updates for bokeh server
Mar 26, 2017
Fixed small bugs in bokeh Callbacks
Mar 26, 2017
Fixed bokeh event callbacks after change to cb_obj
Apr 6, 2017
Implemented UIEvent handling for bokeh server
Apr 6, 2017
Moved bokeh server example apps
Apr 6, 2017
Completely refactored bokeh Callbacks
Apr 6, 2017
Made callback utilities into classmethods
Apr 6, 2017
Minor cleanup on bokeh Callbacks
Apr 6, 2017
Added tests for bokeh Callbacks
Apr 6, 2017
Small fix for bokeh ServerCallback on_change events
Apr 6, 2017
Allow supplying Document to BokehRenderer
Apr 6, 2017
Simplified bokeh Callback initialization
Apr 6, 2017
Moved bokeh server widget handling onto BokehRenderer
Apr 6, 2017
Factored out class method to create bokeh widgets
Apr 6, 2017
Small fixes and improvements for bokeh widgets
Apr 6, 2017
Added tests for BokehServerWidgets
Apr 6, 2017
Fixed unreferenced variable bugs
Apr 6, 2017
Various python3 fixes
Apr 7, 2017
Improved docstrings for bokeh server features
Apr 7, 2017
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.
+985 −249
Diff settings


Just for now

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -526,8 +526,11 @@ def _update_ranges(self, element, ranges):
xfactors, yfactors = None, None
if any(isinstance(ax_range, FactorRange) for ax_range in [x_range, y_range]):
xfactors, yfactors = self._get_factors(element)
self._update_range(x_range, l, r, xfactors, self.invert_xaxis, self._shared['x'])
self._update_range(y_range, b, t, yfactors, self.invert_yaxis, self._shared['y'])
framewise = self.framewise
if not self.drawn or (not self.model_changed(x_range) and framewise):
self._update_range(x_range, l, r, xfactors, self.invert_xaxis, self._shared['x'])
if not self.drawn or (not self.model_changed(y_range) and framewise):
self._update_range(y_range, b, t, yfactors, self.invert_yaxis, self._shared['y'])

def _update_range(self, axis_range, low, high, factors, invert, shared):
@@ -788,6 +791,21 @@ def update_frame(self, key, ranges=None, plot=None, element=None, empty=False):

def model_changed(self, model):
Determines if the bokeh model was just changed on the frontend.
Useful to suppress boomeranging events, e.g. when the frontend
just sent an update to the x_range this should not trigger an
update on the backend.
callbacks = [cb for cbs in self.traverse(lambda x: x.callbacks)
for cb in cbs]
stream_metadata = [stream._metadata for cb in callbacks
for stream in cb.streams if stream._metadata]
return any(md['id'] == model.ref['id'] for models in stream_metadata
for md in models.values())

def current_handles(self):
@@ -821,15 +839,8 @@ def current_handles(self):
if not self.apply_ranges:
rangex, rangey = False, False
elif isinstance(self.hmap, DynamicMap):
callbacks = [cb for cbs in self.traverse(lambda x: x.callbacks)
for cb in cbs]
stream_metadata = [stream._metadata for cb in callbacks
for stream in cb.streams if stream._metadata]
ranges = ['%s_range' % ax for ax in 'xy']
event_ids = [md[ax]['id'] for md in stream_metadata
for ax in ranges if ax in md]
rangex = plot.x_range.ref['id'] not in event_ids and framewise
rangey = plot.y_range.ref['id'] not in event_ids and framewise
rangex = not self.model_changed(plot.x_range) and framewise
rangey = not self.model_changed(plot.y_range) and framewise
elif self.framewise:
rangex, rangey = True, True
@@ -0,0 +1,78 @@
import numpy as np
import pandas as pd
import holoviews as hv
import holoviews.plotting.bokeh

from bokeh.layouts import row, widgetbox
from bokeh.models import Select
from bokeh.plotting import curdoc, figure
from bokeh.sampledata.autompg import autompg

df = autompg.copy()

SIZES = list(range(6, 22, 3))
ORIGINS = ['North America', 'Europe', 'Asia']

# data cleanup
df.cyl = [str(x) for x in df.cyl]
df.origin = [ORIGINS[x-1] for x in df.origin]

df['year'] = [str(x) for x in df.yr]
del df['yr']

df['mfr'] = [x.split()[0] for x in]
df.loc[df.mfr=='chevy', 'mfr'] = 'chevrolet'
df.loc[df.mfr=='chevroelt', 'mfr'] = 'chevrolet'
df.loc[df.mfr=='maxda', 'mfr'] = 'mazda'
df.loc[df.mfr=='mercedes-benz', 'mfr'] = 'mercedes'
df.loc[df.mfr=='toyouta', 'mfr'] = 'toyota'
df.loc[df.mfr=='vokswagen', 'mfr'] = 'volkswagen'
df.loc[df.mfr=='vw', 'mfr'] = 'volkswagen'
del df['name']

columns = sorted(df.columns)
discrete = [x for x in columns if df[x].dtype == object]
continuous = [x for x in columns if x not in discrete]
quantileable = [x for x in continuous if len(df[x].unique()) > 20]

hv.Store.current_backend = 'bokeh'
renderer = hv.Store.renderers['bokeh']
options = hv.Store.options(backend='bokeh')
options.Points = hv.Options('plot', width=800, height=600, size_index=None,)
options.Points = hv.Options('style', cmap='rainbow', line_color='black')

def create_figure():
label = "%s vs %s" % (x.value.title(), y.value.title())
kdims = [x.value, y.value]

opts, style = {}, {}
opts['color_index'] = color.value if color.value != 'None' else None
if size.value != 'None':
opts['size_index'] = size.value
opts['scaling_factor'] = (1./df[size.value].max())*200
points = hv.Points(df, kdims=kdims, label=label)(plot=opts, style=style)
plot = renderer.get_plot(points)
return plot.state

def update(attr, old, new):
layout.children[1] = create_figure()

x = Select(title='X-Axis', value='mpg', options=quantileable)
x.on_change('value', update)

y = Select(title='Y-Axis', value='hp', options=quantileable)
y.on_change('value', update)

size = Select(title='Size', value='None', options=['None'] + quantileable)
size.on_change('value', update)

color = Select(title='Color', value='None', options=['None'] + quantileable)
color.on_change('value', update)

controls = widgetbox([x, y, color, size], width=200)
layout = row(controls, create_figure())

curdoc().title = "Crossfilter"
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
import numpy as np
from import curdoc
from bokeh.layouts import layout
from bokeh.models import (
ColumnDataSource, HoverTool, SingleIntervalTicker, Slider, Button, Label,
import holoviews as hv
import holoviews.plotting.bokeh

renderer = hv.Store.renderers['bokeh']

start = 0
end = 10

hmap = hv.HoloMap({i: hv.Image(np.random.rand(10,10)) for i in range(start, end+1)})
plot = renderer.get_plot(hmap)

def animate_update():
year = slider.value + 1
if year > end:
year = start
slider.value = year

def slider_update(attrname, old, new):

slider = Slider(start=start, end=end, value=0, step=1, title="Year")
slider.on_change('value', slider_update)

def animate():
if button.label == '► Play':
button.label = '❚❚ Pause'
curdoc().add_periodic_callback(animate_update, 200)
button.label = '► Play'

button = Button(label='► Play', width=60)

layout = layout([
[slider, button],
], sizing_mode='fixed')

@@ -0,0 +1,17 @@
import numpy as np
import holoviews as hv
import holoviews.plotting.bokeh
from holoviews.streams import Selection1D

hv.Store.current_backend = 'bokeh'
renderer = hv.Store.renderers['bokeh'].instance(mode='server')
hv.Store.options(backend='bokeh').Points = hv.Options('plot', tools=['box_select'])

data = np.random.multivariate_normal((0, 0), [[1, 0.1], [0.1, 1]], (1000,))
points = hv.Points(data)
sel = Selection1D(source=points)
mean_sel = hv.DynamicMap(lambda index: hv.HLine(points['y'][index].mean()
if index else -10),
kdims=[], streams=[sel])
doc,_ = renderer((points * mean_sel))
doc.title = 'HoloViews Selection Stream'
@@ -81,6 +81,18 @@ def get_data(self, element, ranges=None, empty=False):
raise NotImplementedError

def push(self):
Pushes updated plot data via the Comm.
if self.renderer.mode == 'server':
if self.comm is None:
raise Exception('Renderer does not have a comm.')
diff = self.renderer.diff(self)

def set_root(self, root):
Sets the current document on all subplots.
@@ -342,6 +354,7 @@ def _create_subplots(self, layout, ranges):
subplot = plotting_class(view, dimensions=self.dimensions,
show_title=False, subplot=True,
ranges=frame_ranges, uniform=self.uniform,
keys=self.keys, **dict(opts, **kwargs))
collapsed_layout[coord] = (subplot.layout
@@ -569,6 +582,7 @@ def _create_subplots(self, layout, positions, layout_dimensions, ranges, num=0):
ranges=ranges, subplot=True,
uniform=self.uniform, layout_num=num,
**dict({'shared_axes': self.shared_axes},
subplots[pos] = subplot
@@ -7,15 +7,16 @@
from bokeh.charts import Chart
from bokeh.document import Document
from bokeh.embed import notebook_div
from import load_notebook
from import load_notebook, curdoc
from bokeh.models import (Row, Column, Plot, Model, ToolbarBox,
WidgetBox, Div, DataTable, Tabs)
from bokeh.plotting import Figure
from bokeh.resources import CDN, INLINE

from ...core import Store, HoloMap
from ..comms import JupyterComm, Comm
from ..renderer import Renderer, MIME_TYPES
from .widgets import BokehScrubberWidget, BokehSelectionWidget
from .widgets import BokehScrubberWidget, BokehSelectionWidget, BokehServerWidgets
from .util import compute_static_patch, serialize_json

@@ -28,22 +29,38 @@ class BokehRenderer(Renderer):
Output render format for static figures. If None, no figure
rendering will occur. """)

holomap = param.ObjectSelector(default='auto',
objects=['widgets', 'scrubber', 'server',
None, 'auto'], doc="""
Output render multi-frame (typically animated) format. If
None, no multi-frame rendering will occur.""")

mode = param.ObjectSelector(default='default',
objects=['default', 'server'], doc="""
Whether to render the DynamicMap in regular or server mode. """)

This comment has been minimized.

Copy link

jlstevens Apr 7, 2017


Is it only rendering DynamicMaps? Might want a bit more to say what server mode is about ...

This comment has been minimized.

Copy link

philippjfr Apr 7, 2017

Author Contributor

Hmm, not sure why I wrote that. It handles anything.

# Defines the valid output formats for each mode.
mode_formats = {'fig': {'default': ['html', 'json', 'auto']},
'holomap': {'default': ['widgets', 'scrubber', 'auto', None]}}
mode_formats = {'fig': {'default': ['html', 'json', 'auto'],
'server': ['html', 'json', 'auto']},
'holomap': {'default': ['widgets', 'scrubber', 'auto', None],
'server': ['server', 'auto', None]}}

webgl = param.Boolean(default=False, doc="""Whether to render plots with WebGL
if bokeh version >=0.10""")

widgets = {'scrubber': BokehScrubberWidget,
'widgets': BokehSelectionWidget}
'widgets': BokehSelectionWidget,
'server': BokehServerWidgets}

backend_dependencies = {'js': CDN.js_files if CDN.js_files else tuple(INLINE.js_raw),
'css': CDN.css_files if CDN.css_files else tuple(INLINE.css_raw)}

comms = {'default': (JupyterComm, None),
'server': (Comm, None)}

_loaded = False

def __call__(self, obj, fmt=None):
def __call__(self, obj, fmt=None, doc=None):
Render the supplied HoloViews component using the appropriate
backend. The output is not a file format but a suitable,
@@ -52,19 +69,44 @@ def __call__(self, obj, fmt=None):
plot, fmt = self._validate(obj, fmt)
info = {'file-ext': fmt, 'mime_type': MIME_TYPES[fmt]}

if isinstance(plot, tuple(self.widgets.values())):
if self.mode == 'server':
return self.server_doc(plot, doc), info
elif isinstance(plot, tuple(self.widgets.values())):
return plot(), info
elif fmt == 'html':
html = self.figure_data(plot)
html = self.figure_data(plot, doc=doc)
html = "<div style='display: table; margin: 0 auto;'>%s</div>" % html
return self._apply_post_render_hooks(html, obj, fmt), info
elif fmt == 'json':
return self.diff(plot), info

def get_widget(self_or_cls, plot, widget_type, **kwargs):
if not isinstance(plot, Plot):
plot = self_or_cls.get_plot(plot)
if self_or_cls.mode == 'server':
return BokehServerWidgets(plot, renderer=self_or_cls.instance(), **kwargs)
return super(BokehRenderer, self_or_cls).get_widget(plot, widget_type, **kwargs)

def server_doc(self, plot, doc=None):
Get server document.
if doc is None:
doc = curdoc()
if isinstance(plot, BokehServerWidgets):
plot.plot.document = doc
plot.document = doc
return doc

def figure_data(self, plot, fmt='html', **kwargs):
def figure_data(self, plot, fmt='html', doc=None, **kwargs):
model = plot.state
doc = Document()
doc = Document() if doc is None else doc
for m in model.references():
m._document = None
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.