Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve DynamicMap usability and deprecate sampled mode #1305

Merged
merged 39 commits into from Apr 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
10f5623
Removed unused imports
jlstevens Apr 17, 2017
bbc702a
Fixed bug in DynamicMap._initial_key method
jlstevens Apr 17, 2017
a69c9f9
Improved KeyError exception message in DynamicMap._initial_key
jlstevens Apr 17, 2017
a98ab1b
Raise SkipRendering in initialize_dynamic on initial key error
jlstevens Apr 17, 2017
7f72ace
Removed unhelpful prefix when issuing skip rendering warnings
jlstevens Apr 17, 2017
79c5225
Tweaked _initial_key KeyError exception message
jlstevens Apr 17, 2017
5cccdf1
Replaced DynamicMap sampled parameter with read-only property
jlstevens Apr 17, 2017
7788169
Added deprecation warning when using the sampled parameter
jlstevens Apr 17, 2017
95d8d1d
Replaced sampled property with more general unbounded property
jlstevens Apr 18, 2017
ca27441
Updated code to use the DynamicMap unbounded property
jlstevens Apr 18, 2017
3f80d76
Raising warning about unbounded dynamic map in display_hooks.map_display
jlstevens Apr 18, 2017
a07ebb6
Fixed bug in unbounded property definition
jlstevens Apr 18, 2017
88acb7e
Removed use of sampled parameter from the tests
jlstevens Apr 18, 2017
5f29a75
Improved warning message shown when displaying unbounded DynamicMap
jlstevens Apr 18, 2017
fb5a022
Fixed type comparison issue in DynamicMap.unbounded property
jlstevens Apr 18, 2017
d7b596b
Made DynamicMap._initial_key stream aware
jlstevens Apr 18, 2017
50756c5
Added validation of objects supplied in DynamicMap streams list
jlstevens Apr 18, 2017
fb1d8fe
DynamicMap.event now returns immediately if there are no streams
jlstevens Apr 18, 2017
8381be1
Improved KeyError exception messages raised in DynamicMap.event
jlstevens Apr 18, 2017
6f238b6
Simplified logic in DynamicMap.event method
jlstevens Apr 18, 2017
dc07a5f
Added simple Counter stream
jlstevens Apr 18, 2017
adbb60e
Renamed PositionX/Y/XY streams to PointerX/Y/XY
jlstevens Apr 18, 2017
17130ec
PositionX/Y/XY streams now issue deprecation warnings
jlstevens Apr 18, 2017
f43b8f9
Added simple unlinked X,Y and XY streams
jlstevens Apr 18, 2017
7c7c49b
Updated Pointer stream docstrings
jlstevens Apr 18, 2017
15c5f27
Declared the counter stream parameter as constant
jlstevens Apr 18, 2017
1b02b7c
Updated unit test regexp
jlstevens Apr 18, 2017
adf895c
Merged two test classes testing bounded DynamicMaps
jlstevens Apr 18, 2017
55bad90
Renamed bokeh Position callbacks to Pointer callbacks
jlstevens Apr 18, 2017
5df629d
Updated Position streams to Pointer streams for all unit tests
jlstevens Apr 18, 2017
b38f683
Configured callbacks for deprecated position streams
jlstevens Apr 18, 2017
748be78
DynamicMap no longer has any default key dimensions
jlstevens Apr 18, 2017
a96bab2
Updated tests relying on DynamicMap default kdim
jlstevens Apr 18, 2017
d4c792a
Added seven DynamicMap constructor tests
jlstevens Apr 18, 2017
024e817
Added missing condition to util.validate_dynamic_argspec
jlstevens Apr 18, 2017
90d2bac
Updated invalid test in DynamicMapConstructor
jlstevens Apr 19, 2017
8ef13a2
Testing unbounded property in DynamicMapUnboundedProperty
jlstevens Apr 19, 2017
09e7872
Added mixed kdim and stream tests of unbounded property
jlstevens Apr 19, 2017
6aee813
Fixed test_dynamic_kdims_only_invalid unit test in testcallable.py
jlstevens Apr 19, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
69 changes: 52 additions & 17 deletions holoviews/core/spaces.py
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
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
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
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
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
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
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
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