From 5558dbef274b11d038ef66d4bcf95645ce658062 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 24 Oct 2016 20:57:22 +0100 Subject: [PATCH 01/10] 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 cf162b1597..e066f1cb04 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 4a47dff4d554aaeed0ad3cb9abc883783021e8be Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 24 Oct 2016 21:44:08 +0100 Subject: [PATCH 02/10] Process Callable streams on Plots --- doc/builder | 2 +- 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 ++++- 7 files changed, 64 insertions(+), 13 deletions(-) diff --git a/doc/builder b/doc/builder index f8685fee76..fe584ca37d 160000 --- a/doc/builder +++ b/doc/builder @@ -1 +1 @@ -Subproject commit f8685fee7691375a064d4f4265fd826cdbe3d3cd +Subproject commit fe584ca37d850315c21fa9e7602f56213c7a48ca 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 26aaaa5d14..b928ae198f 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, Overlay) + 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) @@ -295,11 +296,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 52d716c4687e070963806400d9834827e66c350c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 24 Oct 2016 21:45:10 +0100 Subject: [PATCH 03/10] 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 67e5a390c957686829c4ef2361ba4154cd104107 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 24 Oct 2016 22:04:42 +0100 Subject: [PATCH 04/10] 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 628c19ed1d4323b749a7d629eeea8295f0223afe Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 24 Oct 2016 22:18:01 +0100 Subject: [PATCH 05/10] 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] From e3b2b888cabccc968bc5881571ce176c6fcc3676 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 30 Oct 2016 19:12:28 +0000 Subject: [PATCH 06/10] Updated doc/builder --- doc/builder | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/builder b/doc/builder index fe584ca37d..f8685fee76 160000 --- a/doc/builder +++ b/doc/builder @@ -1 +1 @@ -Subproject commit fe584ca37d850315c21fa9e7602f56213c7a48ca +Subproject commit f8685fee7691375a064d4f4265fd826cdbe3d3cd From 2361036cb65900964b68a50eae1d27966215955f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 30 Oct 2016 19:37:48 +0000 Subject: [PATCH 07/10] Added docstrings for Callable classes --- holoviews/core/operation.py | 8 +++++++- holoviews/core/spaces.py | 13 +++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/holoviews/core/operation.py b/holoviews/core/operation.py index 2d92e2a5a3..4a3c2c9e7e 100644 --- a/holoviews/core/operation.py +++ b/holoviews/core/operation.py @@ -161,8 +161,14 @@ def __call__(self, element, **params): class OperationCallable(Callable): + """ + OperationCallable allows wrapping an ElementOperation and the + objects it is processing to allow traversing the operations + applied on a DynamicMap. + """ - operation = param.ClassSelector(class_=ElementOperation) + operation = param.ClassSelector(class_=ElementOperation, doc=""" + The ElementOperation being wrapped.""") class MapOperation(param.ParameterizedFunction): diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 0b69e9fbda..ed72b24f0e 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -400,10 +400,19 @@ def hist(self, num_bins=20, bin_range=None, adjoin=True, individually=True, **kw class Callable(param.Parameterized): + """ + Callable allows wrapping callbacks on one or more DynamicMaps such + that the operation and the objects that are part of the operation + are made available. This allows traversing a DynamicMap that wraps + multiple operations and is primarily used to extracting any + streams attached to the object. + """ - callable_function = param.Callable(default=lambda x: x) + callable_function = param.Callable(default=lambda x: x, doc=""" + The callable function being wrapped.""") - objects = param.List(default=[]) + objects = param.List(default=[], doc=""" + The objects the callable function is processing.""") def __call__(self, *args, **kwargs): return self.callable_function(*args, **kwargs) From f710fa2ffaec5c41b8a2c5272672683c293a79ad Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 30 Oct 2016 19:38:08 +0000 Subject: [PATCH 08/10] Small fix for Dynamic --- holoviews/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/util.py b/holoviews/util.py index 6d52436e10..597309a6e7 100644 --- a/holoviews/util.py +++ b/holoviews/util.py @@ -47,7 +47,7 @@ def __call__(self, map_obj, **params): elif not isinstance(stream, Stream): raise ValueError('Stream must only contain Stream ' 'classes or instances') - stream.update(**{k: self.p.operation.p.get(k) for k, v in + stream.update(**{k: self.p.operation.p.get(k, v) for k, v in stream.contents.items()}) streams.append(stream) return dmap.clone(streams=streams) From ca945bf92e41bcfe9d3975bd53fbdbf4719f01a3 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 30 Oct 2016 23:16:00 +0000 Subject: [PATCH 09/10] Renamed Callable.objects to Callable.inputs --- holoviews/core/overlay.py | 2 +- holoviews/core/spaces.py | 20 ++++++++++---------- holoviews/plotting/util.py | 4 ++-- holoviews/util.py | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/holoviews/core/overlay.py b/holoviews/core/overlay.py index e066f1cb04..6cd903d4b8 100644 --- a/holoviews/core/overlay.py +++ b/holoviews/core/overlay.py @@ -29,7 +29,7 @@ def dynamic_mul(*args, **kwargs): element = other[args] return self * element callback = Callable(callable_function=dynamic_mul, - objects=[self, other]) + inputs=[self, other]) return other.clone(shared_data=False, callback=callback, streams=[]) if isinstance(other, UniformNdMapping) and not isinstance(other, CompositeOverlay): diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index ed72b24f0e..9b2063375a 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -144,7 +144,7 @@ def dynamic_mul(*key, **kwargs): except KeyError: pass return Overlay(layers) - callback = Callable(callable_function=dynamic_mul, objects=[self, other]) + callback = Callable(callable_function=dynamic_mul, inputs=[self, other]) if map_obj: return map_obj.clone(callback=callback, shared_data=False, kdims=dimensions, streams=[]) @@ -211,7 +211,7 @@ def dynamic_mul(*args, **kwargs): element = self[args] return element * other callback = Callable(callable_function=dynamic_mul, - objects=[self, other]) + inputs=[self, other]) return self.clone(shared_data=False, callback=callback, streams=[]) items = [(k, v * other) for (k, v) in self.data.items()] @@ -401,18 +401,18 @@ def hist(self, num_bins=20, bin_range=None, adjoin=True, individually=True, **kw class Callable(param.Parameterized): """ - Callable allows wrapping callbacks on one or more DynamicMaps such - that the operation and the objects that are part of the operation - are made available. This allows traversing a DynamicMap that wraps - multiple operations and is primarily used to extracting any - streams attached to the object. + Callable allows wrapping callbacks on one or more DynamicMaps + allowing their inputs (and in future outputs) to be defined. + This makes it possible to wrap DynamicMaps with streams and + makes it possible to traverse the graph of operations applied + to a DynamicMap. """ callable_function = param.Callable(default=lambda x: x, doc=""" The callable function being wrapped.""") - objects = param.List(default=[], doc=""" - The objects the callable function is processing.""") + inputs = param.List(default=[], doc=""" + The list of inputs the callable function is wrapping.""") def __call__(self, *args, **kwargs): return self.callable_function(*args, **kwargs) @@ -425,7 +425,7 @@ def get_streams(dmap): layer_streams = list(dmap.streams) if not isinstance(dmap.callback, Callable): return layer_streams - for o in dmap.callback.objects: + for o in dmap.callback.inputs: if isinstance(o, DynamicMap): layer_streams += get_streams(o) return layer_streams diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index b928ae198f..81347581ef 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -309,7 +309,7 @@ def get_sources(obj, index=None): """ if isinstance(obj, DynamicMap): if isinstance(obj.callback, Callable): - if len(obj.callback.objects) > 1: + if len(obj.callback.inputs) > 1: layers = [(None, obj)] else: layers = [(index, obj)] @@ -318,7 +318,7 @@ def get_sources(obj, index=None): else: return [(index, obj)] index = 0 if index is None else int(index) - for o in obj.callback.objects: + for o in obj.callback.inputs: if isinstance(o, Overlay): layers.append((None, o)) for i, o in enumerate(overlay): diff --git a/holoviews/util.py b/holoviews/util.py index 597309a6e7..3961b3c084 100644 --- a/holoviews/util.py +++ b/holoviews/util.py @@ -80,9 +80,9 @@ def dynamic_operation(*key, **kwargs): return self._process(el, key) if isinstance(self.p.operation, ElementOperation): return OperationCallable(callable_function=dynamic_operation, - objects=[map_obj], operation=self.p.operation) + inputs=[map_obj], operation=self.p.operation) else: - return Callable(callable_function=dynamic_operation, objects=[map_obj]) + return Callable(callable_function=dynamic_operation, inputs=[map_obj]) def _make_dynamic(self, hmap, dynamic_fn): From e019eabad5938f326ffad838d3208f1e7ad30b94 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 31 Oct 2016 00:51:58 +0000 Subject: [PATCH 10/10] Cleanup of Callable changes --- holoviews/core/spaces.py | 11 ++++++----- holoviews/plotting/plot.py | 13 ++++++++----- holoviews/plotting/renderer.py | 2 +- holoviews/plotting/util.py | 17 +++++------------ 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 9b2063375a..2f5ced9940 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -418,16 +418,17 @@ def __call__(self, *args, **kwargs): return self.callable_function(*args, **kwargs) -def get_streams(dmap): +def get_nested_streams(dmap): """ - Get streams from DynamicMap with Callable callback. + Get all (potentially nested) 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.inputs: if isinstance(o, DynamicMap): - layer_streams += get_streams(o) + layer_streams += get_nested_streams(o) return layer_streams @@ -726,8 +727,8 @@ def __getitem__(self, key): # Cache lookup try: - dimensionless = util.dimensionless_contents(get_streams(self), - self.kdims, False) + dimensionless = util.dimensionless_contents(get_nested_streams(self), + self.kdims, no_duplicates=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/plotting/plot.py b/holoviews/plotting/plot.py index 586724a2bf..66669c3397 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, get_streams) + attach_streams, traverse_setter, get_nested_streams) class Plot(param.Parameterized): @@ -578,7 +578,10 @@ 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 = get_streams(self.hmap) if isinstance(self.hmap, DynamicMap) else [] + streams = [] + if isinstance(self.hmap, DynamicMap): + streams = get_nested_streams(self.hmap) + self.streams = streams # Update plot and style options for batched plots if self.batched: @@ -928,9 +931,9 @@ 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(x), - [DynamicMap]) - for s in streams] + nested_streams = layout.traverse(lambda x: get_nested_streams(x), + [DynamicMap]) + self.streams = [s for streams in nested_streams for s in streams] def _get_frame(self, key): diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py index 046c8d9f28..a2eed14c2f 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, False)): + not unbound_dimensions(plot.streams, plot.dimensions, no_duplicates=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 81347581ef..80f6e4ea4f 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -5,7 +5,7 @@ from ..core import (HoloMap, DynamicMap, CompositeOverlay, Layout, GridSpace, NdLayout, Store, Callable, Overlay) -from ..core.spaces import get_streams +from ..core.spaces import get_nested_streams from ..core.util import (match_spec, is_number, wrap_tuple, basestring, get_overlay_spec, unique_iterator, safe_unicode) @@ -296,7 +296,7 @@ def attach_streams(plot, obj): Attaches plot refresh to all streams on the object. """ def append_refresh(dmap): - for stream in get_streams(dmap): + for stream in get_nested_streams(dmap): stream._hidden_subscribers.append(plot.refresh) return obj.traverse(append_refresh, [DynamicMap]) @@ -307,16 +307,9 @@ def get_sources(obj, index=None): 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.inputs) > 1: - layers = [(None, obj)] - else: - layers = [(index, obj)] - else: - return [(index, obj)] - else: - return [(index, obj)] + layers = [(index, obj)] + if not isinstance(obj, DynamicMap) or not isinstance(obj.callback, Callable): + return layers index = 0 if index is None else int(index) for o in obj.callback.inputs: if isinstance(o, Overlay):