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

Always

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):
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):
"""
@@ -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:
@@ -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"
@@ -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)
@@ -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':
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.
@@ -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
@@ -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
@@ -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


@@ -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

jlstevens Apr 7, 2017

Contributor

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

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

@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)
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.