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
merged 30 commits into from
Apr 7, 2017
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ab26f51
Added BokehRenderer server mode
philippjfr Oct 31, 2016
c1e2550
Added initial bokeh server stream callback handling
philippjfr Oct 31, 2016
afec80f
Small fixes for bokeh server implementation
philippjfr Oct 31, 2016
312975e
Added initial BokehServerWidgets implementation
philippjfr Oct 31, 2016
6f420b3
Fixes for BokehServerWidgets
philippjfr Oct 31, 2016
3b5d10c
Ensure all subplots have the same plotting classes
philippjfr Nov 4, 2016
c7cedc2
Defined bokeh widget parameters
philippjfr Nov 4, 2016
3b43aab
Added bokeh app examples
philippjfr Feb 3, 2017
7697a2a
Small fix for bokeh widget import
philippjfr Mar 26, 2017
27ee566
Improved handling of boomeranging events in bokeh backend
philippjfr Mar 26, 2017
78a2c2a
Improved bokeh server event queue
philippjfr Mar 26, 2017
c32e2ab
Improved range updates for bokeh server
philippjfr Mar 26, 2017
3f0dca8
Fixed small bugs in bokeh Callbacks
philippjfr Mar 26, 2017
9bf0982
Fixed bokeh event callbacks after change to cb_obj
philippjfr Apr 6, 2017
b099b7e
Implemented UIEvent handling for bokeh server
philippjfr Apr 6, 2017
dbef691
Moved bokeh server example apps
philippjfr Apr 6, 2017
2e9ca71
Completely refactored bokeh Callbacks
philippjfr Apr 6, 2017
d7f5e45
Made callback utilities into classmethods
philippjfr Apr 6, 2017
6ab8861
Minor cleanup on bokeh Callbacks
philippjfr Apr 6, 2017
82074b8
Added tests for bokeh Callbacks
philippjfr Apr 6, 2017
a8a10a5
Small fix for bokeh ServerCallback on_change events
philippjfr Apr 6, 2017
3342b0a
Allow supplying Document to BokehRenderer
philippjfr Apr 6, 2017
c65dcbb
Simplified bokeh Callback initialization
philippjfr Apr 6, 2017
a2fcb0c
Moved bokeh server widget handling onto BokehRenderer
philippjfr Apr 6, 2017
b0e1f52
Factored out class method to create bokeh widgets
philippjfr Apr 6, 2017
d9fe1b7
Small fixes and improvements for bokeh widgets
philippjfr Apr 6, 2017
f0c31c6
Added tests for BokehServerWidgets
philippjfr Apr 6, 2017
171e9ca
Fixed unreferenced variable bugs
philippjfr Apr 6, 2017
4e8073a
Various python3 fixes
philippjfr Apr 7, 2017
957b96b
Improved docstrings for bokeh server features
philippjfr Apr 7, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
583 changes: 358 additions & 225 deletions holoviews/plotting/bokeh/callbacks.py

Large diffs are not rendered by default.

33 changes: 22 additions & 11 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -788,6 +791,21 @@ def update_frame(self, key, ranges=None, plot=None, element=None, empty=False):
self._execute_hooks(element)


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())


@property
def current_handles(self):
"""
Expand Down Expand Up @@ -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
else:
Expand Down
78 changes: 78 additions & 0 deletions holoviews/plotting/bokeh/examples/apps/apps/crossfilter.py
Original file line number Diff line number Diff line change
@@ -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.name]
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)
plot.initialize_plot()
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().add_root(layout)
curdoc().title = "Crossfilter"
49 changes: 49 additions & 0 deletions holoviews/plotting/bokeh/examples/apps/apps/player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
import numpy as np
from bokeh.io import curdoc
from bokeh.layouts import layout
from bokeh.models import (
ColumnDataSource, HoverTool, SingleIntervalTicker, Slider, Button, Label,
CategoricalColorMapper,
)
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)
plot.update(0)

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

def slider_update(attrname, old, new):
plot.update(slider.value)

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)
else:
button.label = '► Play'
curdoc().remove_periodic_callback(animate_update)

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

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

curdoc().add_root(layout)
17 changes: 17 additions & 0 deletions holoviews/plotting/bokeh/examples/apps/apps/selection_stream.py
Original file line number Diff line number Diff line change
@@ -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'
14 changes: 14 additions & 0 deletions holoviews/plotting/bokeh/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
return
if self.comm is None:
raise Exception('Renderer does not have a comm.')
diff = self.renderer.diff(self)
self.comm.send(diff)


def set_root(self, root):
"""
Sets the current document on all subplots.
Expand Down Expand Up @@ -342,6 +354,7 @@ def _create_subplots(self, layout, ranges):
else:
subplot = plotting_class(view, dimensions=self.dimensions,
show_title=False, subplot=True,
renderer=self.renderer,
ranges=frame_ranges, uniform=self.uniform,
keys=self.keys, **dict(opts, **kwargs))
collapsed_layout[coord] = (subplot.layout
Expand Down Expand Up @@ -569,6 +582,7 @@ def _create_subplots(self, layout, positions, layout_dimensions, ranges, num=0):
layout_dimensions=layout_dimensions,
ranges=ranges, subplot=True,
uniform=self.uniform, layout_num=num,
renderer=self.renderer,
**dict({'shared_axes': self.shared_axes},
**plotopts))
subplots[pos] = subplot
Expand Down
62 changes: 52 additions & 10 deletions holoviews/plotting/bokeh/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@
from bokeh.charts import Chart
from bokeh.document import Document
from bokeh.embed import notebook_div
from bokeh.io import load_notebook
from bokeh.io 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


Expand All @@ -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. """)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Expand All @@ -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

@bothmethod
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)
else:
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
else:
plot.document = doc
doc.add_root(plot.state)
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
doc.add_root(model)
Expand Down
Loading