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: