From e5c246a308c426852a3267731e6a41d589f9b359 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 4 Apr 2017 15:39:53 +0100 Subject: [PATCH 01/13] Allowed overriding a Stream source --- holoviews/streams.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/streams.py b/holoviews/streams.py index bdf8465bf4..5218b066f6 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -149,7 +149,9 @@ def source(self): @source.setter def source(self, source): if self._source: - raise Exception('source has already been defined on stream.') + source_list = self.registry[id(self._source)] + if self in source_list: + source_list.remove(self) self._source = source self.registry[id(source)].append(self) From abee6be17b64cc8357edd03d689cd178b2a3e18e Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 4 Apr 2017 15:40:28 +0100 Subject: [PATCH 02/13] Made callable_function positional arg of Callable --- holoviews/core/spaces.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 04a3a90fba..b28650b73e 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -410,7 +410,9 @@ class Callable(param.Parameterized): inputs = param.List(default=[], doc=""" The list of inputs the callable function is wrapping.""") - def __init__(self, **params): + def __init__(self, callable_function=None, **params): + if callable_function is not None: + params['callable_function'] = callable_function super(Callable, self).__init__(**params) self._memoized = {} From b1ca5a1caf72b6ac3fb5291a6671f412927655a4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 4 Apr 2017 15:41:04 +0100 Subject: [PATCH 03/13] Raise Exception about uninitializable DynamicMap --- holoviews/core/spaces.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index b28650b73e..6ab7216d5b 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -498,11 +498,17 @@ def _initial_key(self): values on the key dimensions. """ key = [] + undefined = [] for kdim in self.kdims: if kdim.values: key.append(kdim.values[0]) elif kdim.range: key.append(kdim.range[0]) + else: + undefined.append(kdim) + if undefined: + raise KeyError('dimensions do not specify a range or values, ' + 'cannot supply initial key' % ', '.join(undefined)) return tuple(key) From 756f1a2813b7f849018a115d4ae203bd0753e379 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 4 Apr 2017 15:41:55 +0100 Subject: [PATCH 04/13] Implemented collation for nested DynamicMaps --- holoviews/core/spaces.py | 66 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 6ab7216d5b..fdac02aa6b 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -796,6 +796,72 @@ def dynamic_redim(obj): return Dynamic(redimmed, shared_data=True, operation=dynamic_redim) + def collate(self, streams=[]): + """ + Collation allows collapsing DynamicMaps with invalid nesting + hierarchies. This is particularly useful when defining + DynamicMaps returning an (Nd)Layout. Collating will split the + DynamicMap into an (Nd)Layout of individual DynamicMaps. Note + that the Layout should be of consistent length and types for + this to work correctly. In order to attach a stream as a source + for a particular object in the Layout you may supply either + a dictionary or list of lists of streams corresponding to each + Element in the Layout. + """ + # Initialize + if self.last is not None: + pass + else: + self[self._initial_key()] + + if isinstance(self.last, HoloMap): + # Get nested kdims and streams + streams = list(self.streams) + if isinstance(self.last, DynamicMap): + dimensions = [d(values=self.last.dimension_values(d.name)) + for d in self.last.kdims] + streams += self.last.streams + stream_kwargs = set() + for stream in streams: + contents = set(stream.contents()) + if stream_kwargs & contents: + raise KeyError('Cannot collate DynamicMaps with clashing ' + 'stream parameters.') + else: + dimensions = self.last.kdims + kdims = self.kdims+dimensions + + # Define callback + def collation_cb(*args, **kwargs): + return self[args[:self.ndims]][args[self.ndims:]] + callback = Callable(collation_cb, inputs=[self]) + + return self.clone(shared_data=False, callback=callback, + kdims=kdims, streams=streams) + elif isinstance(self.last, (Layout, NdLayout, GridSpace)): + # Expand Layout/NdLayout + from ..util import Dynamic + new_item = self.last.clone(shared_data=False) + for i, (k, v) in enumerate(self.last.items()): + if isinstance(streams, dict): + vstreams = streams.get(i, []) + elif isinstance(streams, list): + vstreams = streams[i] if i < len(streams) else [] + def collation_cb(collation_key=k, *args, **kwargs): + return self[args][collation_key] + callback = Callable(collation_cb, inputs=[self]) + vdmap = self.clone(callback=callback, shared_data=False, + streams=vstreams) + for stream in vstreams: + if stream.source is self: + stream.source = vdmap + new_item[k] = vdmap + return new_item + else: + self.warning('DynamicMap does not need to be collated.') + return dmap + + def groupby(self, dimensions=None, container_type=None, group_type=None, **kwargs): """ Implements a dynamic version of a groupby, which will From 2d0964777a129306ce84042e18359fba7041e994 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 4 Apr 2017 15:54:19 +0100 Subject: [PATCH 05/13] Handled automatic collation of DynamicMaps --- holoviews/plotting/renderer.py | 14 +++++--------- holoviews/plotting/util.py | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py index a06a94572b..26318b40e4 100644 --- a/holoviews/plotting/renderer.py +++ b/holoviews/plotting/renderer.py @@ -18,7 +18,7 @@ from .. import DynamicMap from . import Plot from .comms import JupyterComm -from .util import displayable, collate +from .util import displayable, collate, initialize_dynamic from param.parameterized import bothmethod @@ -159,16 +159,12 @@ def get_plot(self_or_cls, obj, renderer=None): """ Given a HoloViews Viewable return a corresponding plot instance. """ + # Initialize DynamicMaps with first data item + initialize_dynamic(obj) + if not isinstance(obj, Plot) and not displayable(obj): obj = collate(obj) - - # Initialize DynamicMaps with first data item - dmaps = obj.traverse(lambda x: x, specs=[DynamicMap]) - for dmap in dmaps: - if dmap.sampled: - # Skip initialization until plotting code - continue - dmap[dmap._initial_key()] + initialize_dynamic(obj) if not renderer: renderer = self_or_cls.instance() if not isinstance(obj, Plot): diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 199bd38092..45f3cacb5e 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -43,12 +43,12 @@ def collate(obj): "(http://git.io/vtIQh)" % nested_type) return obj.collate() if isinstance(obj, HoloMap): - display_warning.warning("Nesting %ss within a HoloMap makes it difficult " + display_warning.warning("Nesting {0}s within a {1} makes it difficult " "to access your data or control how it appears; " - "we recommend calling .collate() on the HoloMap " + "we recommend calling .collate() on the {1} " "in order to follow the recommended nesting " "structure shown in the Composing Data tutorial" - "(http://git.io/vtIQh)" % obj.type.__name__) + "(http://git.io/vtIQh)".format(obj.type.__name__, type(obj).__name__)) return obj.collate() elif isinstance(obj, (Layout, NdLayout)): try: @@ -70,6 +70,19 @@ def collate(obj): raise Exception(undisplayable_info(obj)) +def initialize_dynamic(obj): + """ + Initializes all DynamicMap objects contained by the object + """ + dmaps = obj.traverse(lambda x: x, specs=[DynamicMap]) + for dmap in dmaps: + if dmap.sampled: + # Skip initialization until plotting code + continue + if not len(dmap): + dmap[dmap._initial_key()] + + def undisplayable_info(obj, html=False): "Generate helpful message regarding an undisplayable object" From 002ded45fdb133ac6afa06291c6d4ed4e63a3901 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 4 Apr 2017 16:34:18 +0100 Subject: [PATCH 06/13] Small fix for dynamic collation closure --- holoviews/core/spaces.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index fdac02aa6b..57fba5f795 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -847,9 +847,10 @@ def collation_cb(*args, **kwargs): vstreams = streams.get(i, []) elif isinstance(streams, list): vstreams = streams[i] if i < len(streams) else [] - def collation_cb(collation_key=k, *args, **kwargs): - return self[args][collation_key] - callback = Callable(collation_cb, inputs=[self]) + def collation_cb(*args, **kwargs): + return self[args][kwargs['collation_key']] + callback = Callable(partial(collation_cb, collation_key=k), + inputs=[self]) vdmap = self.clone(callback=callback, shared_data=False, streams=vstreams) for stream in vstreams: From a2cfa4cd01ef583bf0398c9cf59ca3804571deb6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 5 Apr 2017 03:29:05 +0100 Subject: [PATCH 07/13] Moved stream_remapping specification onto Callable --- holoviews/core/spaces.py | 47 +++++++++++++++++++++++++++++++++----- holoviews/plotting/util.py | 2 ++ holoviews/streams.py | 36 +++++++++++++++++++---------- 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 57fba5f795..166918bca7 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -402,6 +402,13 @@ class Callable(param.Parameterized): returned value based on the arguments to the function and the state of all streams on its inputs, to avoid calling the function unnecessarily. + + A Callable may also specify a stream_mapping which allows + specifying which objects to attached linked streams to on + callbacks which return composite objects like (Nd)Layout and + GridSpace objects. The mapping should map between an integer index + or a type[.group][.label] specification and lists of streams + matching the object. """ callable_function = param.Callable(default=lambda x: x, doc=""" @@ -410,11 +417,12 @@ class Callable(param.Parameterized): inputs = param.List(default=[], doc=""" The list of inputs the callable function is wrapping.""") - def __init__(self, callable_function=None, **params): + def __init__(self, callable_function=None, stream_mapping={}, **params): if callable_function is not None: params['callable_function'] = callable_function super(Callable, self).__init__(**params) self._memoized = {} + self.stream_mapping = stream_mapping def __call__(self, *args, **kwargs): inputs = [i for i in self.inputs if isinstance(i, DynamicMap)] @@ -796,7 +804,7 @@ def dynamic_redim(obj): return Dynamic(redimmed, shared_data=True, operation=dynamic_redim) - def collate(self, streams=[]): + def collate(self): """ Collation allows collapsing DynamicMaps with invalid nesting hierarchies. This is particularly useful when defining @@ -842,21 +850,48 @@ def collation_cb(*args, **kwargs): # Expand Layout/NdLayout from ..util import Dynamic new_item = self.last.clone(shared_data=False) + + # Get stream mapping from callback + remapped_streams = [] + streams = self.callback.stream_mapping for i, (k, v) in enumerate(self.last.items()): - if isinstance(streams, dict): - vstreams = streams.get(i, []) - elif isinstance(streams, list): - vstreams = streams[i] if i < len(streams) else [] + vstreams = streams.get(i, []) + if not vstreams: + if isinstance(self.last, Layout): + for l in range(len(k)): + path = '.'.join(k[:l]) + if path in streams: + vstreams = streams[path] + break + else: + vstreams = streams.get(k, []) + remapped_streams += vstreams + + # Define collation callback def collation_cb(*args, **kwargs): return self[args][kwargs['collation_key']] callback = Callable(partial(collation_cb, collation_key=k), inputs=[self]) vdmap = self.clone(callback=callback, shared_data=False, streams=vstreams) + + # Remap source of streams for stream in vstreams: if stream.source is self: stream.source = vdmap new_item[k] = vdmap + + unmapped_streams = [repr(stream) for stream in self.streams + if (stream.source is self) and + (stream not in remapped_streams) + and stream.linked] + if unmapped_streams: + raise ValueError( + 'The following streams are set to be automatically ' + 'linked to a plot, but no stream_mapping specifying ' + 'which item in the (Nd)Layout to link it to was found:\n%s' + % ', '.join(unmapped_streams) + ) return new_item else: self.warning('DynamicMap does not need to be collated.') diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 45f3cacb5e..69794c6692 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -42,6 +42,8 @@ def collate(obj): "structure shown in the Composing Data tutorial" "(http://git.io/vtIQh)" % nested_type) return obj.collate() + if isinstance(obj, DynamicMap): + return obj.collate() if isinstance(obj, HoloMap): display_warning.warning("Nesting {0}s within a {1} makes it difficult " "to access your data or control how it appears; " diff --git a/holoviews/streams.py b/holoviews/streams.py index 5218b066f6..f5b7789a3a 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -20,8 +20,9 @@ class Stream(param.Parameterized): the parameter dictionary when the trigger classmethod is called. Depending on the plotting backend certain streams may interactively - subscribe to events and changes by the plotting backend. To disable - this behavior instantiate the Stream with linked=False. + subscribe to events and changes by the plotting backend. For this + purpose use the LinkedStream baseclass, which enables the linked + option by default. """ # Mapping from a source id to a list of streams @@ -60,7 +61,7 @@ def trigger(cls, streams): stream.deactivate() - def __init__(self, rename={}, source=None, subscribers=[], linked=True, **params): + def __init__(self, rename={}, source=None, subscribers=[], linked=False, **params): """ The rename argument allows multiple streams with similar event state to be used by remapping parameter names. @@ -222,7 +223,18 @@ def __str__(self): return repr(self) -class PositionX(Stream): +class LinkedStream(Stream): + """ + A LinkedStream indicates is automatically linked to plot interactions + on a backend via a Renderer. Not all backends may support dynamically + supplying stream data. + """ + + def __init__(self, linked=True, **params): + super(LinkedStream, self).__init__(linked=linked, **params) + + +class PositionX(LinkedStream): """ A position along the x-axis in data coordinates. @@ -234,7 +246,7 @@ class PositionX(Stream): Position along the x-axis in data coordinates""", constant=True) -class PositionY(Stream): +class PositionY(LinkedStream): """ A position along the y-axis in data coordinates. @@ -246,7 +258,7 @@ class PositionY(Stream): Position along the y-axis in data coordinates""", constant=True) -class PositionXY(Stream): +class PositionXY(LinkedStream): """ A position along the x- and y-axes in data coordinates. @@ -287,7 +299,7 @@ class MouseLeave(PositionXY): """ -class PlotSize(Stream): +class PlotSize(LinkedStream): """ Returns the dimensions of a plot once it has been displayed. """ @@ -304,7 +316,7 @@ def transform(self): 'height': int(self.height * self.scale)} -class RangeXY(Stream): +class RangeXY(LinkedStream): """ Axis ranges along x- and y-axis in data coordinates. """ @@ -316,7 +328,7 @@ class RangeXY(Stream): Range of the y-axis of a plot in data coordinates""") -class RangeX(Stream): +class RangeX(LinkedStream): """ Axis range along x-axis in data coordinates. """ @@ -325,7 +337,7 @@ class RangeX(Stream): Range of the x-axis of a plot in data coordinates""") -class RangeY(Stream): +class RangeY(LinkedStream): """ Axis range along y-axis in data coordinates. """ @@ -334,7 +346,7 @@ class RangeY(Stream): Range of the y-axis of a plot in data coordinates""") -class Bounds(Stream): +class Bounds(LinkedStream): """ A stream representing the bounds of a box selection as an tuple of the left, bottom, right and top coordinates. @@ -345,7 +357,7 @@ class Bounds(Stream): Bounds defined as (left, bottom, top, right) tuple.""") -class Selection1D(Stream): +class Selection1D(LinkedStream): """ A stream representing a 1D selection of objects by their index. """ From cde0972384fed5d6f724fd378d74447ee83e4b52 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 5 Apr 2017 12:09:34 +0100 Subject: [PATCH 08/13] Updated DynamicMap.collate docstring --- holoviews/core/spaces.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 166918bca7..2343a22a9f 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -808,13 +808,13 @@ def collate(self): """ Collation allows collapsing DynamicMaps with invalid nesting hierarchies. This is particularly useful when defining - DynamicMaps returning an (Nd)Layout. Collating will split the - DynamicMap into an (Nd)Layout of individual DynamicMaps. Note - that the Layout should be of consistent length and types for - this to work correctly. In order to attach a stream as a source - for a particular object in the Layout you may supply either - a dictionary or list of lists of streams corresponding to each - Element in the Layout. + DynamicMaps returning an (Nd)Layout or GridSpace + type. Collating will split the DynamicMap into of individual + DynamicMaps. Note that the composite object has to be of + consistent length and types for this to work + correctly. Associating streams with specific viewables in the + returned container declare a stream_mapping on the DynamicMap + Callable during instantiation. """ # Initialize if self.last is not None: From fab70ee3d106e533ac1e15f340c073f9f7e7e08b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 5 Apr 2017 14:46:08 +0100 Subject: [PATCH 09/13] Added validation for ambiguous stream mapping --- holoviews/core/spaces.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 2343a22a9f..e89c95549b 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -854,7 +854,7 @@ def collation_cb(*args, **kwargs): # Get stream mapping from callback remapped_streams = [] streams = self.callback.stream_mapping - for i, (k, v) in enumerate(self.last.items()): + for i, (k, v) in enumerate(self.last.data.items()): vstreams = streams.get(i, []) if not vstreams: if isinstance(self.last, Layout): @@ -865,6 +865,11 @@ def collation_cb(*args, **kwargs): break else: vstreams = streams.get(k, []) + if any(s in remapped_streams for s in vstreams): + raise ValueError( + "The stream_mapping supplied on the Callable " + "is ambiguous please supply more specific Layout " + "path specs.") remapped_streams += vstreams # Define collation callback From 6f4d9562a42a57f117a757c0add7c0a070334e46 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 5 Apr 2017 14:56:59 +0100 Subject: [PATCH 10/13] Added tests for dynamic collation --- tests/testdynamic.py | 115 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/tests/testdynamic.py b/tests/testdynamic.py index 2a2b33f4f2..be065bb200 100644 --- a/tests/testdynamic.py +++ b/tests/testdynamic.py @@ -1,5 +1,7 @@ import numpy as np -from holoviews import Dimension, DynamicMap, Image, HoloMap, Scatter, Curve +from holoviews import Dimension, NdLayout, GridSpace +from holoviews.core.spaces import DynamicMap, HoloMap, Callable +from holoviews.element import Image, Scatter, Curve, Text from holoviews.streams import PositionXY from holoviews.util import Dynamic from holoviews.element.comparison import ComparisonTestCase @@ -201,3 +203,114 @@ def fn(x, y): with self.assertRaisesRegexp(KeyError, regexp): dmap.event(x=1, y=2) + + +class DynamicCollate(ComparisonTestCase): + + def test_dynamic_collate_layout(self): + def callback(): + return Image(np.array([[0, 1], [2, 3]])) + Text(0, 0, 'Test') + dmap = DynamicMap(callback, kdims=[]) + layout = dmap.collate() + self.assertEqual(list(layout.keys()), [('Image', 'I'), ('Text', 'I')]) + self.assertEqual(layout.Image.I[()], Image(np.array([[0, 1], [2, 3]]))) + + def test_dynamic_collate_layout_raise_no_remapping_error(self): + def callback(x, y): + return Image(np.array([[0, 1], [2, 3]])) + Text(0, 0, 'Test') + stream = PositionXY() + cb_callable = Callable(callback) + dmap = DynamicMap(cb_callable, kdims=[], streams=[stream]) + with self.assertRaisesRegexp(ValueError, 'The following streams are set to be automatically linked'): + layout = dmap.collate() + + def test_dynamic_collate_layout_raise_ambiguous_remapping_error(self): + def callback(x, y): + return Image(np.array([[0, 1], [2, 3]])) + Image(np.array([[0, 1], [2, 3]])) + stream = PositionXY() + cb_callable = Callable(callback, stream_mapping={'Image': [stream]}) + dmap = DynamicMap(cb_callable, kdims=[], streams=[stream]) + with self.assertRaisesRegexp(ValueError, 'The stream_mapping supplied on the Callable is ambiguous'): + layout = dmap.collate() + + def test_dynamic_collate_layout_with_integer_stream_mapping(self): + def callback(x, y): + return Image(np.array([[0, 1], [2, 3]])) + Text(0, 0, 'Test') + stream = PositionXY() + cb_callable = Callable(callback, stream_mapping={0: [stream]}) + dmap = DynamicMap(cb_callable, kdims=[], streams=[stream]) + layout = dmap.collate() + self.assertEqual(list(layout.keys()), [('Image', 'I'), ('Text', 'I')]) + self.assertIs(stream.source, layout.Image.I) + + def test_dynamic_collate_layout_with_spec_stream_mapping(self): + def callback(x, y): + return Image(np.array([[0, 1], [2, 3]])) + Text(0, 0, 'Test') + stream = PositionXY() + cb_callable = Callable(callback, stream_mapping={'Image': [stream]}) + dmap = DynamicMap(cb_callable, kdims=[], streams=[stream]) + layout = dmap.collate() + self.assertEqual(list(layout.keys()), [('Image', 'I'), ('Text', 'I')]) + self.assertIs(stream.source, layout.Image.I) + + def test_dynamic_collate_ndlayout(self): + def callback(): + return NdLayout({i: Image(np.array([[i, 1], [2, 3]])) for i in range(1, 3)}) + dmap = DynamicMap(callback, kdims=[]) + layout = dmap.collate() + self.assertEqual(list(layout.keys()), [1, 2]) + self.assertEqual(layout[1][()], Image(np.array([[1, 1], [2, 3]]))) + + def test_dynamic_collate_ndlayout_with_integer_stream_mapping(self): + def callback(x, y): + return NdLayout({i: Image(np.array([[i, 1], [2, 3]])) for i in range(1, 3)}) + stream = PositionXY() + cb_callable = Callable(callback, stream_mapping={0: [stream]}) + dmap = DynamicMap(cb_callable, kdims=[], streams=[stream]) + layout = dmap.collate() + self.assertEqual(list(layout.keys()), [1, 2]) + self.assertIs(stream.source, layout[1]) + + def test_dynamic_collate_ndlayout_with_key_stream_mapping(self): + def callback(x, y): + return NdLayout({i: Image(np.array([[i, 1], [2, 3]])) for i in range(1, 3)}) + stream = PositionXY() + cb_callable = Callable(callback, stream_mapping={(1,): [stream]}) + dmap = DynamicMap(cb_callable, kdims=[], streams=[stream]) + layout = dmap.collate() + self.assertEqual(list(layout.keys()), [1, 2]) + self.assertIs(stream.source, layout[1]) + + def test_dynamic_collate_grid(self): + def callback(): + return GridSpace({(i, j): Image(np.array([[i, j], [2, 3]])) + for i in range(1, 3) for j in range(1, 3)}) + dmap = DynamicMap(callback, kdims=[]) + grid = dmap.collate() + self.assertEqual(list(grid.keys()), [(i, j) for i in range(1, 3) + for j in range(1, 3)]) + self.assertEqual(grid[(0, 1)][()], Image(np.array([[1, 1], [2, 3]]))) + + def test_dynamic_collate_grid_with_integer_stream_mapping(self): + def callback(): + return GridSpace({(i, j): Image(np.array([[i, j], [2, 3]])) + for i in range(1, 3) for j in range(1, 3)}) + stream = PositionXY() + cb_callable = Callable(callback, stream_mapping={1: [stream]}) + dmap = DynamicMap(cb_callable, kdims=[]) + grid = dmap.collate() + self.assertEqual(list(grid.keys()), [(i, j) for i in range(1, 3) + for j in range(1, 3)]) + self.assertEqual(stream.source, grid[(1, 2)]) + + def test_dynamic_collate_grid_with_key_stream_mapping(self): + def callback(): + return GridSpace({(i, j): Image(np.array([[i, j], [2, 3]])) + for i in range(1, 3) for j in range(1, 3)}) + stream = PositionXY() + cb_callable = Callable(callback, stream_mapping={(1, 2): [stream]}) + dmap = DynamicMap(cb_callable, kdims=[]) + grid = dmap.collate() + self.assertEqual(list(grid.keys()), [(i, j) for i in range(1, 3) + for j in range(1, 3)]) + self.assertEqual(stream.source, grid[(1, 2)]) From 91e881ed2c157535f70b8706fed3b2cb5208a550 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 5 Apr 2017 21:40:26 +0100 Subject: [PATCH 11/13] Cleaned up Callable renaming callable_function --- holoviews/core/overlay.py | 3 +-- holoviews/core/spaces.py | 41 ++++++++++++++++++++++----------------- holoviews/util.py | 6 +++--- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/holoviews/core/overlay.py b/holoviews/core/overlay.py index 5c86143a24..344e03b968 100644 --- a/holoviews/core/overlay.py +++ b/holoviews/core/overlay.py @@ -28,8 +28,7 @@ def __mul__(self, other): def dynamic_mul(*args, **kwargs): element = other[args] return self * element - callback = Callable(callable_function=dynamic_mul, - inputs=[self, other]) + callback = Callable(dynamic_mul, 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 e89c95549b..d18205f144 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -138,7 +138,7 @@ def dynamic_mul(*key, **kwargs): except KeyError: pass return Overlay(layers) - callback = Callable(callable_function=dynamic_mul, inputs=[self, other]) + callback = Callable(dynamic_mul, inputs=[self, other]) if map_obj: return map_obj.clone(callback=callback, shared_data=False, kdims=dimensions, streams=[]) @@ -205,8 +205,7 @@ def __mul__(self, other): def dynamic_mul(*args, **kwargs): element = self[args] return element * other - callback = Callable(callable_function=dynamic_mul, - inputs=[self, other]) + callback = Callable(dynamic_mul, inputs=[self, other]) return self.clone(shared_data=False, callback=callback, streams=[]) items = [(k, v * other) for (k, v) in self.data.items()] @@ -403,26 +402,33 @@ class Callable(param.Parameterized): state of all streams on its inputs, to avoid calling the function unnecessarily. - A Callable may also specify a stream_mapping which allows - specifying which objects to attached linked streams to on - callbacks which return composite objects like (Nd)Layout and - GridSpace objects. The mapping should map between an integer index - or a type[.group][.label] specification and lists of streams - matching the object. + A Callable may also specify a stream_mapping which specifies the + objects that are associated with interactive (i.e linked) streams + when composite objects such as Layouts are returned from the + callback. This is required for building interactive, linked + visualizations (for the backends that support them) when returning + Layouts, NdLayouts or GridSpace objects. + + The mapping should map from an appropriate key to a list of + streams associated with the selected object. The appropriate key + may be a type[.group][.label] specification for Layouts, an + integer index or a suitable NdLayout/GridSpace key. For more + information see the DynamicMap tutorial at holoviews.org. """ - callable_function = param.Callable(default=lambda x: x, doc=""" + callable = param.Callable(default=None, doc=""" The callable function being wrapped.""") inputs = param.List(default=[], doc=""" The list of inputs the callable function is wrapping.""") - def __init__(self, callable_function=None, stream_mapping={}, **params): - if callable_function is not None: - params['callable_function'] = callable_function - super(Callable, self).__init__(**params) + stream_mapping = param.Dict(default={}, doc=""" + Defines how streams should be mapped to objects returned by + the Callable, e.g. when it returns a Layout.""") + + def __init__(self, callable, **params): + super(Callable, self).__init__(callable=callable, **params) self._memoized = {} - self.stream_mapping = stream_mapping def __call__(self, *args, **kwargs): inputs = [i for i in self.inputs if isinstance(i, DynamicMap)] @@ -430,11 +436,10 @@ def __call__(self, *args, **kwargs): values = tuple(tuple(sorted(s.contents.items())) for s in streams) key = args + tuple(sorted(kwargs.items())) + values - hashed_key = util.deephash(key) ret = self._memoized.get(hashed_key, None) if hashed_key and ret is None: - ret = self.callable_function(*args, **kwargs) + ret = self.callable(*args, **kwargs) self._memoized = {hashed_key : ret} return ret @@ -492,7 +497,7 @@ class DynamicMap(HoloMap): def __init__(self, callback, initial_items=None, **params): if not isinstance(callback, Callable): - callback = Callable(callable_function=callback) + callback = Callable(callback) super(DynamicMap, self).__init__(initial_items, callback=callback, **params) # Set source to self if not already specified diff --git a/holoviews/util.py b/holoviews/util.py index aa282bba16..32dd8ad229 100644 --- a/holoviews/util.py +++ b/holoviews/util.py @@ -83,10 +83,10 @@ def dynamic_operation(*key, **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, - inputs=[map_obj], operation=self.p.operation) + return OperationCallable(dynamic_operation, inputs=[map_obj], + operation=self.p.operation) else: - return Callable(callable_function=dynamic_operation, inputs=[map_obj]) + return Callable(dynamic_operation, inputs=[map_obj]) def _make_dynamic(self, hmap, dynamic_fn): From 5196ddc9c3b1b8288a1ed043ee5c554d14e0e0e6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 5 Apr 2017 21:49:39 +0100 Subject: [PATCH 12/13] Simplified DynamicMap.collate --- holoviews/core/spaces.py | 141 ++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 85 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index d18205f144..31564b0b83 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -811,101 +811,72 @@ def dynamic_redim(obj): def collate(self): """ - Collation allows collapsing DynamicMaps with invalid nesting + Collation allows reorganizing DynamicMaps with invalid nesting hierarchies. This is particularly useful when defining DynamicMaps returning an (Nd)Layout or GridSpace - type. Collating will split the DynamicMap into of individual - DynamicMaps. Note that the composite object has to be of - consistent length and types for this to work - correctly. Associating streams with specific viewables in the - returned container declare a stream_mapping on the DynamicMap - Callable during instantiation. + types. Collating will split the DynamicMap into individual + DynamicMaps for each item in the container. Note that the + composite object has to be of consistent length and types for + this to work correctly. """ # Initialize if self.last is not None: pass else: - self[self._initial_key()] - - if isinstance(self.last, HoloMap): - # Get nested kdims and streams - streams = list(self.streams) - if isinstance(self.last, DynamicMap): - dimensions = [d(values=self.last.dimension_values(d.name)) - for d in self.last.kdims] - streams += self.last.streams - stream_kwargs = set() - for stream in streams: - contents = set(stream.contents()) - if stream_kwargs & contents: - raise KeyError('Cannot collate DynamicMaps with clashing ' - 'stream parameters.') - else: - dimensions = self.last.kdims - kdims = self.kdims+dimensions + self.clone()[self._initial_key()] - # Define callback - def collation_cb(*args, **kwargs): - return self[args[:self.ndims]][args[self.ndims:]] - callback = Callable(collation_cb, inputs=[self]) + if not isinstance(self.last, (Layout, NdLayout, GridSpace)): + return self - return self.clone(shared_data=False, callback=callback, - kdims=kdims, streams=streams) - elif isinstance(self.last, (Layout, NdLayout, GridSpace)): - # Expand Layout/NdLayout - from ..util import Dynamic - new_item = self.last.clone(shared_data=False) - - # Get stream mapping from callback - remapped_streams = [] - streams = self.callback.stream_mapping - for i, (k, v) in enumerate(self.last.data.items()): - vstreams = streams.get(i, []) - if not vstreams: - if isinstance(self.last, Layout): - for l in range(len(k)): - path = '.'.join(k[:l]) - if path in streams: - vstreams = streams[path] - break - else: - vstreams = streams.get(k, []) - if any(s in remapped_streams for s in vstreams): - raise ValueError( - "The stream_mapping supplied on the Callable " - "is ambiguous please supply more specific Layout " - "path specs.") - remapped_streams += vstreams - - # Define collation callback - def collation_cb(*args, **kwargs): - return self[args][kwargs['collation_key']] - callback = Callable(partial(collation_cb, collation_key=k), - inputs=[self]) - vdmap = self.clone(callback=callback, shared_data=False, - streams=vstreams) - - # Remap source of streams - for stream in vstreams: - if stream.source is self: - stream.source = vdmap - new_item[k] = vdmap - - unmapped_streams = [repr(stream) for stream in self.streams - if (stream.source is self) and - (stream not in remapped_streams) - and stream.linked] - if unmapped_streams: + container = self.last.clone(shared_data=False) + + # Get stream mapping from callback + remapped_streams = [] + streams = self.callback.stream_mapping + for i, (k, v) in enumerate(self.last.data.items()): + vstreams = streams.get(i, []) + if not vstreams: + if isinstance(self.last, Layout): + for l in range(len(k)): + path = '.'.join(k[:l]) + if path in streams: + vstreams = streams[path] + break + else: + vstreams = streams.get(k, []) + if any(s in remapped_streams for s in vstreams): raise ValueError( - 'The following streams are set to be automatically ' - 'linked to a plot, but no stream_mapping specifying ' - 'which item in the (Nd)Layout to link it to was found:\n%s' - % ', '.join(unmapped_streams) - ) - return new_item - else: - self.warning('DynamicMap does not need to be collated.') - return dmap + "The stream_mapping supplied on the Callable " + "is ambiguous please supply more specific Layout " + "path specs.") + remapped_streams += vstreams + + # Define collation callback + def collation_cb(*args, **kwargs): + return self[args][kwargs['selection_key']] + callback = Callable(partial(collation_cb, selection_key=k), + inputs=[self]) + vdmap = self.clone(callback=callback, shared_data=False, + streams=vstreams) + + # Remap source of streams + for stream in vstreams: + if stream.source is self: + stream.source = vdmap + container[k] = vdmap + + unmapped_streams = [repr(stream) for stream in self.streams + if (stream.source is self) and + (stream not in remapped_streams) + and stream.linked] + if unmapped_streams: + raise ValueError( + 'The following streams are set to be automatically ' + 'linked to a plot, but no stream_mapping specifying ' + 'which item in the (Nd)Layout to link it to was found:\n%s' + % ', '.join(unmapped_streams) + ) + return container def groupby(self, dimensions=None, container_type=None, group_type=None, **kwargs): From 00bec26b859c3410957d1bf2cbb56ac7b7b4d995 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 5 Apr 2017 21:57:15 +0100 Subject: [PATCH 13/13] Fixed initialization bug on dynamic collate --- holoviews/core/spaces.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 31564b0b83..d2cf2a5392 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -821,22 +821,23 @@ def collate(self): """ # Initialize if self.last is not None: - pass + initialized = self else: - self.clone()[self._initial_key()] + initialized = self.clone() + initialized[initialized._initial_key()] - if not isinstance(self.last, (Layout, NdLayout, GridSpace)): + if not isinstance(initialized.last, (Layout, NdLayout, GridSpace)): return self - container = self.last.clone(shared_data=False) + container = initialized.last.clone(shared_data=False) # Get stream mapping from callback remapped_streams = [] streams = self.callback.stream_mapping - for i, (k, v) in enumerate(self.last.data.items()): + for i, (k, v) in enumerate(initialized.last.data.items()): vstreams = streams.get(i, []) if not vstreams: - if isinstance(self.last, Layout): + if isinstance(initialized.last, Layout): for l in range(len(k)): path = '.'.join(k[:l]) if path in streams: