From 4a319b05ba7de5118c4ae836940b6c1e1f0da76e Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Mon, 24 Oct 2016 20:57:22 +0100
Subject: [PATCH 1/5] Added Callable and OperationCallable
Allows traversing DynamicMap callbacks to get streams and sources
---
holoviews/core/operation.py | 6 +++++-
holoviews/core/overlay.py | 10 +++++++---
holoviews/core/spaces.py | 33 ++++++++++++++++++++++++---------
holoviews/util.py | 25 +++++++++++++++----------
4 files changed, 51 insertions(+), 23 deletions(-)
diff --git a/holoviews/core/operation.py b/holoviews/core/operation.py
index 99f20e596d..2d92e2a5a3 100644
--- a/holoviews/core/operation.py
+++ b/holoviews/core/operation.py
@@ -14,7 +14,7 @@
from .element import Element, HoloMap, GridSpace, Collator
from .layout import Layout
from .overlay import NdOverlay, Overlay
-from .spaces import DynamicMap
+from .spaces import DynamicMap, Callable
from .traversal import unique_dimkeys
from . import util
@@ -160,6 +160,10 @@ def __call__(self, element, **params):
return processed
+class OperationCallable(Callable):
+
+ operation = param.ClassSelector(class_=ElementOperation)
+
class MapOperation(param.ParameterizedFunction):
"""
diff --git a/holoviews/core/overlay.py b/holoviews/core/overlay.py
index 0a611da9ca..698dc1c10f 100644
--- a/holoviews/core/overlay.py
+++ b/holoviews/core/overlay.py
@@ -24,10 +24,14 @@ class Overlayable(object):
def __mul__(self, other):
if type(other).__name__ == 'DynamicMap':
- from ..util import Dynamic
- def dynamic_mul(element):
+ from .spaces import Callable
+ def dynamic_mul(*args, **kwargs):
+ element = other[args]
return self * element
- return Dynamic(other, operation=dynamic_mul)
+ callback = Callable(callable_function=dynamic_mul,
+ objects=[self, other])
+ return other.clone(shared_data=False, callback=callback,
+ streams=[])
if isinstance(other, UniformNdMapping) and not isinstance(other, CompositeOverlay):
items = [(k, self * v) for (k, v) in other.items()]
return other.clone(items)
diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py
index 9a9eaa461f..929f10d575 100644
--- a/holoviews/core/spaces.py
+++ b/holoviews/core/spaces.py
@@ -120,12 +120,13 @@ def _dynamic_mul(self, dimensions, other, keys):
map_obj = self if isinstance(self, DynamicMap) else other
mode = map_obj.mode
- def dynamic_mul(*key):
+ def dynamic_mul(*key, **kwargs):
key = key[0] if mode == 'open' else key
layers = []
try:
if isinstance(self, DynamicMap):
- _, self_el = util.get_dynamic_item(self, dimensions, key)
+ safe_key = () if not self.kdims else key
+ _, self_el = util.get_dynamic_item(self, dimensions, safe_key)
if self_el is not None:
layers.append(self_el)
else:
@@ -134,7 +135,8 @@ def dynamic_mul(*key):
pass
try:
if isinstance(other, DynamicMap):
- _, other_el = util.get_dynamic_item(other, dimensions, key)
+ safe_key = () if not other.kdims else key
+ _, other_el = util.get_dynamic_item(other, dimensions, safe_key)
if other_el is not None:
layers.append(other_el)
else:
@@ -142,11 +144,12 @@ def dynamic_mul(*key):
except KeyError:
pass
return Overlay(layers)
+ callback = Callable(callable_function=dynamic_mul, objects=[self, other])
if map_obj:
- return map_obj.clone(callback=dynamic_mul, shared_data=False,
- kdims=dimensions)
+ return map_obj.clone(callback=callback, shared_data=False,
+ kdims=dimensions, streams=[])
else:
- return DynamicMap(callback=dynamic_mul, kdims=dimensions)
+ return DynamicMap(callback=callback, kdims=dimensions)
def __mul__(self, other):
@@ -204,10 +207,13 @@ def __mul__(self, other):
return self.clone(items, kdims=dimensions, label=self._label, group=self._group)
elif isinstance(other, self.data_type):
if isinstance(self, DynamicMap):
- from ..util import Dynamic
- def dynamic_mul(element):
+ def dynamic_mul(*args, **kwargs):
+ element = self[args]
return element * other
- return Dynamic(self, operation=dynamic_mul)
+ callback = Callable(callable_function=dynamic_mul,
+ objects=[self, other])
+ return self.clone(shared_data=False, callback=callback,
+ streams=[])
items = [(k, v * other) for (k, v) in self.data.items()]
return self.clone(items, label=self._label, group=self._group)
else:
@@ -393,6 +399,15 @@ def hist(self, num_bins=20, bin_range=None, adjoin=True, individually=True, **kw
return histmaps[0]
+class Callable(param.Parameterized):
+
+ callable_function = param.Callable(default=lambda x: x)
+
+ objects = param.List(default=[])
+
+ def __call__(self, *args, **kwargs):
+ return self.callable_function(*args, **kwargs)
+
class DynamicMap(HoloMap):
"""
diff --git a/holoviews/util.py b/holoviews/util.py
index e42aa3f339..6d52436e10 100644
--- a/holoviews/util.py
+++ b/holoviews/util.py
@@ -5,6 +5,8 @@
from .core import DynamicMap, ViewableElement
from .core.operation import ElementOperation
from .core.util import Aliases
+from .core.operation import OperationCallable
+from .core.spaces import Callable
from .core import util
from .streams import Stream
@@ -33,7 +35,8 @@ def __call__(self, map_obj, **params):
self.p = param.ParamOverrides(self, params)
callback = self._dynamic_operation(map_obj)
if isinstance(map_obj, DynamicMap):
- dmap = map_obj.clone(callback=callback, shared_data=False)
+ dmap = map_obj.clone(callback=callback, shared_data=False,
+ streams=[])
else:
dmap = self._make_dynamic(map_obj, callback)
if isinstance(self.p.operation, ElementOperation):
@@ -69,15 +72,17 @@ def _dynamic_operation(self, map_obj):
def dynamic_operation(*key, **kwargs):
self.p.kwargs.update(kwargs)
return self._process(map_obj[key], key)
- return dynamic_operation
-
- def dynamic_operation(*key, **kwargs):
- key = key[0] if map_obj.mode == 'open' else key
- self.p.kwargs.update(kwargs)
- _, el = util.get_dynamic_item(map_obj, map_obj.kdims, key)
- return self._process(el, key)
-
- return dynamic_operation
+ else:
+ def dynamic_operation(*key, **kwargs):
+ key = key[0] if map_obj.mode == 'open' else key
+ self.p.kwargs.update(kwargs)
+ _, el = util.get_dynamic_item(map_obj, map_obj.kdims, key)
+ return self._process(el, key)
+ if isinstance(self.p.operation, ElementOperation):
+ return OperationCallable(callable_function=dynamic_operation,
+ objects=[map_obj], operation=self.p.operation)
+ else:
+ return Callable(callable_function=dynamic_operation, objects=[map_obj])
def _make_dynamic(self, hmap, dynamic_fn):
From 95430c853b9072de5a485873b30fd5c9cee1abf9 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Mon, 24 Oct 2016 21:44:08 +0100
Subject: [PATCH 2/5] Process Callable streams on Plots
---
holoviews/core/spaces.py | 16 ++++++++++-
holoviews/core/util.py | 8 +++---
holoviews/plotting/plot.py | 6 ++---
holoviews/plotting/renderer.py | 2 +-
holoviews/plotting/util.py | 37 ++++++++++++++++++++++++--
holoviews/plotting/widgets/__init__.py | 6 ++++-
6 files changed, 63 insertions(+), 12 deletions(-)
diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py
index 929f10d575..0b69e9fbda 100644
--- a/holoviews/core/spaces.py
+++ b/holoviews/core/spaces.py
@@ -409,6 +409,19 @@ def __call__(self, *args, **kwargs):
return self.callable_function(*args, **kwargs)
+def get_streams(dmap):
+ """
+ Get streams from DynamicMap with Callable callback.
+ """
+ layer_streams = list(dmap.streams)
+ if not isinstance(dmap.callback, Callable):
+ return layer_streams
+ for o in dmap.callback.objects:
+ if isinstance(o, DynamicMap):
+ layer_streams += get_streams(o)
+ return layer_streams
+
+
class DynamicMap(HoloMap):
"""
A DynamicMap is a type of HoloMap where the elements are dynamically
@@ -704,7 +717,8 @@ def __getitem__(self, key):
# Cache lookup
try:
- dimensionless = util.dimensionless_contents(self.streams, self.kdims)
+ dimensionless = util.dimensionless_contents(get_streams(self),
+ self.kdims, False)
if (dimensionless and not self._dimensionless_cache):
raise KeyError('Using dimensionless streams disables DynamicMap cache')
cache = super(DynamicMap,self).__getitem__(key)
diff --git a/holoviews/core/util.py b/holoviews/core/util.py
index 797103fba3..1fca496ede 100644
--- a/holoviews/core/util.py
+++ b/holoviews/core/util.py
@@ -799,21 +799,21 @@ def stream_parameters(streams, no_duplicates=True, exclude=['name']):
return [name for name in names if name not in exclude]
-def dimensionless_contents(streams, kdims):
+def dimensionless_contents(streams, kdims, no_duplicates=True):
"""
Return a list of stream parameters that have not been associated
with any of the key dimensions.
"""
- names = stream_parameters(streams)
+ names = stream_parameters(streams, no_duplicates)
return [name for name in names if name not in kdims]
-def unbound_dimensions(streams, kdims):
+def unbound_dimensions(streams, kdims, no_duplicates=True):
"""
Return a list of dimensions that have not been associated with
any streams.
"""
- params = stream_parameters(streams)
+ params = stream_parameters(streams, no_duplicates)
return [d for d in kdims if d not in params]
diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py
index d52691a18f..4b42e83128 100644
--- a/holoviews/plotting/plot.py
+++ b/holoviews/plotting/plot.py
@@ -22,7 +22,7 @@
from ..core.util import stream_parameters
from ..element import Table
from .util import (get_dynamic_mode, initialize_sampled, dim_axis_label,
- attach_streams, traverse_setter)
+ attach_streams, traverse_setter, get_streams)
class Plot(param.Parameterized):
@@ -578,7 +578,7 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None,
**dict(params, **plot_opts))
if top_level:
self.comm = self.init_comm(element)
- self.streams = self.hmap.streams if isinstance(self.hmap, DynamicMap) else []
+ self.streams = get_streams(self.hmap) if isinstance(self.hmap, DynamicMap) else []
# Update plot and style options for batched plots
if self.batched:
@@ -928,7 +928,7 @@ def __init__(self, layout, keys=None, dimensions=None, **params):
if top_level:
self.comm = self.init_comm(layout)
self.traverse(lambda x: setattr(x, 'comm', self.comm))
- self.streams = [s for streams in layout.traverse(lambda x: x.streams,
+ self.streams = [s for streams in layout.traverse(lambda x: get_streams(streams),
[DynamicMap])
for s in streams]
diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py
index 5bdc01ff6d..046c8d9f28 100644
--- a/holoviews/plotting/renderer.py
+++ b/holoviews/plotting/renderer.py
@@ -204,7 +204,7 @@ def _validate(self, obj, fmt):
if (((len(plot) == 1 and not plot.dynamic)
or (len(plot) > 1 and self.holomap is None) or
(plot.dynamic and len(plot.keys[0]) == 0)) or
- not unbound_dimensions(plot.streams, plot.dimensions)):
+ not unbound_dimensions(plot.streams, plot.dimensions, False)):
fmt = fig_formats[0] if self.fig=='auto' else self.fig
else:
fmt = holomap_formats[0] if self.holomap=='auto' else self.holomap
diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py
index f272018594..0a31ee1ddc 100644
--- a/holoviews/plotting/util.py
+++ b/holoviews/plotting/util.py
@@ -4,7 +4,8 @@
import param
from ..core import (HoloMap, DynamicMap, CompositeOverlay, Layout,
- GridSpace, NdLayout, Store)
+ GridSpace, NdLayout, Store, Callable, Overlay)
+from ..core.spaces import get_streams
from ..core.util import (match_spec, is_number, wrap_tuple, basestring,
get_overlay_spec, unique_iterator, safe_unicode)
@@ -282,11 +283,43 @@ def attach_streams(plot, obj):
Attaches plot refresh to all streams on the object.
"""
def append_refresh(dmap):
- for stream in dmap.streams:
+ for stream in get_streams(dmap):
stream._hidden_subscribers.append(plot.refresh)
return obj.traverse(append_refresh, [DynamicMap])
+def get_sources(obj, index=None):
+ """
+ Traverses Callable graph to resolve sources on
+ DynamicMap objects, returning a list of sources
+ indexed by the Overlay layer.
+ """
+ if isinstance(obj, DynamicMap):
+ if isinstance(obj.callback, Callable):
+ if len(obj.callback.objects) > 1:
+ layers = [(None, obj)]
+ else:
+ layers = [(index, obj)]
+ else:
+ return [(index, obj)]
+ else:
+ return [(index, obj)]
+ index = 0 if index is None else int(index)
+ for o in obj.callback.objects:
+ if isinstance(o, Overlay):
+ layers.append((None, o))
+ for i, o in enumerate(overlay):
+ layers.append((index+i, o))
+ index += len(o)
+ elif isinstance(o, DynamicMap):
+ layers += get_sources(o, index)
+ index = layers[-1][0]+1
+ else:
+ layers.append((index, o))
+ index += 1
+ return layers
+
+
def traverse_setter(obj, attribute, value):
"""
Traverses the object and sets the supplied attribute on the
diff --git a/holoviews/plotting/widgets/__init__.py b/holoviews/plotting/widgets/__init__.py
index eb38fdb481..0d1b634b8a 100644
--- a/holoviews/plotting/widgets/__init__.py
+++ b/holoviews/plotting/widgets/__init__.py
@@ -108,7 +108,11 @@ def __init__(self, plot, renderer=None, **params):
super(NdWidget, self).__init__(**params)
self.id = plot.comm.target if plot.comm else uuid.uuid4().hex
self.plot = plot
- self.dimensions, self.keys = drop_streams(plot.streams,
+ streams = []
+ for stream in plot.streams:
+ if any(k in plot.dimensions for k in stream.contents):
+ streams.append(stream)
+ self.dimensions, self.keys = drop_streams(streams,
plot.dimensions,
plot.keys)
From 8142188f5dd2701552fb19467eb273c54bf28c9a Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Mon, 24 Oct 2016 21:45:10 +0100
Subject: [PATCH 3/5] Register bokeh callbacks for Callable streams
---
holoviews/plotting/bokeh/element.py | 29 ++++++++++++++++++++---------
1 file changed, 20 insertions(+), 9 deletions(-)
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
index d6104e4932..88ca993702 100644
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -31,7 +31,7 @@
from ...element import RGB
from ...streams import Stream, RangeXY, RangeX, RangeY
from ..plot import GenericElementPlot, GenericOverlayPlot
-from ..util import dynamic_update
+from ..util import dynamic_update, get_sources
from .plot import BokehPlot
from .util import (mpl_to_bokeh, convert_datetime, update_plot,
bokeh_version, mplcmap_to_palette)
@@ -177,15 +177,18 @@ def _construct_callbacks(self):
the plotted object as a source.
"""
if not self.static or isinstance(self.hmap, DynamicMap):
- source = self.hmap
+ sources = [(i, o) for i, o in get_sources(self.hmap)
+ if i in [None, self.zorder]]
else:
- source = self.hmap.last
- streams = Stream.registry.get(id(source), [])
- registry = Stream._callbacks['bokeh']
- callbacks = {(registry[type(stream)], stream) for stream in streams
- if type(stream) in registry and streams}
+ sources = [(self.zorder, self.hmap.last)]
+ cb_classes = set()
+ for _, source in sources:
+ streams = Stream.registry.get(id(source), [])
+ registry = Stream._callbacks['bokeh']
+ cb_classes |= {(registry[type(stream)], stream) for stream in streams
+ if type(stream) in registry and streams}
cbs = []
- sorted_cbs = sorted(callbacks, key=lambda x: id(x[0]))
+ sorted_cbs = sorted(cb_classes, key=lambda x: id(x[0]))
for cb, group in groupby(sorted_cbs, lambda x: x[0]):
cb_streams = [s for _, s in group]
cbs.append(cb(self, cb_streams, source))
@@ -560,6 +563,11 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None):
if plot is None:
plot = self._init_plot(key, style_element, ranges=ranges, plots=plots)
self._init_axes(plot)
+ else:
+ self.handles['xaxis'] = plot.xaxis[0]
+ self.handles['x_range'] = plot.x_range
+ self.handles['y_axis'] = plot.yaxis[0]
+ self.handles['y_range'] = plot.y_range
self.handles['plot'] = plot
# Get data and initialize data source
@@ -675,7 +683,10 @@ def current_handles(self):
rangex, rangey = True, True
elif isinstance(self.hmap, DynamicMap):
rangex, rangey = True, True
- for stream in self.hmap.streams:
+ callbacks = [cb for p in [self]+list(self.subplots.values())
+ for cb in p.callbacks]
+ streams = [s for cb in callbacks for s in cb.streams]
+ for stream in streams:
if isinstance(stream, RangeXY):
rangex, rangey = False, False
break
From b75c3998469d15f2d3582fb89d2700dcd8106bda Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Mon, 24 Oct 2016 22:04:42 +0100
Subject: [PATCH 4/5] Fixed bokeh ElementPlot.current_handles bug
---
holoviews/plotting/bokeh/element.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
index 88ca993702..4ecb70c6fa 100644
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -683,8 +683,8 @@ def current_handles(self):
rangex, rangey = True, True
elif isinstance(self.hmap, DynamicMap):
rangex, rangey = True, True
- callbacks = [cb for p in [self]+list(self.subplots.values())
- for cb in p.callbacks]
+ subplots = list(self.subplots.values()) if self.subplots else []
+ callbacks = [cb for p in [self]+subplots for cb in p.callbacks]
streams = [s for cb in callbacks for s in cb.streams]
for stream in streams:
if isinstance(stream, RangeXY):
From bb871843c0d750b12edf207624b62e4ddc6f79e0 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Mon, 24 Oct 2016 22:18:01 +0100
Subject: [PATCH 5/5] Fixed streams bug in GenericCompositePlot
---
holoviews/plotting/plot.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py
index 4b42e83128..586724a2bf 100644
--- a/holoviews/plotting/plot.py
+++ b/holoviews/plotting/plot.py
@@ -928,7 +928,7 @@ def __init__(self, layout, keys=None, dimensions=None, **params):
if top_level:
self.comm = self.init_comm(layout)
self.traverse(lambda x: setattr(x, 'comm', self.comm))
- self.streams = [s for streams in layout.traverse(lambda x: get_streams(streams),
+ self.streams = [s for streams in layout.traverse(lambda x: get_streams(x),
[DynamicMap])
for s in streams]