Skip to content

Commit

Permalink
Merge pull request #1305 from ioam/deprecate_sampled_mode
Browse files Browse the repository at this point in the history
Improve DynamicMap usability and deprecate sampled mode
  • Loading branch information
philippjfr committed Apr 19, 2017
2 parents eaf34f5 + 6aee813 commit 3d05c84
Show file tree
Hide file tree
Showing 15 changed files with 351 additions and 185 deletions.
69 changes: 52 additions & 17 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .ndmapping import UniformNdMapping, NdMapping, item_check
from .overlay import Overlay, CompositeOverlay, NdOverlay, Overlayable
from .options import Store, StoreOptions
from ..streams import Stream

class HoloMap(UniformNdMapping, Overlayable):
"""
Expand Down Expand Up @@ -555,6 +556,10 @@ class DynamicMap(HoloMap):
# Declare that callback is a positional parameter (used in clone)
__pos_params = ['callback']

kdims = param.List(default=[], constant=True, doc="""
The key dimensions of a DynamicMap map to the arguments of the
callback. This mapping can be by position or by name.""")

callback = param.ClassSelector(class_=Callable, doc="""
The callable used to generate the elements. The arguments to the
callable includes any number of declared key dimensions as well
Expand All @@ -574,17 +579,21 @@ class DynamicMap(HoloMap):
cache where the least recently used item is overwritten once
the cache is full.""")

sampled = param.Boolean(default=False, doc="""
Allows defining a DynamicMap without defining the dimension
bounds or values. The DynamicMap may then be explicitly sampled
via getitem or the sampling is determined during plotting by a
HoloMap with fixed sampling.
""")

def __init__(self, callback, initial_items=None, **params):
if not isinstance(callback, Callable):
callback = Callable(callback)

if 'sampled' in params:
self.warning('DynamicMap sampled parameter is deprecated '
'and no longer neededs to be specified.')
del params['sampled']

super(DynamicMap, self).__init__(initial_items, callback=callback, **params)
invalid = [s for s in self.streams if not isinstance(s, Stream)]
if invalid:
msg = ('The supplied streams list contains objects that '
'are not Stream instances: {objs}')
raise TypeError(msg.format(objs = ', '.join('%r' % el for el in invalid)))

self._posarg_keys = util.validate_dynamic_argspec(self.callback.argspec,
self.kdims,
Expand All @@ -595,23 +604,48 @@ def __init__(self, callback, initial_items=None, **params):
stream.source = self
self.redim = redim(self, mode='dynamic')

@property
def unbounded(self):
"""
Returns a list of key dimensions that are unbounded, excluding
stream parameters. If any of theses key dimensions are
unbounded, the DynamicMap as a whole is also unbounded.
"""
unbounded_dims = []
# Dimensioned streams do not need to be bounded
stream_params = set(util.stream_parameters(self.streams))
for kdim in self.kdims:
if str(kdim) in stream_params:
continue
if kdim.values:
continue
if None in kdim.range:
unbounded_dims.append(str(kdim))
return unbounded_dims

def _initial_key(self):
"""
Construct an initial key for based on the lower range bounds or
values on the key dimensions.
"""
key = []
undefined = []
stream_params = set(util.stream_parameters(self.streams))
for kdim in self.kdims:
if kdim.values:
if str(kdim) in stream_params:
key.append(None)
elif kdim.values:
key.append(kdim.values[0])
elif kdim.range:
elif kdim.range[0] is not None:
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))
msg = ('Dimension(s) {undefined_dims} do not specify range or values needed '
'to generate initial key')
undefined_dims = ', '.join(['%r' % str(dim) for dim in undefined])
raise KeyError(msg.format(undefined_dims=undefined_dims))

return tuple(key)


Expand Down Expand Up @@ -639,20 +673,21 @@ def event(self, trigger=True, **kwargs):
This method allows any of the available stream parameters
(renamed as appropriate) to be updated in an event.
"""
if self.streams == []: return
stream_params = set(util.stream_parameters(self.streams))
for k in stream_params - set(kwargs.keys()):
raise KeyError('Key %r does not correspond to any stream parameter')
invalid = [k for k in kwargs.keys() if k not in stream_params]
if invalid:
msg = 'Key(s) {invalid} do not correspond to stream parameters'
raise KeyError(msg.format(invalid = ', '.join('%r' % i for i in invalid)))

updated_streams = []
for stream in self.streams:
applicable_kws = {k:v for k,v in kwargs.items()
if k in set(stream.contents.keys())}
rkwargs = util.rename_stream_kwargs(stream, applicable_kws, reverse=True)
stream.update(**dict(rkwargs, trigger=False))
updated_streams.append(stream)

if updated_streams and trigger:
updated_streams[0].trigger(updated_streams)
if trigger:
Stream.trigger(self.streams)


def _style(self, retval):
Expand Down
4 changes: 4 additions & 0 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ def validate_dynamic_argspec(argspec, kdims, streams):
raise KeyError('Callable missing keywords to accept %s stream parameters'
% ', '.join(unassigned_streams))


if len(posargs) > len(kdims) + len(stream_params):
raise KeyError('Callable accepts more positional arguments than '
'there are kdims and stream parameters')
if kdims == []: # Can be no posargs, stream kwargs already validated
return []
if set(kdims) == set(posargs): # Posargs match exactly, can all be passed as kwargs
Expand Down
11 changes: 9 additions & 2 deletions holoviews/ipython/display_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def wrapped(element):
return html
except SkipRendering as e:
if e.warn:
sys.stderr.write("Rendering process skipped: %s" % str(e))
sys.stderr.write(str(e))
return None
except AbbreviatedException as e:

Expand Down Expand Up @@ -179,8 +179,15 @@ def element_display(element, max_frames):
@display_hook
def map_display(vmap, max_frames):
if not isinstance(vmap, (HoloMap, DynamicMap)): return None
if len(vmap) == 0 and (not isinstance(vmap, DynamicMap) or vmap.sampled):
if len(vmap) == 0 and isinstance(vmap, DynamicMap) and vmap.unbounded:
dims = ', '.join('%r' % dim for dim in vmap.unbounded)
msg = ('DynamicMap cannot be displayed without explicit indexing '
'as {dims} dimension(s) are unbounded. '
'\nSet dimensions bounds with the DynamicMap redim.range '
'or redim.values methods.')
sys.stderr.write(msg.format(dims=dims))
return None

elif len(vmap) > max_frames:
max_frame_warning(max_frames)
return None
Expand Down
30 changes: 18 additions & 12 deletions holoviews/plotting/bokeh/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from bokeh.models import CustomJS

from ...core import OrderedDict
from ...streams import (Stream, PositionXY, RangeXY, Selection1D, RangeX,
RangeY, PositionX, PositionY, Bounds, Tap,
from ...streams import (Stream, PointerXY, RangeXY, Selection1D, RangeX,
RangeY, PointerX, PointerY, Bounds, Tap,
DoubleTap, MouseEnter, MouseLeave, PlotSize)
from ...streams import PositionX, PositionY, PositionXY # Deprecated: remove in 2.0
from ..comms import JupyterCommJS
from .util import bokeh_version

Expand Down Expand Up @@ -515,7 +516,7 @@ def initialize(self):



class PositionXYCallback(Callback):
class PointerXYCallback(Callback):
"""
Returns the mouse x/y-position on mousemove event.
"""
Expand All @@ -525,39 +526,39 @@ class PositionXYCallback(Callback):
on_events = ['mousemove']


class PositionXCallback(PositionXYCallback):
class PointerXCallback(PointerXYCallback):
"""
Returns the mouse x-position on mousemove event.
"""

attributes = {'x': 'cb_obj.x'}


class PositionYCallback(PositionXYCallback):
class PointerYCallback(PointerXYCallback):
"""
Returns the mouse x/y-position on mousemove event.
"""

attributes = {'y': 'cb_obj.y'}


class TapCallback(PositionXYCallback):
class TapCallback(PointerXYCallback):
"""
Returns the mouse x/y-position on tap event.
"""

on_events = ['tap']


class DoubleTapCallback(PositionXYCallback):
class DoubleTapCallback(PointerXYCallback):
"""
Returns the mouse x/y-position on doubletap event.
"""

on_events = ['doubletap']


class MouseEnterCallback(PositionXYCallback):
class MouseEnterCallback(PointerXYCallback):
"""
Returns the mouse x/y-position on mouseenter event, i.e. when
mouse enters the plot canvas.
Expand All @@ -566,7 +567,7 @@ class MouseEnterCallback(PositionXYCallback):
on_events = ['mouseenter']


class MouseLeaveCallback(PositionXYCallback):
class MouseLeaveCallback(PointerXYCallback):
"""
Returns the mouse x/y-position on mouseleave event, i.e. when
mouse leaves the plot canvas.
Expand Down Expand Up @@ -676,9 +677,9 @@ def _process_msg(self, msg):

callbacks = Stream._callbacks['bokeh']

callbacks[PositionXY] = PositionXYCallback
callbacks[PositionX] = PositionXCallback
callbacks[PositionY] = PositionYCallback
callbacks[PointerXY] = PointerXYCallback
callbacks[PointerX] = PointerXCallback
callbacks[PointerY] = PointerYCallback
callbacks[Tap] = TapCallback
callbacks[DoubleTap] = DoubleTapCallback
callbacks[MouseEnter] = MouseEnterCallback
Expand All @@ -689,3 +690,8 @@ def _process_msg(self, msg):
callbacks[Bounds] = BoundsCallback
callbacks[Selection1D] = Selection1DCallback
callbacks[PlotSize] = PlotSizeCallback

# Aliases for deprecated streams
callbacks[PositionXY] = PointerXYCallback
callbacks[PositionX] = PointerXCallback
callbacks[PositionY] = PointerYCallback
2 changes: 1 addition & 1 deletion holoviews/plotting/bokeh/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ...element import Histogram
from ..plot import (DimensionedPlot, GenericCompositePlot, GenericLayoutPlot,
GenericElementPlot)
from ..util import get_dynamic_mode, initialize_sampled
from ..util import get_dynamic_mode
from .renderer import BokehRenderer
from .util import (bokeh_version, layout_padding, pad_plots,
filter_toolboxes, make_axis, update_shared_sources)
Expand Down
2 changes: 1 addition & 1 deletion holoviews/plotting/mpl/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from ...core.util import int_to_roman, int_to_alpha, basestring
from ...core import traversal
from ..plot import DimensionedPlot, GenericLayoutPlot, GenericCompositePlot
from ..util import get_dynamic_mode, initialize_sampled
from ..util import get_dynamic_mode
from .util import compute_ratios, fix_aspect


Expand Down
10 changes: 5 additions & 5 deletions holoviews/plotting/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from ..core.spaces import HoloMap, DynamicMap
from ..core.util import stream_parameters
from ..element import Table
from .util import (get_dynamic_mode, initialize_sampled, dim_axis_label,
from .util import (get_dynamic_mode, initialize_unbounded, dim_axis_label,
attach_streams, traverse_setter, get_nested_streams,
compute_overlayable_zorders)

Expand Down Expand Up @@ -590,7 +590,7 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None,
defaults=False)
plot_opts.update(**{k: v[0] for k, v in inherited.items()})

dynamic = isinstance(element, DynamicMap) and not element.sampled
dynamic = isinstance(element, DynamicMap) and not element.unbounded
super(GenericElementPlot, self).__init__(keys=keys, dimensions=dimensions,
dynamic=dynamic,
**dict(params, **plot_opts))
Expand Down Expand Up @@ -966,9 +966,9 @@ def __init__(self, layout, keys=None, dimensions=None, **params):
if top_level:
dimensions, keys = traversal.unique_dimkeys(layout)

dynamic, sampled = get_dynamic_mode(layout)
if sampled:
initialize_sampled(layout, dimensions, keys[0])
dynamic, unbounded = get_dynamic_mode(layout)
if unbounded:
initialize_unbounded(layout, dimensions, keys[0])
self.layout = layout
super(GenericCompositePlot, self).__init__(keys=keys,
dynamic=dynamic,
Expand Down
23 changes: 12 additions & 11 deletions holoviews/plotting/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ..core import (HoloMap, DynamicMap, CompositeOverlay, Layout,
Overlay, GridSpace, NdLayout, Store, Dataset)
from ..core.spaces import get_nested_streams, Callable
from ..core.options import SkipRendering
from ..core.util import (match_spec, is_number, wrap_tuple, basestring,
get_overlay_spec, unique_iterator, unique_iterator)
from ..streams import LinkedStream
Expand Down Expand Up @@ -167,7 +168,7 @@ def initialize_dynamic(obj):
"""
dmaps = obj.traverse(lambda x: x, specs=[DynamicMap])
for dmap in dmaps:
if dmap.sampled:
if dmap.unbounded:
# Skip initialization until plotting code
continue
if not len(dmap):
Expand Down Expand Up @@ -259,14 +260,14 @@ def within_range(range1, range2):
(range1[1] is None or range2[1] is None or range1[1] <= range2[1]))


def validate_sampled_mode(holomaps, dynmaps):
def validate_unbounded_mode(holomaps, dynmaps):
composite = HoloMap(enumerate(holomaps), kdims=['testing_kdim'])
holomap_kdims = set(unique_iterator([kd.name for dm in holomaps for kd in dm.kdims]))
hmranges = {d: composite.range(d) for d in holomap_kdims}
if any(not set(d.name for d in dm.kdims) <= holomap_kdims
for dm in dynmaps):
raise Exception('In sampled mode DynamicMap key dimensions must be a '
'subset of dimensions of the HoloMap(s) defining the sampling.')
raise Exception('DynamicMap that are unbounded must have key dimensions that are a '
'subset of dimensions of the HoloMap(s) defining the keys.')
elif not all(within_range(hmrange, dm.range(d)) for dm in dynmaps
for d, hmrange in hmranges.items() if d in dm.kdims):
raise Exception('HoloMap(s) have keys outside the ranges specified on '
Expand All @@ -277,18 +278,18 @@ def get_dynamic_mode(composite):
"Returns the common mode of the dynamic maps in given composite object"
dynmaps = composite.traverse(lambda x: x, [DynamicMap])
holomaps = composite.traverse(lambda x: x, ['HoloMap'])
dynamic_sampled = any(m.sampled for m in dynmaps)
dynamic_unbounded = any(m.unbounded for m in dynmaps)
if holomaps:
validate_sampled_mode(holomaps, dynmaps)
elif dynamic_sampled and not holomaps:
raise Exception("DynamicMaps in sampled mode must be displayed alongside "
validate_unbounded_mode(holomaps, dynmaps)
elif dynamic_unbounded and not holomaps:
raise Exception("DynamicMaps in unbounded mode must be displayed alongside "
"a HoloMap to define the sampling.")
return dynmaps and not holomaps, dynamic_sampled
return dynmaps and not holomaps, dynamic_unbounded


def initialize_sampled(obj, dimensions, key):
def initialize_unbounded(obj, dimensions, key):
"""
Initializes any DynamicMaps in sampled mode.
Initializes any DynamicMaps in unbounded mode.
"""
select = dict(zip([d.name for d in dimensions], key))
try:
Expand Down
Loading

0 comments on commit 3d05c84

Please sign in to comment.