Skip to content

Commit

Permalink
Merge 8ad43db into 5160d11
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Mar 26, 2017
2 parents 5160d11 + 8ad43db commit 21efccf
Show file tree
Hide file tree
Showing 11 changed files with 465 additions and 47 deletions.
78 changes: 78 additions & 0 deletions examples/apps/crossfilter.py
@@ -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 examples/apps/player.py
@@ -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 examples/apps/selection_stream.py
@@ -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'
111 changes: 85 additions & 26 deletions holoviews/plotting/bokeh/callbacks.py
Expand Up @@ -229,39 +229,59 @@ class Callback(object):
def __init__(self, plot, streams, source, **params):
self.plot = plot
self.streams = streams
self.comm = self._comm_type(plot, on_msg=self.on_msg)
if plot.renderer.mode != 'server':
self.comm = self._comm_type(plot, on_msg=self.on_msg)
self.source = source
self.handle_ids = defaultdict(dict)
self.callbacks = []
self.plot_handles = {}
self._event_queue = []


def initialize(self):
plots = [self.plot]
if self.plot.subplots:
plots += list(self.plot.subplots.values())

handles = self._get_plot_handles(plots)
self.plot_handles = self._get_plot_handles(plots)
requested = {}
for h in self.models+self.extra_models:
if h in handles:
requested[h] = handles[h]
if h in self.plot_handles:
requested[h] = self.plot_handles[h]
elif h in self.extra_models:
print("Warning %s could not find the %s model. "
"The corresponding stream may not work.")
self.handle_ids.update(self._get_stream_handle_ids(requested))

found = []
for plot in plots:
for handle_name in self.models:
if handle_name not in handles:
if handle_name not in self.plot_handles:
warn_args = (handle_name, type(self.plot).__name__,
type(self).__name__)
self.warning('%s handle not found on %s, cannot'
'attach %s callback' % warn_args)
print('%s handle not found on %s, cannot '
'attach %s callback' % warn_args)
continue
handle = handles[handle_name]
js_callback = self.get_customjs(requested)
self.set_customjs(js_callback, handle)
self.callbacks.append(js_callback)
handle = self.plot_handles[handle_name]

# Hash the plot handle with Callback type allowing multiple
# callbacks on one handle to be merged
cb_hash = (id(handle), id(type(self)))
if cb_hash in self._callbacks:
# Merge callbacks if another callback has already been attached
cb = self._callbacks[cb_hash]
cb.streams += self.streams
for k, v in self.handle_ids.items():
cb.handle_ids[k].update(v)
continue

if self.plot.renderer.mode == 'server':
self.set_onchange(plot.handles[handle_name])
else:
js_callback = self.get_customjs(requested)
self.set_customjs(js_callback, handle)
self.callbacks.append(js_callback)
self._callbacks[cb_hash] = self


def _filter_msg(self, msg, ids):
Expand All @@ -278,7 +298,7 @@ def _filter_msg(self, msg, ids):
else:
filtered_msg[k] = v
return filtered_msg


def on_msg(self, msg):
for stream in self.streams:
Expand Down Expand Up @@ -330,7 +350,58 @@ def _get_stream_handle_ids(self, handles):
return stream_handle_ids


def get_customjs(self, references):
def on_change(self, attr, old, new):
"""
Process change events adding timeout to process multiple concerted
value change at once rather than firing off multiple plot updates.
"""
self._event_queue.append((attr, old, new))
if self.trigger not in self.plot.document._session_callbacks:
self.plot.document.add_timeout_callback(self.trigger, 50)


def trigger(self):
"""
Trigger callback change event and triggering corresponding streams.
"""
if not self._event_queue:
return
self._event_queue = []

values = {}
for attr, path in self.attributes.items():
attr_path = path.split('.')
if attr_path[0] == 'cb_obj':
attr_path = self.models[0]
obj = self.plot_handles.get(attr_path[0])
attr_val = obj
if not obj:
raise Exception('Bokeh plot attribute %s could not be found' % path)
for p in attr_path[1:]:
if p == 'attributes':
continue
if isinstance(attr_val, dict):
attr_val = attr_val.get(p)
else:
attr_val = getattr(attr_val, p, None)
values[attr] = {'id': obj.ref['id'], 'value': attr_val}
self.on_msg(values)
self.plot.document.add_timeout_callback(self.trigger, 50)


def set_onchange(self, handle):
"""
Set up on_change events for bokeh server interactions.
"""
if self.events and bokeh_version >= '0.12.5':
for event in self.events:
handle.on_event(event, self.on_change)
elif self.change:
for change in self.change:
handle.on_change(change, self.on_change)


def set_customjs(self, handle, references):
"""
Creates a CustomJS callback that will send the requested
attributes back to python.
Expand All @@ -357,23 +428,11 @@ def set_customjs(self, js_callback, handle):
the requested callback handle.
"""

# Hash the plot handle with Callback type allowing multiple
# callbacks on one handle to be merged
cb_hash = (id(handle), id(type(self)))
if cb_hash in self._callbacks:
# Merge callbacks if another callback has already been attached
cb = self._callbacks[cb_hash]
if isinstance(cb, type(self)):
cb.streams += self.streams
for k, v in self.handle_ids.items():
cb.handle_ids[k].update(v)
return

self._callbacks[cb_hash] = self
if self.events and bokeh_version >= '0.12.5':
for event in self.events:
handle.js_on_event(event, js_callback)
elif self.change and bokeh_version >= '0.12.5':
elif self.change:
for change in self.change:
handle.js_on_change(change, js_callback)
elif hasattr(handle, 'callback'):
Expand Down
32 changes: 21 additions & 11 deletions holoviews/plotting/bokeh/element.py
Expand Up @@ -530,8 +530,10 @@ 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'])
if not self.model_changed(x_range):
self._update_range(x_range, l, r, xfactors, self.invert_xaxis, self._shared['x'])
if not self.model_changed(y_range):
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 +790,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 +838,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
14 changes: 14 additions & 0 deletions holoviews/plotting/bokeh/plot.py
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

0 comments on commit 21efccf

Please sign in to comment.