Skip to content

Commit

Permalink
Merge pull request #1243 from ioam/dynamic_collation
Browse files Browse the repository at this point in the history
Dynamic collation
  • Loading branch information
jlstevens authored Apr 5, 2017
2 parents 78c61e2 + 00bec26 commit ea22ec8
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 40 deletions.
3 changes: 1 addition & 2 deletions holoviews/core/overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
110 changes: 101 additions & 9 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[])
Expand Down Expand Up @@ -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()]
Expand Down Expand Up @@ -402,16 +401,33 @@ 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 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, **params):
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 = {}

def __call__(self, *args, **kwargs):
Expand All @@ -420,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

Expand Down Expand Up @@ -482,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
Expand All @@ -496,11 +511,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)


Expand Down Expand Up @@ -788,6 +809,77 @@ def dynamic_redim(obj):
return Dynamic(redimmed, shared_data=True, operation=dynamic_redim)


def collate(self):
"""
Collation allows reorganizing DynamicMaps with invalid nesting
hierarchies. This is particularly useful when defining
DynamicMaps returning an (Nd)Layout or GridSpace
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:
initialized = self
else:
initialized = self.clone()
initialized[initialized._initial_key()]

if not isinstance(initialized.last, (Layout, NdLayout, GridSpace)):
return self

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(initialized.last.data.items()):
vstreams = streams.get(i, [])
if not vstreams:
if isinstance(initialized.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['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):
"""
Implements a dynamic version of a groupby, which will
Expand Down
14 changes: 5 additions & 9 deletions holoviews/plotting/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
21 changes: 18 additions & 3 deletions holoviews/plotting/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,15 @@ 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 %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:
Expand All @@ -70,6 +72,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"

Expand Down
40 changes: 27 additions & 13 deletions holoviews/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -149,7 +150,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)

Expand Down Expand Up @@ -220,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.
Expand All @@ -232,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.
Expand All @@ -244,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.
Expand Down Expand Up @@ -285,7 +299,7 @@ class MouseLeave(PositionXY):
"""


class PlotSize(Stream):
class PlotSize(LinkedStream):
"""
Returns the dimensions of a plot once it has been displayed.
"""
Expand All @@ -302,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.
"""
Expand All @@ -314,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.
"""
Expand All @@ -323,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.
"""
Expand All @@ -332,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.
Expand All @@ -343,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.
"""
Expand Down
6 changes: 3 additions & 3 deletions holoviews/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading

0 comments on commit ea22ec8

Please sign in to comment.