Skip to content

Commit

Permalink
Merge pull request #959 from ioam/bokeh_server
Browse files Browse the repository at this point in the history
Bokeh server support
  • Loading branch information
jlstevens authored Apr 7, 2017
2 parents 4d86fa0 + 957b96b commit 3503bb0
Show file tree
Hide file tree
Showing 13 changed files with 987 additions and 249 deletions.
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
64 changes: 54 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,40 @@ 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 object in regular or server mode. In server
mode a bokeh Document will be returned which can be served as a
bokeh server app. By default renders all output is rendered to HTML.""")

# 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 +71,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

0 comments on commit 3503bb0

Please sign in to comment.