From d671883bb58b401533dee36e14e0f6e93ba65641 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 31 Aug 2017 14:14:56 +0100 Subject: [PATCH] Fixed scrubber bug and add support for discrete DynamicMap scrubber (#1832) --- holoviews/core/spaces.py | 6 +++++- holoviews/core/traversal.py | 7 ++++++- holoviews/plotting/plot.py | 14 +++++++------- holoviews/plotting/renderer.py | 7 ++++--- holoviews/plotting/widgets/widgets.js | 19 ++++--------------- tests/testtraversal.py | 21 +++++++++++++++++++++ 6 files changed, 47 insertions(+), 27 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index ebe56f6c7a..f6cadf7e6c 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -518,6 +518,10 @@ def __call__(self, *args, **kwargs): try: ret = self.callable(*args, **kwargs) + except KeyError: + # KeyError is caught separately because it is used to signal + # invalid keys on DynamicMap and should not warn + raise except: posstr = ', '.join(['%r' % el for el in self.args]) if self.args else '' kwstr = ', '.join('%s=%r' % (k,v) for k,v in self.kwargs.items()) @@ -1055,7 +1059,7 @@ def _cache(self, key, val): if len(self) >= cache_size: first_key = next(k for k in self.data) self.data.pop(first_key) - self.data[key] = val + self[key] = val def map(self, map_fn, specs=None, clone=True): diff --git a/holoviews/core/traversal.py b/holoviews/core/traversal.py index 93e48a9da6..e320601d17 100644 --- a/holoviews/core/traversal.py +++ b/holoviews/core/traversal.py @@ -8,7 +8,7 @@ from operator import itemgetter from .dimension import Dimension -from .util import merge_dimensions +from .util import merge_dimensions, cartesian_product try: import itertools.izip as zip @@ -89,6 +89,11 @@ def unique_dimkeys(obj, default_dim='Frame'): if not matches: unique_keys.append(padded_key) + # Add cartesian product of DynamicMap values to keys + values = [d.values for d in all_dims] + if obj.traverse(lambda x: x, ['DynamicMap']) and values and all(values): + unique_keys += list(zip(*cartesian_product(values))) + with item_check(False): sorted_keys = NdMapping({key: None for key in unique_keys}, kdims=all_dims).data.keys() diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index d52171d6fa..ac67302956 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -18,7 +18,7 @@ from ..core.options import Store, Compositor, SkipRendering from ..core.overlay import NdOverlay from ..core.spaces import HoloMap, DynamicMap -from ..core.util import stream_parameters +from ..core.util import stream_parameters, cartesian_product from ..element import Table from .util import (get_dynamic_mode, initialize_unbounded, dim_axis_label, attach_streams, traverse_setter, get_nested_streams, @@ -587,10 +587,15 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, if self.batched and not isinstance(self, GenericOverlayPlot): plot_element = plot_element.last + dynamic = isinstance(element, DynamicMap) and not element.unbounded self.top_level = keys is None if self.top_level: dimensions = self.hmap.kdims - keys = list(self.hmap.data.keys()) + values = [d.values for d in dimensions] + if dynamic and values and all(values): + keys = list(zip(*cartesian_product(values))) + else: + keys = list(self.hmap.data.keys()) self.style = self.lookup_options(plot_element, 'style') if style is None else style plot_opts = self.lookup_options(plot_element, 'plot').options @@ -600,7 +605,6 @@ 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.unbounded super(GenericElementPlot, self).__init__(keys=keys, dimensions=dimensions, dynamic=dynamic, **dict(params, **plot_opts)) @@ -997,10 +1001,6 @@ def _get_frame(self, key): return layout_frame - def __len__(self): - return len(self.keys) - - def _format_title(self, key, dimensions=True, separator='\n'): dim_title = self._frame_title(key, 3, separator) if dimensions else '' layout = self.layout diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py index 7d15e310ce..2f600e0c63 100644 --- a/holoviews/plotting/renderer.py +++ b/holoviews/plotting/renderer.py @@ -301,15 +301,16 @@ def get_widget(self_or_cls, plot, widget_type, **kwargs): if not isinstance(plot, Plot): plot = self_or_cls.get_plot(plot) dynamic = plot.dynamic + # Whether dimensions define discrete space + discrete = all(d.values for d in plot.dimensions) if widget_type == 'auto': isuniform = plot.uniform if not isuniform: widget_type = 'scrubber' else: widget_type = 'widgets' - elif dynamic: widget_type = 'widgets' - elif widget_type == 'scrubber' and dynamic: - raise ValueError('DynamicMap do not support scrubber widget') + elif dynamic and not discrete: + widget_type = 'widgets' if widget_type in [None, 'auto']: holomap_formats = self_or_cls.mode_formats['holomap'][self_or_cls.mode] diff --git a/holoviews/plotting/widgets/widgets.js b/holoviews/plotting/widgets/widgets.js index cbb805c886..f9d14d55bf 100644 --- a/holoviews/plotting/widgets/widgets.js +++ b/holoviews/plotting/widgets/widgets.js @@ -257,17 +257,7 @@ ScrubberWidget.prototype.get_loop_state = function(){ ScrubberWidget.prototype.next_frame = function() { - if (this.dynamic || !this.cached) { - if (this.wait) { - return - } - this.wait = true; - } - if (this.dynamic && this.current_frame + 1 >= this.length) { - this.length += 1; - document.getElementById(this.slider_id).max = this.length-1; - } - this.set_frame(Math.min(this.length - 1, this.current_frame + 1)); + this.set_frame(Math.min(this.length - 1, this.current_frame + 1)); } ScrubberWidget.prototype.previous_frame = function() { @@ -295,7 +285,7 @@ ScrubberWidget.prototype.faster = function() { } ScrubberWidget.prototype.anim_step_forward = function() { - if(this.current_frame < this.length || (this.dynamic && !this.stopped)){ + if(this.current_frame < this.length - 1){ this.next_frame(); }else{ var loop_state = this.get_loop_state(); @@ -312,9 +302,8 @@ ScrubberWidget.prototype.anim_step_forward = function() { } ScrubberWidget.prototype.anim_step_reverse = function() { - this.current_frame -= 1; - if(this.current_frame >= 0){ - this.set_frame(this.current_frame); + if(this.current_frame > 0){ + this.previous_frame(); } else { var loop_state = this.get_loop_state(); if(loop_state == "loop"){ diff --git a/tests/testtraversal.py b/tests/testtraversal.py index cc37ec7b03..d469c6ad8e 100644 --- a/tests/testtraversal.py +++ b/tests/testtraversal.py @@ -27,6 +27,27 @@ def test_unique_keys_no_overlap_exception(self): with self.assertRaisesRegexp(Exception, exception): dims, keys = unique_dimkeys(hmap1+hmap2) + def test_unique_keys_dmap_complete_overlap(self): + hmap1 = DynamicMap(lambda x: Curve(range(10)), kdims=['x']).redim.values(x=[1, 2, 3]) + hmap2 = DynamicMap(lambda x: Curve(range(10)), kdims=['x']).redim.values(x=[1, 2, 3]) + dims, keys = unique_dimkeys(hmap1+hmap2) + self.assertEqual(hmap1.kdims, dims) + self.assertEqual(keys, [(i,) for i in range(1, 4)]) + + def test_unique_keys_dmap_partial_overlap(self): + hmap1 = DynamicMap(lambda x: Curve(range(10)), kdims=['x']).redim.values(x=[1, 2, 3]) + hmap2 = DynamicMap(lambda x: Curve(range(10)), kdims=['x']).redim.values(x=[1, 2, 3, 4]) + dims, keys = unique_dimkeys(hmap1+hmap2) + self.assertEqual(hmap2.kdims, dims) + self.assertEqual(keys, [(i,) for i in range(1, 5)]) + + def test_unique_keys_dmap_cartesian_product(self): + hmap1 = DynamicMap(lambda x, y: Curve(range(10)), kdims=['x', 'y']).redim.values(x=[1, 2, 3]) + hmap2 = DynamicMap(lambda x, y: Curve(range(10)), kdims=['x', 'y']).redim.values(y=[1, 2, 3]) + dims, keys = unique_dimkeys(hmap1+hmap2) + self.assertEqual(hmap1.kdims[:1]+hmap2.kdims[1:], dims) + self.assertEqual(keys, [(x, y) for x in range(1, 4) for y in range(1, 4)]) + def test_unique_keys_no_overlap_dynamicmap_uninitialized(self): dmap1 = DynamicMap(lambda A: Curve(range(10)), kdims=['A']) dmap2 = DynamicMap(lambda B: Curve(range(10)), kdims=['B'])