From 7e6a9b07d86ffe7f8bf9992055b47443d19951e3 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Sun, 28 Jun 2015 01:04:23 +0100 Subject: [PATCH 01/60] Initial commit of DynamicMap subclass of HoloMap --- holoviews/core/element.py | 131 +++++++++++++++++++++++++++++ holoviews/ipython/display_hooks.py | 6 +- holoviews/plotting/plot.py | 6 +- 3 files changed, 137 insertions(+), 6 deletions(-) diff --git a/holoviews/core/element.py b/holoviews/core/element.py index fc1a189d62..3c042c2435 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -1,4 +1,7 @@ import operator +import inspect +import types + from itertools import groupby from numbers import Number import numpy as np @@ -715,6 +718,134 @@ def hist(self, num_bins=20, bin_range=None, adjoin=True, individually=True, **kw return (self << histmap) if adjoin else histmap +class DynamicMap(HoloMap): + """ + A DynamicMap is a type of HoloMap where the elements are + dynamically generated by a generator or callable. A DynamicMap + supports two different interval modes 'closed' where the limits of + the parameter space is known ahead of time (as declared by the + ranges on the key dimensions) or 'open' which allows the continual + generation of elements (e.g as data output by a simulator over an + unbounded simulated time dimension). + + Open mode is defined by code using the next() interface (iterators + and generators) whereas closed mode is supported by callable + interfaces. + """ + + gen = param.Parameter(doc=""" + The generator of the elements in the DynamicMap. In the simplest + case, this can be anything supporting next() such as a Python + generator or iterator: + + (Image(np.random.rand(25,25)) for i in range(10)) + + Objects with the next() interface may only be used in 'open' + mode as they accept no arguments. As the above example returns + only returns element, the key dimension is a simple integer + counter. + + If an integer counter is not appropriate, the generator may + return keys together with the elements as follows: + + ((i, Image(i*np.random.rand(25,25))) for i in np.linspace(0,1,10)) + + To support 'closed' mode, the output element must be a callable + that accepts the requested key.. + + Note that in 'closed' mode only the element is to be returned as + the key is already known and that the callable is expected to be + a function in the mathematical sense; a particular key should + map to a unique element output. + + In 'both' mode, both the next() and callable interfaces must be + supported. + """) + + interval = param.ObjectSelector(default='closed', + objects=['open', 'closed', 'both'], doc=""" + Whether the dynamic map operates on a closed interval (requests + to the supplied function are guaranteed to be within a known, + finite interval) or an open interval (for instance for a + simulation that can may run for an unknown length of time).""") + + cache_size = param.Integer(default=10, doc=""" + The number of entries to cache for fast access. This is an LRU + cache where the least recently used item is overwritten once + the cache is full.""") + + def __init__(self, initial_items=None, **params): + + item_types = (list, dict, OrderedDict) + initial_items = initial_items if isinstance(initial_items, item_types) else [] + super(DynamicMap, self).__init__(initial_items, **params) + + if self.gen is None: + raise Exception("A generator or generator function must be " + "supplied to declare a DynamicMap") + + self.counter = 0 if self.interval == 'open' else None + + def clone(self, data=None, shared_data=True, *args, **overrides): + """ + Overrides Dimensioned clone to avoid checking items if data + is unchanged. + """ + return super(UniformNdMapping, self).clone(data, shared_data, + *args, **overrides) + + + def reset(self): + """ + Clear the cache and reset the counter to zero. + """ + self.counter = 0 if self.interval == 'open' else None + self.data = OrderedDict() + + + def __getitem__(self, key): + """ + Return an element for any key chosen key (in 'closed mode') or + for a previously generated key that is still in the cache + ('open mode') + """ + try: + return super(DynamicMap,self).__getitem__(key) + except KeyError as e: + if self.interval == 'open': + raise KeyError(str(e) + " Note: Cannot index outside " + "available cache in open interval mode.") + val = self.gen(key) + self.data[key if isinstance(key, tuple) else (key,) ] = val + return val + + + def next(self): + """ + Interface for 'open' mode that mirrors the next() interface of + the supplied generator or iterator. Both a key and element are + required (if no key is explicitly supplied, the counter is + used instead). + """ + if self.interval == 'closed': + raise Exception("The next() method should only be called in open interval mode.") + + retval = self.gen.next() + (key, val) = (retval if isinstance(retval, tuple) + and len(retval) ==2 else (self.counter, retval)) + + key = key if isinstance(key, tuple) else (key,) + if len(key) != len(self.key_dimensions): + raise Exception("Generated key does not match the number of key dimensions") + + self.data[key] = val + self.counter += 1 + return val + + def __iter__(self): + return self if self.interval=='open' else iter(list(self.values())) + + class GridSpace(UniformNdMapping): """ Grids are distinct from Layouts as they ensure all contained diff --git a/holoviews/ipython/display_hooks.py b/holoviews/ipython/display_hooks.py index 081665cbcf..deeca8fd4d 100644 --- a/holoviews/ipython/display_hooks.py +++ b/holoviews/ipython/display_hooks.py @@ -9,7 +9,7 @@ from ..core.options import Store, StoreOptions from ..core import Element, ViewableElement, UniformNdMapping, HoloMap, AdjointLayout, NdLayout,\ - GridSpace, Layout, displayable, undisplayable_info, CompositeOverlay + GridSpace, Layout, CompositeOverlay, DynamicMap, displayable, undisplayable_info from ..core.traversal import unique_dimkeys, bijective from .magics import OutputMagic, OptsMagic @@ -198,7 +198,7 @@ class Warning(param.Parameterized): pass @display_hook def map_display(vmap, size, max_frames, max_branches, widget_mode): - if not isinstance(vmap, HoloMap): return None + if not isinstance(vmap, (HoloMap, DynamicMap)): return None if not displayable(vmap): display_warning.warning("Nesting %ss within a HoloMap makes it difficult " @@ -299,7 +299,7 @@ def display(obj, raw=False): html = element_display(obj) elif isinstance(obj, (Layout, NdLayout, AdjointLayout)): html = layout_display(obj) - elif isinstance(obj, HoloMap): + elif isinstance(obj, (HoloMap, DynamicMap)): html = map_display(obj) else: return repr(obj) if raw else IPython.display.display(obj) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 06f468936d..d8f06fcb52 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -12,7 +12,7 @@ from ..core import OrderedDict from ..core import util, traversal -from ..core.element import HoloMap, Element +from ..core.element import HoloMap, DynamicMap, Element from ..core.overlay import Overlay, CompositeOverlay from ..core.layout import Empty, NdLayout, Layout from ..core.options import Store, Compositor @@ -322,7 +322,7 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, self.zorder = zorder self.cyclic_index = cyclic_index self.overlaid = overlaid - if not isinstance(element, HoloMap): + if not isinstance(element, (HoloMap, DynamicMap)): self.map = HoloMap(initial_items=(0, element), kdims=['Frame'], id=element.id) else: @@ -352,7 +352,7 @@ def _get_frame(self, key): else: select = dict(zip(self.map.dimensions('key', label=True), key)) try: - selection = self.map.select((HoloMap,), **select) + selection = self.map.select((HoloMap, DynamicMap), **select) except KeyError: selection = None return selection.last if isinstance(selection, HoloMap) else selection From f865915bf8d1632a7a3ec296efff1acf26fd0c5d Mon Sep 17 00:00:00 2001 From: jlstevens Date: Sun, 28 Jun 2015 01:09:27 +0100 Subject: [PATCH 02/60] Removed unnecessary import --- holoviews/core/element.py | 1 - 1 file changed, 1 deletion(-) diff --git a/holoviews/core/element.py b/holoviews/core/element.py index 3c042c2435..c0dd8744e6 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -1,6 +1,5 @@ import operator import inspect -import types from itertools import groupby from numbers import Number From 118f97ec2687e5b167a961519e9a485939bc7b94 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Sun, 28 Jun 2015 01:16:49 +0100 Subject: [PATCH 03/60] Set default interval mode to 'open' as it is the simplest mode to use Open mode can be used quickly and easily with inline generators whereas 'closed' interval mode requires ranges on all the key dimensions. --- holoviews/core/element.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/core/element.py b/holoviews/core/element.py index c0dd8744e6..4c15f6e125 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -761,13 +761,14 @@ class DynamicMap(HoloMap): supported. """) - interval = param.ObjectSelector(default='closed', + interval = param.ObjectSelector(default='open', objects=['open', 'closed', 'both'], doc=""" Whether the dynamic map operates on a closed interval (requests to the supplied function are guaranteed to be within a known, finite interval) or an open interval (for instance for a simulation that can may run for an unknown length of time).""") + cache_size = param.Integer(default=10, doc=""" The number of entries to cache for fast access. This is an LRU cache where the least recently used item is overwritten once From 536965fec24c352c5a87ba122ff8402b558dba14 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Sun, 28 Jun 2015 01:21:18 +0100 Subject: [PATCH 04/60] Ensuring all key dimensions have finite ranges in closed interval mode --- holoviews/core/element.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/holoviews/core/element.py b/holoviews/core/element.py index 4c15f6e125..f08657b38a 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -786,6 +786,12 @@ def __init__(self, initial_items=None, **params): self.counter = 0 if self.interval == 'open' else None + if self.interval == 'closed': + for kdim in self.kdims: + if None in kdim.range: + raise Exception("In closed interval mode all key " + "dimensions ranges need specified ranges") + def clone(self, data=None, shared_data=True, *args, **overrides): """ Overrides Dimensioned clone to avoid checking items if data From d83d2b88ad558ebbabce815464582186fd02c137 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Sun, 28 Jun 2015 01:04:23 +0100 Subject: [PATCH 05/60] Initial commit of DynamicMap subclass of HoloMap --- holoviews/core/element.py | 131 +++++++++++++++++++++++++++++ holoviews/ipython/display_hooks.py | 6 +- holoviews/plotting/plot.py | 6 +- 3 files changed, 137 insertions(+), 6 deletions(-) diff --git a/holoviews/core/element.py b/holoviews/core/element.py index d85822e368..97a6188d09 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -1,4 +1,7 @@ import operator +import inspect +import types + from itertools import groupby from numbers import Number import numpy as np @@ -776,6 +779,134 @@ def hist(self, num_bins=20, bin_range=None, adjoin=True, individually=True, **kw return (self << histmap) if adjoin else histmap +class DynamicMap(HoloMap): + """ + A DynamicMap is a type of HoloMap where the elements are + dynamically generated by a generator or callable. A DynamicMap + supports two different interval modes 'closed' where the limits of + the parameter space is known ahead of time (as declared by the + ranges on the key dimensions) or 'open' which allows the continual + generation of elements (e.g as data output by a simulator over an + unbounded simulated time dimension). + + Open mode is defined by code using the next() interface (iterators + and generators) whereas closed mode is supported by callable + interfaces. + """ + + gen = param.Parameter(doc=""" + The generator of the elements in the DynamicMap. In the simplest + case, this can be anything supporting next() such as a Python + generator or iterator: + + (Image(np.random.rand(25,25)) for i in range(10)) + + Objects with the next() interface may only be used in 'open' + mode as they accept no arguments. As the above example returns + only returns element, the key dimension is a simple integer + counter. + + If an integer counter is not appropriate, the generator may + return keys together with the elements as follows: + + ((i, Image(i*np.random.rand(25,25))) for i in np.linspace(0,1,10)) + + To support 'closed' mode, the output element must be a callable + that accepts the requested key.. + + Note that in 'closed' mode only the element is to be returned as + the key is already known and that the callable is expected to be + a function in the mathematical sense; a particular key should + map to a unique element output. + + In 'both' mode, both the next() and callable interfaces must be + supported. + """) + + interval = param.ObjectSelector(default='closed', + objects=['open', 'closed', 'both'], doc=""" + Whether the dynamic map operates on a closed interval (requests + to the supplied function are guaranteed to be within a known, + finite interval) or an open interval (for instance for a + simulation that can may run for an unknown length of time).""") + + cache_size = param.Integer(default=10, doc=""" + The number of entries to cache for fast access. This is an LRU + cache where the least recently used item is overwritten once + the cache is full.""") + + def __init__(self, initial_items=None, **params): + + item_types = (list, dict, OrderedDict) + initial_items = initial_items if isinstance(initial_items, item_types) else [] + super(DynamicMap, self).__init__(initial_items, **params) + + if self.gen is None: + raise Exception("A generator or generator function must be " + "supplied to declare a DynamicMap") + + self.counter = 0 if self.interval == 'open' else None + + def clone(self, data=None, shared_data=True, *args, **overrides): + """ + Overrides Dimensioned clone to avoid checking items if data + is unchanged. + """ + return super(UniformNdMapping, self).clone(data, shared_data, + *args, **overrides) + + + def reset(self): + """ + Clear the cache and reset the counter to zero. + """ + self.counter = 0 if self.interval == 'open' else None + self.data = OrderedDict() + + + def __getitem__(self, key): + """ + Return an element for any key chosen key (in 'closed mode') or + for a previously generated key that is still in the cache + ('open mode') + """ + try: + return super(DynamicMap,self).__getitem__(key) + except KeyError as e: + if self.interval == 'open': + raise KeyError(str(e) + " Note: Cannot index outside " + "available cache in open interval mode.") + val = self.gen(key) + self.data[key if isinstance(key, tuple) else (key,) ] = val + return val + + + def next(self): + """ + Interface for 'open' mode that mirrors the next() interface of + the supplied generator or iterator. Both a key and element are + required (if no key is explicitly supplied, the counter is + used instead). + """ + if self.interval == 'closed': + raise Exception("The next() method should only be called in open interval mode.") + + retval = self.gen.next() + (key, val) = (retval if isinstance(retval, tuple) + and len(retval) ==2 else (self.counter, retval)) + + key = key if isinstance(key, tuple) else (key,) + if len(key) != len(self.key_dimensions): + raise Exception("Generated key does not match the number of key dimensions") + + self.data[key] = val + self.counter += 1 + return val + + def __iter__(self): + return self if self.interval=='open' else iter(list(self.values())) + + class GridSpace(UniformNdMapping): """ Grids are distinct from Layouts as they ensure all contained diff --git a/holoviews/ipython/display_hooks.py b/holoviews/ipython/display_hooks.py index 37a11fac4e..6b615ef4aa 100644 --- a/holoviews/ipython/display_hooks.py +++ b/holoviews/ipython/display_hooks.py @@ -9,7 +9,7 @@ from ..core.options import Store, StoreOptions from ..core import Element, ViewableElement, UniformNdMapping, HoloMap, AdjointLayout, NdLayout,\ - GridSpace, Layout, displayable, undisplayable_info, CompositeOverlay + GridSpace, Layout, CompositeOverlay, DynamicMap, displayable, undisplayable_info from ..core.traversal import unique_dimkeys, bijective from .magics import OutputMagic, OptsMagic @@ -198,7 +198,7 @@ class Warning(param.Parameterized): pass @display_hook def map_display(vmap, size, max_frames, max_branches, widget_mode): - if not isinstance(vmap, HoloMap): return None + if not isinstance(vmap, (HoloMap, DynamicMap)): return None if not displayable(vmap): display_warning.warning("Nesting %ss within a HoloMap makes it difficult " @@ -299,7 +299,7 @@ def display(obj, raw=False): html = element_display(obj) elif isinstance(obj, (Layout, NdLayout, AdjointLayout)): html = layout_display(obj) - elif isinstance(obj, HoloMap): + elif isinstance(obj, (HoloMap, DynamicMap)): html = map_display(obj) else: return repr(obj) if raw else IPython.display.display(obj) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index ef9ec353f5..00d93e9054 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -12,7 +12,7 @@ from ..core import OrderedDict from ..core import util, traversal -from ..core.element import HoloMap, Element +from ..core.element import HoloMap, DynamicMap, Element from ..core.overlay import Overlay, CompositeOverlay from ..core.layout import Empty, NdLayout, Layout from ..core.options import Store, Compositor @@ -332,7 +332,7 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, self.zorder = zorder self.cyclic_index = cyclic_index self.overlaid = overlaid - if not isinstance(element, HoloMap): + if not isinstance(element, (HoloMap, DynamicMap)): self.map = HoloMap(initial_items=(0, element), kdims=['Frame'], id=element.id) else: @@ -362,7 +362,7 @@ def _get_frame(self, key): else: select = dict(zip(self.map.dimensions('key', label=True), key)) try: - selection = self.map.select((HoloMap,), **select) + selection = self.map.select((HoloMap, DynamicMap), **select) except KeyError: selection = None return selection.last if isinstance(selection, HoloMap) else selection From 9470d3b16f222d4cbceb81e5da45f258fadd6a9a Mon Sep 17 00:00:00 2001 From: jlstevens Date: Sun, 28 Jun 2015 01:09:27 +0100 Subject: [PATCH 06/60] Removed unnecessary import --- holoviews/core/element.py | 1 - 1 file changed, 1 deletion(-) diff --git a/holoviews/core/element.py b/holoviews/core/element.py index 97a6188d09..a7198a9906 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -1,6 +1,5 @@ import operator import inspect -import types from itertools import groupby from numbers import Number From cb3f6c4d6967e6d39eea11dda3f1ac0229a6da52 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Sun, 28 Jun 2015 01:16:49 +0100 Subject: [PATCH 07/60] Set default interval mode to 'open' as it is the simplest mode to use Open mode can be used quickly and easily with inline generators whereas 'closed' interval mode requires ranges on all the key dimensions. --- holoviews/core/element.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/core/element.py b/holoviews/core/element.py index a7198a9906..e6ba27e59e 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -822,13 +822,14 @@ class DynamicMap(HoloMap): supported. """) - interval = param.ObjectSelector(default='closed', + interval = param.ObjectSelector(default='open', objects=['open', 'closed', 'both'], doc=""" Whether the dynamic map operates on a closed interval (requests to the supplied function are guaranteed to be within a known, finite interval) or an open interval (for instance for a simulation that can may run for an unknown length of time).""") + cache_size = param.Integer(default=10, doc=""" The number of entries to cache for fast access. This is an LRU cache where the least recently used item is overwritten once From 90d95c6a7b8a1c8fea0b1b638da3d760fc6491a8 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Sun, 28 Jun 2015 01:21:18 +0100 Subject: [PATCH 08/60] Ensuring all key dimensions have finite ranges in closed interval mode --- holoviews/core/element.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/holoviews/core/element.py b/holoviews/core/element.py index e6ba27e59e..9fe5696985 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -847,6 +847,12 @@ def __init__(self, initial_items=None, **params): self.counter = 0 if self.interval == 'open' else None + if self.interval == 'closed': + for kdim in self.kdims: + if None in kdim.range: + raise Exception("In closed interval mode all key " + "dimensions ranges need specified ranges") + def clone(self, data=None, shared_data=True, *args, **overrides): """ Overrides Dimensioned clone to avoid checking items if data From 58d1a5e029f4ffff28594819c5d32e58bf88f7db Mon Sep 17 00:00:00 2001 From: philippjfr Date: Tue, 29 Sep 2015 02:31:14 +0100 Subject: [PATCH 09/60] DynamicMap now defaults to closed interval --- holoviews/core/spaces.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 413b6b1582..3305dbc444 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -352,7 +352,7 @@ class DynamicMap(HoloMap): supported. """) - interval = param.ObjectSelector(default='open', + interval = param.ObjectSelector(default='closed', objects=['open', 'closed', 'both'], doc=""" Whether the dynamic map operates on a closed interval (requests to the supplied function are guaranteed to be within a known, @@ -378,10 +378,19 @@ def __init__(self, initial_items=None, **params): self.counter = 0 if self.interval == 'open' else None if self.interval == 'closed': + key = [] for kdim in self.kdims: - if None in kdim.range: + if kdim.values: + key.append(kdim.values[0]) + elif kdim.range: + key.append(kdim.range[0]) + else: raise Exception("In closed interval mode all key " - "dimensions ranges need specified ranges") + "dimensions need specified ranges" + "or values.") + if not len(self): + self[tuple(key)] + def clone(self, data=None, shared_data=True, *args, **overrides): """ @@ -413,7 +422,7 @@ def __getitem__(self, key): raise KeyError(str(e) + " Note: Cannot index outside " "available cache in open interval mode.") val = self.gen(key) - self.data[key if isinstance(key, tuple) else (key,) ] = val + self.data[key if isinstance(key, tuple) else (key,)] = val return val From b6201cbc399f5dfb5824da72f27e91985a11404c Mon Sep 17 00:00:00 2001 From: philippjfr Date: Tue, 29 Sep 2015 02:33:39 +0100 Subject: [PATCH 10/60] Added support for handling DynamicMap to plot baseclasses --- holoviews/ipython/display_hooks.py | 2 +- holoviews/plotting/plot.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/holoviews/ipython/display_hooks.py b/holoviews/ipython/display_hooks.py index 6b615ef4aa..954aa6f9e6 100644 --- a/holoviews/ipython/display_hooks.py +++ b/holoviews/ipython/display_hooks.py @@ -129,7 +129,7 @@ def render_plot(plot, widget_mode, message=None): renderer = OutputMagic.renderer(dpi=kwargs['dpi'], fps=kwargs['fps']) with renderer.state(): - if len(plot) == 1: + if len(plot) == 1 and not plot.dynamic: plot.update(0) return display_frame(plot, renderer, **kwargs) elif widget_mode is not None: diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 42a0ebc959..1a42f89868 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -146,7 +146,7 @@ class DimensionedPlot(Plot): def __init__(self, keys=None, dimensions=None, layout_dimensions=None, uniform=True, subplot=False, adjoined=None, layout_num=0, - style=None, subplots=None, **params): + style=None, subplots=None, dynamic=False, **params): self.subplots = subplots self.adjoined = adjoined self.dimensions = dimensions @@ -155,6 +155,7 @@ def __init__(self, keys=None, dimensions=None, layout_dimensions=None, self.subplot = subplot self.keys = keys self.uniform = uniform + self.dynamic = dynamic self.drawn = False self.handles = {} self.group = None @@ -170,10 +171,11 @@ def __getitem__(self, frame): """ Get the state of the Plot for a given frame number. """ - if frame > len(self): + if isinstance(frame, int) and frame > len(self): self.warning("Showing last frame available: %d" % len(self)) if not self.drawn: self.handles['fig'] = self.initialize_plot() - self.update_frame(self.keys[frame]) + if not isinstance(frame, tuple): frame = self.keys[frame] + self.update_frame(frame) return self.state @@ -361,6 +363,7 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, self.zorder = zorder self.cyclic_index = cyclic_index self.overlaid = overlaid + dynamic = isinstance(element, DynamicMap) if not isinstance(element, (HoloMap, DynamicMap)): self.hmap = HoloMap(initial_items=(0, element), kdims=['Frame'], id=element.id) @@ -371,6 +374,7 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, keys = keys if keys else list(self.hmap.data.keys()) plot_opts = self.lookup_options(self.hmap.last, 'plot').options super(GenericElementPlot, self).__init__(keys=keys, dimensions=dimensions, + dynamic=dynamic, **dict(params, **plot_opts)) @@ -708,7 +712,9 @@ def __init__(self, layout, **params): self.coords = list(product(range(self.rows), range(self.cols))) dimensions, keys = traversal.unique_dimkeys(layout) + dynamic = bool(layout.traverse(lambda x: x, [DynamicMap])) + uniform = traversal.uniform(layout) plotopts = self.lookup_options(layout, 'plot').options super(GenericLayoutPlot, self).__init__(keys=keys, dimensions=dimensions, - uniform=traversal.uniform(layout), - **dict(plotopts, **params)) + uniform=uniform, dynamic=dynamic, + **dict(plotopts, **params)) From 8b1e5ee9752a45def0e9cb432a112ecbd7a83893 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Tue, 29 Sep 2015 02:42:03 +0100 Subject: [PATCH 11/60] Added support for DynamicMaps to widgets --- holoviews/plotting/bokeh/bokehwidgets.js | 56 +++++++++++++---------- holoviews/plotting/mpl/mplwidgets.js | 29 +++++++----- holoviews/plotting/mpl/widgets.py | 9 ++-- holoviews/plotting/widgets/__init__.py | 41 +++++++++++------ holoviews/plotting/widgets/jsslider.jinja | 33 ++++++++----- holoviews/plotting/widgets/widgets.js | 49 +++++++++++--------- tests/testwidgets.py | 4 +- 7 files changed, 134 insertions(+), 87 deletions(-) diff --git a/holoviews/plotting/bokeh/bokehwidgets.js b/holoviews/plotting/bokeh/bokehwidgets.js index 192f45d010..2dc58a1d91 100644 --- a/holoviews/plotting/bokeh/bokehwidgets.js +++ b/holoviews/plotting/bokeh/bokehwidgets.js @@ -14,38 +14,44 @@ BokehScrubberWidget.prototype = Object.create(ScrubberWidget.prototype); // Define methods to override on widgets var BokehMethods = { init_slider : function(init_val){ - $.each(this.frames, $.proxy(function(index, frame) { - this.frames[index] = JSON.parse(frame); - }, this)); + $.each(this.frames, $.proxy(function(index, frame) { + this.frames[index] = JSON.parse(frame); + }, this)); }, update : function(current){ - var data = this.frames[current]; - - $.each(data, function(id, value) { + var data = this.frames[current]; + $.each(data, function(id, value) { var ds = Bokeh.Collections(value.type).get(id); if (ds != undefined) { - ds.set(value.data); + ds.set(value.data); } - }); + }); }, dynamic_update : function(current){ - function callback(initialized, msg){ - /* This callback receives data from Python as a string - in order to parse it correctly quotes are sliced off*/ - if (msg.msg_type == "execute_result") { - var data = msg.content.data['text/plain'].slice(1, -1); - this.frames[current] = JSON.parse(data); - this.update(current); - } - } - if(!(current in this.frames)) { - var kernel = IPython.notebook.kernel; - callbacks = {iopub: {output: $.proxy(callback, this, this.initialized)}}; - var cmd = "holoviews.plotting.widgets.NdWidget.widgets['" + this.id + "'].update(" + current + ")"; - kernel.execute("import holoviews;" + cmd, callbacks, {silent : false}); - } else { - this.update(current); - } + if(this.dynamic) { + current = JSON.stringify(current); + } + function callback(initialized, msg){ + /* This callback receives data from Python as a string + in order to parse it correctly quotes are sliced off*/ + if (msg.msg_type != "execute_result") { + console.log("Warning: HoloViews callback returned unexpected data for key: (", current, ") with the following content:", msg.content) + return + } + if (msg.msg_type == "execute_result") { + var data = msg.content.data['text/plain'].slice(1, -1); + this.frames[current] = JSON.parse(data); + this.update(current); + } + } + if(!(current in this.frames)) { + var kernel = IPython.notebook.kernel; + callbacks = {iopub: {output: $.proxy(callback, this, this.initialized)}}; + var cmd = "holoviews.plotting.widgets.NdWidget.widgets['" + this.id + "'].update(" + current + ")"; + kernel.execute("import holoviews;" + cmd, callbacks, {silent : false}); + } else { + this.update(current); + } } } diff --git a/holoviews/plotting/mpl/mplwidgets.js b/holoviews/plotting/mpl/mplwidgets.js index 901c2639b5..8c987d6618 100644 --- a/holoviews/plotting/mpl/mplwidgets.js +++ b/holoviews/plotting/mpl/mplwidgets.js @@ -40,26 +40,33 @@ var MPLMethods = { } } else { if(this.mode == 'mpld3') { - mpld3.draw_figure(cache_id, this.frames[idx]); + mpld3.draw_figure(cache_id, this.frames[idx]); } else { - this.cache[idx].html(this.frames[idx]); + this.cache[idx].html(this.frames[idx]); } } }, dynamic_update : function(current){ + if (this.dynamic) { + current = JSON.stringify(current); + } function callback(msg){ /* This callback receives data from Python as a string in order to parse it correctly quotes are sliced off*/ - if (!(this.mode == 'nbagg')) { - if(!(current in this.cache)) { - var data = msg.content.data['text/plain'].slice(1, -1); - if(this.mode == 'mpld3'){ - data = JSON.parse(data)[0]; - } - this.frames[current] = data; - this.update_cache(); + if (msg.msg_type != "execute_result") { + console.log("Warning: HoloViews callback returned unexpected data for key: (", current, ") with the following content:", msg.content) + return } - this.update(current); + if (!(this.mode == 'nbagg')) { + if(!(current in this.cache)) { + var data = msg.content.data['text/plain'].slice(1, -1); + if(this.mode == 'mpld3'){ + data = JSON.parse(data)[0]; + } + this.frames[current] = data; + this.update_cache(); + } + this.update(current); } } if((this.mode == 'nbagg') || !(current in this.cache)) { diff --git a/holoviews/plotting/mpl/widgets.py b/holoviews/plotting/mpl/widgets.py index ee2ed75b50..64a0dc46e0 100644 --- a/holoviews/plotting/mpl/widgets.py +++ b/holoviews/plotting/mpl/widgets.py @@ -71,16 +71,19 @@ def _plot_figure(self, idx): return self.renderer.html(self.plot, figure_format, css=css) - def update(self, n): + def update(self, key): + if self.dynamic: + key = tuple(key) + if self.renderer.mode == 'nbagg': if not self.manager._shown: self.comm.start() self.manager.add_web_socket(self.comm) self.manager._shown = True - fig = self.plot[n] + fig = self.plot[key] fig.canvas.draw_idle() return '' - frame = self._plot_figure(n) + frame = self._plot_figure(key) if self.renderer.mode == 'mpld3': frame = self.encode_frames({0: frame}) return frame diff --git a/holoviews/plotting/widgets/__init__.py b/holoviews/plotting/widgets/__init__.py index c8f1546c0c..f11b6cb735 100644 --- a/holoviews/plotting/widgets/__init__.py +++ b/holoviews/plotting/widgets/__init__.py @@ -71,6 +71,7 @@ def __init__(self, plot, renderer=None, **params): self.plot = plot self.dimensions = plot.dimensions self.keys = plot.keys + self.dynamic = plot.dynamic if renderer is None: self.renderer = plot.renderer.instance(dpi=self.display_options.get('dpi', 72)) else: @@ -141,8 +142,9 @@ def _plot_figure(self, idx): return self.renderer.html(self.plot, figure_format, css=css) - def update(self, n): - return self._plot_figure(n) + def update(self, key): + if self.dynamic: key = tuple(key) + return self._plot_figure(key) @@ -205,18 +207,28 @@ def get_widgets(self): dimensions = [] init_dim_vals = [] for idx, dim in enumerate(self.mock_obj.kdims): - dim_vals = dim.values if dim.values else sorted(set(self.mock_obj.dimension_values(dim.name))) - dim_vals = [v for v in dim_vals if v is not None] - if isnumeric(dim_vals[0]): - dim_vals = [round(v, 10) for v in dim_vals] - widget_type = 'slider' + if self.dynamic: + if dim.values: + dim_vals = dim.values + widget_type = 'dropdown' + else: + dim_vals = list(dim.range) + widget_type = 'slider' else: - widget_type = 'dropdown' + dim_vals = dim.values if dim.values else sorted(set(self.mock_obj.dimension_values(dim.name))) + if isnumeric(dim_vals[0]): + dim_vals = [round(v, 10) for v in dim_vals] + widget_type = 'slider' + else: + widget_type = 'dropdown' + dim_vals = repr([v for v in dim_vals if v is not None]) init_dim_vals.append(dim_vals[0]) dim_str = safe_unicode(dim.name) visibility = 'visibility: visible' if len(dim_vals) > 1 else 'visibility: hidden; height: 0;' - widgets.append(dict(dim=sanitize_identifier(dim_str), dim_label=dim_str, dim_idx=idx, vals=repr(dim_vals), - type=widget_type, visibility=visibility)) + widget_data = dict(dim=sanitize_identifier(dim_str), dim_label=dim_str, + dim_idx=idx, vals=dim_vals, type=widget_type, + visibility=visibility) + widgets.append(widget_data) dimensions.append(dim_str) return widgets, dimensions, init_dim_vals @@ -227,19 +239,20 @@ def get_key_data(self): for i, k in enumerate(self.mock_obj.data.keys()): key = [("%.1f" % v if v % 1 == 0 else "%.10f" % v) if isnumeric(v) else v for v in k] - key_data[str(tuple(key))] = i + key = str(tuple(key)) + key_data[key] = i return json.dumps(key_data) def _get_data(self): data = super(SelectionWidget, self)._get_data() widgets, dimensions, init_dim_vals = self.get_widgets() - key_data = self.get_key_data() + key_data = {} if self.dynamic else self.get_key_data() notfound_msg = "

{% elif widget_data['type']=='dropdown' %} @@ -118,16 +129,16 @@ diff --git a/holoviews/plotting/widgets/jsslider.jinja b/holoviews/plotting/widgets/jsslider.jinja index 6689d8321b..5ad3cfc353 100644 --- a/holoviews/plotting/widgets/jsslider.jinja +++ b/holoviews/plotting/widgets/jsslider.jinja @@ -1,155 +1,159 @@
-
- {% block init_frame %} - {{ init_frame }} - {% endblock %} -
+
+ {% block init_frame %} + {{ init_frame }} + {% endblock %} +
-
- {% for widget_data in widgets %} - {% if widget_data['type'] == 'slider' %} -
- -
-
- -
-
-
- - {% elif widget_data['type']=='dropdown' %} -
- - -
- - {% endif %} - {% endfor %} -
+
+ {% for widget_data in widgets %} + {% if widget_data['type'] == 'slider' %} +
+ +
+
+ +
+
+
+
+ + {% elif widget_data['type']=='dropdown' %} +
+ + +
+ + {% endif %} + {% endfor %} +
@@ -160,22 +164,23 @@ (function() { var widget_ids = new Array({{ Nwidget }}); {% for dim in dimensions %} - widget_ids[{{ loop.index0 }}] = "_anim_widget{{ id }}_{{ dim }}"; + widget_ids[{{ loop.index0 }}] = "_anim_widget{{ id }}_{{ dim }}"; {% endfor %} var frame_data = {{ frames | safe }}; var dim_vals = {{ init_dim_vals }}; var keyMap = {{ key_data }}; var notFound = "{{ notFound }}"; - function create_widget() { + function create_widget() { setTimeout(function() { - anim{{ id }} = new {{ widget_name }}(frame_data, "{{ id }}", widget_ids, - keyMap, dim_vals, notFound, {{ load_json }}, {{ mode }}, {{ cached }}, "", {{ dynamic }}); - }, 0); - } + anim{{ id }} = new {{ widget_name }}(frame_data, "{{ id }}", widget_ids, + keyMap, dim_vals, notFound, {{ load_json }}, {{ mode }}, + {{ cached }}, "", {{ dynamic }}); + }, 0); + } - {% block create_widget %} - create_widget(); - {% endblock %} + {% block create_widget %} + create_widget(); + {% endblock %} })(); diff --git a/holoviews/plotting/widgets/widgets.js b/holoviews/plotting/widgets/widgets.js index 0d3ffb482d..2ea03077b1 100644 --- a/holoviews/plotting/widgets/widgets.js +++ b/holoviews/plotting/widgets/widgets.js @@ -3,71 +3,71 @@ function HoloViewsWidget(){ HoloViewsWidget.prototype.init_slider = function(init_val){ if(this.cached) { - this.update_cache(); - this.update(0); + this.update_cache(); + this.update(0); } else { - this.dynamic_update(0); + this.dynamic_update(0); } } HoloViewsWidget.prototype.populate_cache = function(idx){ if(this.load_json) { - var data_url = this.server + this.fig_id + "/" + idx; - this.cache[idx].load(data_url); + var data_url = this.server + this.fig_id + "/" + idx; + this.cache[idx].load(data_url); } else { - this.cache[idx].html(this.frames[idx]); - if (this.embed) { - delete this.frames[idx]; - } + this.cache[idx].html(this.frames[idx]); + if (this.embed) { + delete this.frames[idx]; + } } } HoloViewsWidget.prototype.dynamic_update = function(current){ function callback(msg){ - /* This callback receives data from Python as a string - in order to parse it correctly quotes are sliced off*/ - var data = msg.content.data['text/plain'].slice(1, -1); - this.frames[current] = data; - this.update_cache(); - this.update(current); + /* This callback receives data from Python as a string + in order to parse it correctly quotes are sliced off*/ + var data = msg.content.data['text/plain'].slice(1, -1); + this.frames[current] = data; + this.update_cache(); + this.update(current); } if(!(current in this.cache)) { - var kernel = IPython.notebook.kernel; - callbacks = {iopub: {output: $.proxy(callback, this)}}; - var cmd = "holoviews.plotting.widgets.NdWidget.widgets['" + this.id + "'].update(" + current + ")"; - kernel.execute("import holoviews;" + cmd, callbacks, {silent : false}); + var kernel = IPython.notebook.kernel; + callbacks = {iopub: {output: $.proxy(callback, this)}}; + var cmd = "holoviews.plotting.widgets.NdWidget.widgets['" + this.id + "'].update(" + current + ")"; + kernel.execute("import holoviews;" + cmd, callbacks, {silent : false}); } else { - this.update(current); + this.update(current); } } HoloViewsWidget.prototype.update_cache = function(){ if(this.load_json) { - var frame_len = Object.keys(this.keyMap).length; + var frame_len = Object.keys(this.keyMap).length; } else { - var frame_len = Object.keys(this.frames).length; + var frame_len = Object.keys(this.frames).length; } for (var i=0; i').appendTo("#" + this.img_id).hide(); - var cache_id = this.img_id+"_"+frame; - this.cache[frame].attr("id", cache_id); - this.populate_cache(frame); - } + if(!this.load_json || this.dynamic) { + frame = Object.keys(this.frames)[i]; + } else { + frame = i; + } + if(!(frame in this.cache)) { + this.cache[frame] = $('
').appendTo("#" + this.img_id).hide(); + var cache_id = this.img_id+"_"+frame; + this.cache[frame].attr("id", cache_id); + this.populate_cache(frame); + } } } HoloViewsWidget.prototype.update = function(current){ if(current in this.cache) { - $.each(this.cache, function(index, value) { - value.hide(); - }); - this.cache[current].show(); + $.each(this.cache, function(index, value) { + value.hide(); + }); + this.cache[current].show(); } } @@ -86,7 +86,7 @@ function SelectionWidget(frames, id, slider_ids, keyMap, dim_vals, notFound, loa this.mode = mode; this.notFound = notFound; this.cached = cached; - this.dynamic = dynamic; + this.dynamic = dynamic; this.cache = {}; this.init_slider(this.current_vals[0]); } @@ -95,10 +95,10 @@ SelectionWidget.prototype = new HoloViewsWidget; SelectionWidget.prototype.set_frame = function(dim_val, dim_idx){ this.current_vals[dim_idx] = dim_val; - if(this.dynamic) { - this.dynamic_update(this.current_vals) - return; - } + if(this.dynamic) { + this.dynamic_update(this.current_vals) + return; + } var key = "("; for (var i=0; i Date: Wed, 30 Sep 2015 12:47:02 +0100 Subject: [PATCH 14/60] Added traverse method to DimensionedPlot --- holoviews/plotting/plot.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 1a42f89868..63d116de93 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -187,6 +187,40 @@ def _get_frame(self, key): pass + def matches(self, spec): + """ + Matches a specification against the current Plot. + """ + if callable(spec) and not isinstance(spec, type): return spec(self) + elif isinstance(spec, type): return isinstance(self, spec) + else: + raise ValueError("Matching specs have to be either a type or a callable.") + + + def traverse(self, fn=None, specs=None, full_breadth=True): + """ + Traverses any nested DimensionedPlot returning a list + of all plots that match the specs. The specs should + be supplied as a list of either Plot types or callables, + which should return a boolean given the plot class. + """ + accumulator = [] + matches = specs is None + if not matches: + for spec in specs: + matches = self.matches(spec) + if matches: break + if matches: + accumulator.append(fn(self) if fn else self) + + # Assumes composite objects are iterables + if hasattr(self, 'subplots') and self.subplots: + for el in self.subplots.values(): + accumulator += el.traverse(fn, specs, full_breadth) + if not full_breadth: break + return accumulator + + def _frame_title(self, key, group_size=2, separator='\n'): """ Returns the formatted dimension group strings From 393abfa80a1a798a58e953b91a9c747bbe84324f Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 30 Sep 2015 12:48:25 +0100 Subject: [PATCH 15/60] Added support for framewise ranges in Bokeh --- holoviews/plotting/bokeh/element.py | 21 +++++++++++++++++++++ holoviews/plotting/util.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 37a49573c2..72f00df058 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -171,6 +171,9 @@ def _axes_props(self, plots, subplots, element, ranges): else: l, b, r, t = self.get_extents(element, ranges) low, high = (b, t) if self.invert_axes else (l, r) + if low == high: + low -= 0.5 + high += 0.5 if all(x is not None for x in (low, high)): plot_ranges['x_range'] = [low, high] @@ -183,6 +186,9 @@ def _axes_props(self, plots, subplots, element, ranges): else: l, b, r, t = self.get_extents(element, ranges) low, high = (l, r) if self.invert_axes else (b, t) + if low == high: + low -= 0.5 + high += 0.5 if all(y is not None for y in (low, high)): plot_ranges['y_range'] = [low, high] if self.invert_yaxis: @@ -297,6 +303,20 @@ def _update_plot(self, key, plot, element=None): plot.yaxis[0].set(**props['y']) + def _update_ranges(self, element, ranges): + framewise = self.lookup_options(element, 'norm').options.get('framewise') + if framewise or self.dynamic: + return + plot = self.handles['plot'] + dims = element.dimensions(label=True) + xlow, xhigh = ranges.get(dims[0], element.range(0)) + ylow, yhigh = ranges.get(dims[1], element.range(1)) + plot.x_range.start = xlow + plot.x_range.end = xhigh + plot.y_range.start = ylow + plot.y_range.end = yhigh + + def _process_legend(self): """ Disables legends if show_legend is disabled. @@ -380,6 +400,7 @@ def update_frame(self, key, ranges=None, plot=None): source = self.handles['source'] data, mapping = self.get_data(element, ranges) self._update_datasource(source, data) + self._update_ranges(element, ranges) if not self.overlaid: self._update_plot(key, plot, element) diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 7065ec3c4b..0a2cc58420 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -34,7 +34,7 @@ def get_sideplot_ranges(plot, element, main, ranges): else: framewise = plot.lookup_options(range_item.last, 'norm').options.get('framewise') if framewise and range_item.get(key, False): - main_range = range_item.get(key, False).range(dim) + main_range = range_item[key].range(dim) else: main_range = range_item.range(dim) From d8599264a3c7829586e19f13d26db39f2e5026a9 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 30 Sep 2015 12:50:45 +0100 Subject: [PATCH 16/60] Bokeh Plots now handle which plot objects to update --- holoviews/plotting/bokeh/element.py | 13 +++++++++++++ holoviews/plotting/bokeh/plot.py | 9 +++++++++ holoviews/plotting/bokeh/renderer.py | 4 ++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 72f00df058..2bad1b397d 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -405,6 +405,19 @@ def update_frame(self, key, ranges=None, plot=None): self._update_plot(key, plot, element) + @property + def current_handles(self): + """ + Returns a list of the plot objects to update. + """ + plot = self.state + handles = [plot, self.handles['source']] + framewise = self.lookup_options(self.current_frame, 'norm').options.get('framewise') + if framewise or self.dynamic: + handles += [plot.x_range, plot.y_range] + return handles + + class BokehMPLWrapper(ElementPlot): """ diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index aee15f3442..c6d8b43af9 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -62,6 +62,15 @@ def state(self): return self.handles['plot'] + @property + def current_handles(self): + """ + Should return a list of plot objects that have changed and + should be updated. + """ + return [] + + def _fontsize(self, key, label='fontsize', common=True): """ Converts integer fontsizes to a string specifying diff --git a/holoviews/plotting/bokeh/renderer.py b/holoviews/plotting/bokeh/renderer.py index 809bc78850..2b257b2bd8 100644 --- a/holoviews/plotting/bokeh/renderer.py +++ b/holoviews/plotting/bokeh/renderer.py @@ -35,8 +35,8 @@ def __call__(self, obj, fmt=None): html = '
%s
' % html return html, {'file-ext':fmt, 'mime_type':MIME_TYPES[fmt]} elif fmt == 'json': - types = [DataSource, Figure] - plotobjects = [o for tp in types for o in plot.state.select({'type': tp})] + plotobjects = [h for handles in plot.traverse(lambda x: x.current_handles) + for h in handles] data = {} for plotobj in plotobjects: json = plotobj.vm_serialize(changed_only=True) From 66e5d50481468fcf7361afca38a14dce36b1759e Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 30 Sep 2015 12:51:21 +0100 Subject: [PATCH 17/60] Fixed DynamicMap handling of ranges (always framewise) --- holoviews/plotting/bokeh/element.py | 9 +++++---- holoviews/plotting/plot.py | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 2bad1b397d..09dfa3106e 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -305,12 +305,13 @@ def _update_plot(self, key, plot, element=None): def _update_ranges(self, element, ranges): framewise = self.lookup_options(element, 'norm').options.get('framewise') - if framewise or self.dynamic: + dims = element.dimensions() + dim_ranges = dims[0].range + dims[1].range + if framewise or self.dynamic or not None in dim_ranges: return plot = self.handles['plot'] - dims = element.dimensions(label=True) - xlow, xhigh = ranges.get(dims[0], element.range(0)) - ylow, yhigh = ranges.get(dims[1], element.range(1)) + xlow, xhigh = ranges.get(dims[0].name, element.range(0)) + ylow, yhigh = ranges.get(dims[1].name, element.range(1)) plot.x_range.start = xlow plot.x_range.end = xhigh plot.y_range.start = ylow diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 63d116de93..516de2da4b 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -281,7 +281,7 @@ def compute_ranges(self, obj, key, ranges): for group, (axiswise, framewise) in norm_opts.items(): if group in ranges: continue # Skip if ranges are already computed - elif not framewise: # Traverse to get all elements + elif not framewise and not self.dynamic: # Traverse to get all elements elements = obj.traverse(return_fn, [group]) elif key is not None: # Traverse to get elements for each frame elements = self._get_frame(key).traverse(return_fn, [group]) @@ -747,6 +747,7 @@ def __init__(self, layout, **params): range(self.cols))) dimensions, keys = traversal.unique_dimkeys(layout) dynamic = bool(layout.traverse(lambda x: x, [DynamicMap])) + dynamic = dynamic and not bool(layout.traverse(lambda x: x, [HoloMap])) uniform = traversal.uniform(layout) plotopts = self.lookup_options(layout, 'plot').options super(GenericLayoutPlot, self).__init__(keys=keys, dimensions=dimensions, From 5120cc76f45247f39e98328dccdcc57c7ae8bd71 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 30 Sep 2015 13:32:12 +0100 Subject: [PATCH 18/60] Added support for DynamicMaps in GridSpace plots --- holoviews/plotting/bokeh/plot.py | 12 +++++++----- holoviews/plotting/mpl/plot.py | 3 +++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index c6d8b43af9..47904a3d78 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -1,20 +1,19 @@ -import numpy as np - import param from bokeh.io import gridplot, vplot, hplot from bokeh.models import ColumnDataSource from bokeh.models.widgets import Panel, Tabs -from ...core import OrderedDict, CompositeOverlay, Element -from ...core import Store, Layout, AdjointLayout, NdLayout, Empty, GridSpace, HoloMap -from ...core.options import Compositor +from ...core import (OrderedDict, CompositeOverlay, DynamicMap, Store, Layout, + AdjointLayout, NdLayout, Empty, GridSpace, HoloMap) from ...core import traversal +from ...core.options import Compositor from ...core.util import basestring from ..plot import Plot, GenericCompositePlot, GenericLayoutPlot from .renderer import BokehRenderer from .util import layout_padding + class BokehPlot(Plot): """ Plotting baseclass for the Bokeh backends, implementing the basic @@ -92,6 +91,8 @@ def __init__(self, layout, ranges=None, keys=None, dimensions=None, layout_num=1, **params): if not isinstance(layout, GridSpace): raise Exception("GridPlot only accepts GridSpace.") + dynamic = bool(layout.traverse(lambda x: x, [DynamicMap])) + dynamic = dynamic and not bool(layout.traverse(lambda x: x, [HoloMap])) self.layout = layout self.rows, self.cols = layout.shape self.layout_num = layout_num @@ -102,6 +103,7 @@ def __init__(self, layout, ranges=None, keys=None, dimensions=None, params['uniform'] = traversal.uniform(layout) super(GridPlot, self).__init__(keys=keys, dimensions=dimensions, + dynamic=dynamic, **dict(extra_opts, **params)) self.subplots, self.layout = self._create_subplots(layout, ranges) diff --git a/holoviews/plotting/mpl/plot.py b/holoviews/plotting/mpl/plot.py index 9df3cdc2a6..1429a17a18 100644 --- a/holoviews/plotting/mpl/plot.py +++ b/holoviews/plotting/mpl/plot.py @@ -256,6 +256,8 @@ def __init__(self, layout, axis=None, create_axes=True, ranges=None, keys=None, dimensions=None, layout_num=1, **params): if not isinstance(layout, GridSpace): raise Exception("GridPlot only accepts GridSpace.") + dynamic = bool(layout.traverse(lambda x: x, [DynamicMap])) + dynamic = dynamic and not bool(layout.traverse(lambda x: x, [HoloMap])) self.layout = layout self.cols, self.rows = layout.shape self.layout_num = layout_num @@ -266,6 +268,7 @@ def __init__(self, layout, axis=None, create_axes=True, ranges=None, params['uniform'] = traversal.uniform(layout) super(GridPlot, self).__init__(keys=keys, dimensions=dimensions, + dynamic=dynamic, **dict(extra_opts, **params)) # Compute ranges layoutwise grid_kwargs = {} From 00c5ee44ecd34b414c6f9859fe95a31565874daa Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 30 Sep 2015 19:36:30 +0100 Subject: [PATCH 19/60] Updated widget test SHAs --- tests/testwidgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testwidgets.py b/tests/testwidgets.py index 9dabf5fadb..4961364d25 100644 --- a/tests/testwidgets.py +++ b/tests/testwidgets.py @@ -59,8 +59,8 @@ def test_scrubber_widget_2(self): def test_selection_widget_1(self): html = normalize(SelectionWidget(self.plot1, display_options={'figure_format': 'png'})()) - self.assertEqual(digest_data(html), 'd0fbc5439e8ea000b7a2a15f40e8d988dcc8723d960f4cf2eae4ad915a70984f') + self.assertEqual(digest_data(html), 'ca10c0ce18017eb09b98a0153d0262293c75f89f2091e9a74d105c21f0ad353e') def test_selection_widget_2(self): html = normalize(SelectionWidget(self.plot2, display_options={'figure_format': 'png'})()) - self.assertEqual(digest_data(html), '5949656f8c699951ce4b69ea25577732d62c45fc85702693858a1810c91ed3c8') + self.assertEqual(digest_data(html), 'aa8b2b03c168d74c4fa97bfef2b36e408304b46a37b4d9293f7f8606422db615') From 4a17d83e4a1ff672d652322dbe2aebf847c7fae8 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 30 Sep 2015 19:39:03 +0100 Subject: [PATCH 20/60] Fixes to imports in plotting module --- holoviews/plotting/mpl/plot.py | 11 +++++------ holoviews/plotting/plot.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/holoviews/plotting/mpl/plot.py b/holoviews/plotting/mpl/plot.py index 1429a17a18..e8231dbec9 100644 --- a/holoviews/plotting/mpl/plot.py +++ b/holoviews/plotting/mpl/plot.py @@ -7,15 +7,14 @@ from mpl_toolkits.mplot3d import Axes3D # pyflakes:ignore (For 3D plots) from matplotlib import pyplot as plt from matplotlib import gridspec, animation - import param -from ...core import OrderedDict, HoloMap, AdjointLayout, NdLayout,\ - GridSpace, Element, CompositeOverlay, Element3D, Empty, Collator + +from ...core import (OrderedDict, HoloMap, AdjointLayout, NdLayout, + GridSpace, Element, CompositeOverlay, Element3D, + Empty, Collator, DynamicMap) from ...core.options import Store, Compositor +from ...core.util import int_to_roman, int_to_alpha, basestring from ...core import traversal -from ...core.util import int_to_roman,\ - int_to_alpha, basestring - from ..plot import DimensionedPlot, GenericLayoutPlot, GenericCompositePlot from .renderer import MPLRenderer diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 516de2da4b..70afaf9702 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -17,7 +17,7 @@ from ..core.layout import Empty, NdLayout, Layout from ..core.options import Store, Compositor from ..core.spaces import HoloMap, DynamicMap -from ..element import Table, Annotation +from ..element import Table class Plot(param.Parameterized): From 259dfda9343d65bf0d369522e1e14f7058ec6f0d Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 01:57:03 +0100 Subject: [PATCH 21/60] Handle non-existent frames in bokeh --- holoviews/plotting/bokeh/bokehwidgets.js | 23 ++++++++++++++++------- holoviews/plotting/bokeh/element.py | 9 ++++++--- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/holoviews/plotting/bokeh/bokehwidgets.js b/holoviews/plotting/bokeh/bokehwidgets.js index d2a46bd661..52e36695e3 100644 --- a/holoviews/plotting/bokeh/bokehwidgets.js +++ b/holoviews/plotting/bokeh/bokehwidgets.js @@ -19,15 +19,24 @@ var BokehMethods = { }, this)); }, update : function(current){ - var data = this.frames[current]; - $.each(data, function(id, value) { - var ds = Bokeh.Collections(value.type).get(id); - if (ds != undefined) { - ds.set(value.data); - } - }); + if (current === undefined) { + var data = undefined; + } else { + var data = this.frames[current]; + } + if (data != undefined) { + $.each(data, function(id, value) { + var ds = Bokeh.Collections(value.type).get(id); + if (ds != undefined) { + ds.set(value.data); + } + }); + } }, dynamic_update : function(current){ + if (current === undefined) { + return + } if(this.dynamic) { current = JSON.stringify(current); } diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 09dfa3106e..e2e86c7a70 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -392,6 +392,8 @@ def update_frame(self, key, ranges=None, plot=None): """ element = self._get_frame(key) if not element: + source = self.handles['source'] + source.data = {k: [] for k in source.data} return ranges = self.compute_ranges(self.hmap, key, ranges) @@ -413,9 +415,10 @@ def current_handles(self): """ plot = self.state handles = [plot, self.handles['source']] - framewise = self.lookup_options(self.current_frame, 'norm').options.get('framewise') - if framewise or self.dynamic: - handles += [plot.x_range, plot.y_range] + if self.current_frame: + framewise = self.lookup_options(self.current_frame, 'norm').options.get('framewise') + if framewise or self.dynamic: + handles += [plot.x_range, plot.y_range] return handles From de98de29493aa944f64a27ddf00db0643e9ef564 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 04:04:05 +0100 Subject: [PATCH 22/60] Factored out function to get dynamic interval --- holoviews/plotting/bokeh/plot.py | 6 +++--- holoviews/plotting/mpl/plot.py | 6 ++---- holoviews/plotting/plot.py | 7 +++---- holoviews/plotting/util.py | 14 +++++++++++++- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index 47904a3d78..ff3ca4f437 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -4,12 +4,13 @@ from bokeh.models import ColumnDataSource from bokeh.models.widgets import Panel, Tabs -from ...core import (OrderedDict, CompositeOverlay, DynamicMap, Store, Layout, +from ...core import (OrderedDict, CompositeOverlay, Store, Layout, AdjointLayout, NdLayout, Empty, GridSpace, HoloMap) from ...core import traversal from ...core.options import Compositor from ...core.util import basestring from ..plot import Plot, GenericCompositePlot, GenericLayoutPlot +from ..util import get_dynamic_interval from .renderer import BokehRenderer from .util import layout_padding @@ -91,8 +92,7 @@ def __init__(self, layout, ranges=None, keys=None, dimensions=None, layout_num=1, **params): if not isinstance(layout, GridSpace): raise Exception("GridPlot only accepts GridSpace.") - dynamic = bool(layout.traverse(lambda x: x, [DynamicMap])) - dynamic = dynamic and not bool(layout.traverse(lambda x: x, [HoloMap])) + dynamic = get_dynamic_interval(layout) self.layout = layout self.rows, self.cols = layout.shape self.layout_num = layout_num diff --git a/holoviews/plotting/mpl/plot.py b/holoviews/plotting/mpl/plot.py index e8231dbec9..803acf5f8a 100644 --- a/holoviews/plotting/mpl/plot.py +++ b/holoviews/plotting/mpl/plot.py @@ -8,10 +8,9 @@ from matplotlib import pyplot as plt from matplotlib import gridspec, animation import param - from ...core import (OrderedDict, HoloMap, AdjointLayout, NdLayout, GridSpace, Element, CompositeOverlay, Element3D, - Empty, Collator, DynamicMap) + Empty, Collator) from ...core.options import Store, Compositor from ...core.util import int_to_roman, int_to_alpha, basestring from ...core import traversal @@ -255,8 +254,7 @@ def __init__(self, layout, axis=None, create_axes=True, ranges=None, keys=None, dimensions=None, layout_num=1, **params): if not isinstance(layout, GridSpace): raise Exception("GridPlot only accepts GridSpace.") - dynamic = bool(layout.traverse(lambda x: x, [DynamicMap])) - dynamic = dynamic and not bool(layout.traverse(lambda x: x, [HoloMap])) + dynamic = get_dynamic_interval(layout) self.layout = layout self.cols, self.rows = layout.shape self.layout_num = layout_num diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 70afaf9702..0b681c05c1 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -18,6 +18,7 @@ from ..core.options import Store, Compositor from ..core.spaces import HoloMap, DynamicMap from ..element import Table +from .util import get_dynamic_interval class Plot(param.Parameterized): @@ -397,7 +398,7 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, self.zorder = zorder self.cyclic_index = cyclic_index self.overlaid = overlaid - dynamic = isinstance(element, DynamicMap) + dynamic = element.interval if isinstance(element, DynamicMap) else None if not isinstance(element, (HoloMap, DynamicMap)): self.hmap = HoloMap(initial_items=(0, element), kdims=['Frame'], id=element.id) @@ -739,15 +740,13 @@ def __init__(self, layout, **params): raise ValueError("GenericLayoutPlot only accepts Layout objects.") if len(layout.values()) == 0: raise ValueError("Cannot display empty layout") - self.layout = layout self.subplots = {} self.rows, self.cols = layout.shape self.coords = list(product(range(self.rows), range(self.cols))) + dynamic = get_dynamic_interval(layout) dimensions, keys = traversal.unique_dimkeys(layout) - dynamic = bool(layout.traverse(lambda x: x, [DynamicMap])) - dynamic = dynamic and not bool(layout.traverse(lambda x: x, [HoloMap])) uniform = traversal.uniform(layout) plotopts = self.lookup_options(layout, 'plot').options super(GenericLayoutPlot, self).__init__(keys=keys, dimensions=dimensions, diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 0a2cc58420..f447cd871e 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -1,4 +1,4 @@ -from ..core import HoloMap, CompositeOverlay +from ..core import HoloMap, DynamicMap, CompositeOverlay from ..core.util import match_spec def compute_sizes(sizes, size_fn, scaling, base_size): @@ -45,3 +45,15 @@ def get_sideplot_ranges(plot, element, main, ranges): range_item = [ov for ov in range_item if dim in ov.dimensions('all', label=True)][0] return range_item, main_range, dim + + +def get_dynamic_interval(composite): + "Returns interval of dynamic map objects in given composite object" + dynamic_intervals = composite.traverse(lambda x: x.interval, [DynamicMap]) + if dynamic_intervals and not composite.traverse(lambda x: x, [HoloMap]): + if set(dynamic_intervals) > 1: + raise Exception("Cannot display DynamicMap objects with" + "different intervals") + return dynamic_intervals[0] + else: + return None From 8e0bf1c62c3096d9d61aeed10b7163baa026d100 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 04:04:40 +0100 Subject: [PATCH 23/60] Added support for 'open' interval DynamicMap in plotting --- holoviews/plotting/plot.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 0b681c05c1..fdee2b12a9 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -172,10 +172,11 @@ def __getitem__(self, frame): """ Get the state of the Plot for a given frame number. """ - if isinstance(frame, int) and frame > len(self): + if not self.dynamic == 'open' and isinstance(frame, int) and frame > len(self): self.warning("Showing last frame available: %d" % len(self)) if not self.drawn: self.handles['fig'] = self.initialize_plot() - if not isinstance(frame, tuple): frame = self.keys[frame] + if not self.dynamic == 'open' and not isinstance(frame, tuple): + frame = self.keys[frame] self.update_frame(frame) return self.state @@ -414,6 +415,8 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, def _get_frame(self, key): + if self.dynamic == 'open' and key > self.hmap.counter: + return next(self.hmap) if isinstance(key, int): key = self.hmap.keys()[min([key, len(self.hmap)-1])] From ac4f8ff3ad8b0a60626e345ece6525d960456600 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 04:05:53 +0100 Subject: [PATCH 24/60] Initializing DynamicMaps in 'open' mode, removed iter Iter causes unexpected behaviour in many places because HoloViews generally expects to be able to iterate over Elements in a HoloMap type. --- holoviews/core/spaces.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 3305dbc444..574d0fbec3 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -390,6 +390,8 @@ def __init__(self, initial_items=None, **params): "or values.") if not len(self): self[tuple(key)] + else: + next(self) def clone(self, data=None, shared_data=True, *args, **overrides): @@ -448,9 +450,6 @@ def next(self): self.counter += 1 return val - def __iter__(self): - return self if self.interval=='open' else iter(list(self.values())) - class GridSpace(UniformNdMapping): From cc66e8d56d1f90e3a0dba7aa6c67c631cf21e103 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 04:07:53 +0100 Subject: [PATCH 25/60] Added support for DynamicMap open mode in ScrubberWidget --- holoviews/plotting/widgets/__init__.py | 14 ++++++++++---- holoviews/plotting/widgets/jsscrubber.jinja | 2 +- holoviews/plotting/widgets/widgets.js | 21 +++++++++------------ 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/holoviews/plotting/widgets/__init__.py b/holoviews/plotting/widgets/__init__.py index 704f54c050..f7d8bc6f48 100644 --- a/holoviews/plotting/widgets/__init__.py +++ b/holoviews/plotting/widgets/__init__.py @@ -71,7 +71,9 @@ def __init__(self, plot, renderer=None, **params): self.plot = plot self.dimensions = plot.dimensions self.keys = plot.keys + self.dynamic = plot.dynamic + if self.dynamic: self.embed = False if renderer is None: self.renderer = plot.renderer.instance(dpi=self.display_options.get('dpi', 72)) else: @@ -100,11 +102,12 @@ def _get_data(self): cached = str(self.embed).lower() load_json = str(self.export_json).lower() mode = repr(self.renderer.mode) + dynamic = repr(self.dynamic) if self.dynamic else 'false' return dict(CDN=CDN, frames=self.get_frames(), delay=delay, server=self.server_url, cached=cached, load_json=load_json, mode=mode, id=self.id, Nframes=len(self.plot), widget_name=name, - widget_template=template) + widget_template=template, dynamic=dynamic) def render_html(self, data): @@ -143,7 +146,6 @@ def _plot_figure(self, idx): def update(self, key): - if self.dynamic: key = tuple(key) return self._plot_figure(key) @@ -258,5 +260,9 @@ def _get_data(self): Nwidget=self.mock_obj.ndims, dimensions=dimensions, key_data=key_data, widgets=widgets, init_dim_vals=init_dim_vals, - throttle=throttle, notFound=notfound_msg, - dynamic=str(self.dynamic).lower()) + throttle=throttle, notFound=notfound_msg) + + + def update(self, key): + if self.dynamic: key = tuple(key) + return self._plot_figure(key) diff --git a/holoviews/plotting/widgets/jsscrubber.jinja b/holoviews/plotting/widgets/jsscrubber.jinja index 355478eddc..32eea330e5 100644 --- a/holoviews/plotting/widgets/jsscrubber.jinja +++ b/holoviews/plotting/widgets/jsscrubber.jinja @@ -32,7 +32,7 @@ function create_widget() { setTimeout(function() { - anim{{ id }} = new {{ widget_name }}(frame_data, {{ Nframes }}, "{{ id }}", {{ delay }}, {{ load_json }}, {{ mode }}, {{ cached }}); + anim{{ id }} = new {{ widget_name }}(frame_data, {{ Nframes }}, "{{ id }}", {{ delay }}, {{ load_json }}, {{ mode }}, {{ cached }}, {{ dynamic }}); }, 0); } diff --git a/holoviews/plotting/widgets/widgets.js b/holoviews/plotting/widgets/widgets.js index c95f00c021..9b7a7eac48 100644 --- a/holoviews/plotting/widgets/widgets.js +++ b/holoviews/plotting/widgets/widgets.js @@ -124,7 +124,7 @@ SelectionWidget.prototype.set_frame = function(dim_val, dim_idx){ /* Define the ScrubberWidget class */ -function ScrubberWidget(frames, num_frames, id, interval, load_json, mode, cached){ +function ScrubberWidget(frames, num_frames, id, interval, load_json, mode, cached, dynamic){ this.img_id = "_anim_img" + id; this.slider_id = "_anim_slider" + id; this.loop_select_id = "_anim_loop_select" + id; @@ -133,6 +133,7 @@ function ScrubberWidget(frames, num_frames, id, interval, load_json, mode, cache this.interval = interval; this.current_frame = 0; this.direction = 0; + this.dynamic = dynamic; this.timer = null; this.load_json = load_json; this.mode = mode; @@ -156,6 +157,7 @@ ScrubberWidget.prototype.set_frame = function(frame){ } } + ScrubberWidget.prototype.get_loop_state = function(){ var button_group = document[this.loop_select_id].state; for (var i = 0; i < button_group.length; i++) { @@ -167,16 +169,12 @@ ScrubberWidget.prototype.get_loop_state = function(){ return undefined; } -ScrubberWidget.prototype.update = function(current){ - if(current in this.cache) { - $.each(this.cache, function(index, value) { - value.hide(); - }); - this.cache[current].show(); - } -} ScrubberWidget.prototype.next_frame = function() { + 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)); } @@ -205,9 +203,8 @@ ScrubberWidget.prototype.faster = function() { } ScrubberWidget.prototype.anim_step_forward = function() { - this.current_frame += 1; - if(this.current_frame < this.length){ - this.set_frame(this.current_frame); + if(this.current_frame < this.length || this.dynamic){ + this.next_frame(); }else{ var loop_state = this.get_loop_state(); if(loop_state == "loop"){ From 94e527217428885d256e5f0808f410d3fc1aac05 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 04:09:29 +0100 Subject: [PATCH 26/60] Updated ScrubberWidget unit test SHAs --- tests/testwidgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testwidgets.py b/tests/testwidgets.py index 4961364d25..2e0c47c849 100644 --- a/tests/testwidgets.py +++ b/tests/testwidgets.py @@ -51,11 +51,11 @@ def tearDown(self): def test_scrubber_widget_1(self): html = normalize(ScrubberWidget(self.plot1, display_options={'figure_format': 'png'})()) - self.assertEqual(digest_data(html), 'db71b98841b2f505b4854950bc751fc1025e863d27792436d7d3084f8e9ac5b6') + self.assertEqual(digest_data(html), '61d1b1a41e3a06899110c36e23c5db85d3dba064ca26470f932f2c1d6c3497b4') def test_scrubber_widget_2(self): html = normalize(ScrubberWidget(self.plot2, display_options={'figure_format': 'png'})()) - self.assertEqual(digest_data(html), '78bf66f657c82497d170e90ceeaca6712d1b9fb497a8ae3bee3fdb4e6c97e102') + self.assertEqual(digest_data(html), 'ce0d3917cf72e4ffc87f9b91afd5dfaa302263d818fd784f72ee4c0d8b1a3a40') def test_selection_widget_1(self): html = normalize(SelectionWidget(self.plot1, display_options={'figure_format': 'png'})()) From 8b005b96aa37d026b9a57a9fd30d0fc5a1c48cda Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 04:27:34 +0100 Subject: [PATCH 27/60] Handle titles appropriately in open mode --- holoviews/plotting/plot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index fdee2b12a9..a46ae17e6b 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -228,9 +228,11 @@ def _frame_title(self, key, group_size=2, separator='\n'): Returns the formatted dimension group strings for a particular frame. """ + if self.dynamic == 'open' and self.current_key: + key = self.current_key if self.layout_dimensions is not None: dimensions, key = zip(*self.layout_dimensions.items()) - elif not self.uniform or len(self) == 1 or self.subplot: + elif not self.dynamic and (not self.uniform or len(self) == 1 or self.subplot): return '' else: key = key if isinstance(key, tuple) else (key,) From f90d05d5a7fac4d6213f06fcd41e13ffe490f030 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 12:40:27 +0100 Subject: [PATCH 28/60] Fixed open mode in matplotlib widgets --- holoviews/plotting/mpl/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/mpl/widgets.py b/holoviews/plotting/mpl/widgets.py index 64a0dc46e0..93de600255 100644 --- a/holoviews/plotting/mpl/widgets.py +++ b/holoviews/plotting/mpl/widgets.py @@ -72,7 +72,7 @@ def _plot_figure(self, idx): def update(self, key): - if self.dynamic: + if self.dynamic == 'closed': key = tuple(key) if self.renderer.mode == 'nbagg': From 972afd9f3d144f4e0dca633a968d29f3bc597589 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 12:59:44 +0100 Subject: [PATCH 29/60] Fixed get_dynamic_interval function --- holoviews/plotting/mpl/plot.py | 1 + holoviews/plotting/util.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/mpl/plot.py b/holoviews/plotting/mpl/plot.py index 803acf5f8a..e49eb2e489 100644 --- a/holoviews/plotting/mpl/plot.py +++ b/holoviews/plotting/mpl/plot.py @@ -15,6 +15,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_interval from .renderer import MPLRenderer diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index f447cd871e..b72f985f7f 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -50,8 +50,8 @@ def get_sideplot_ranges(plot, element, main, ranges): def get_dynamic_interval(composite): "Returns interval of dynamic map objects in given composite object" dynamic_intervals = composite.traverse(lambda x: x.interval, [DynamicMap]) - if dynamic_intervals and not composite.traverse(lambda x: x, [HoloMap]): - if set(dynamic_intervals) > 1: + if dynamic_intervals and not composite.traverse(lambda x: x, ['HoloMap']): + if len(set(dynamic_intervals)) > 1: raise Exception("Cannot display DynamicMap objects with" "different intervals") return dynamic_intervals[0] From ece6abf655cdb0e77a6e7cc2b4e0b41f250a4c6d Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 13:07:15 +0100 Subject: [PATCH 30/60] Added support for open DynamicMap in Layout --- holoviews/plotting/plot.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index a46ae17e6b..5baadd6fc8 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -685,6 +685,7 @@ def _get_frame(self, key): Element. """ layout_frame = self.layout.clone(shared_data=False) + if not isinstance(key, tuple): key = (key,) nthkey_fn = lambda x: zip(tuple(x.name for x in x.kdims), list(x.data.keys())[min([key[0], len(x)-1])]) if key == self.current_key: @@ -693,7 +694,12 @@ def _get_frame(self, key): self.current_key = key for path, item in self.layout.items(): - if self.uniform: + if self.dynamic == 'open': + counts = item.traverse(lambda x: x.counter, (DynamicMap,)) + if key[0] > counts[0]: + item.traverse(lambda x: next(x), (DynamicMap,)) + dim_keys = item.traverse(nthkey_fn, (HoloMap,))[0] + elif self.uniform: dim_keys = zip([d.name for d in self.dimensions if d in item.dimensions('key')], key) else: From 08283cb6d8059c3bcb07de14c61bb07359feb007 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 13:07:32 +0100 Subject: [PATCH 31/60] Switching to appropriate widget type for dynamic automatically --- holoviews/ipython/display_hooks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/holoviews/ipython/display_hooks.py b/holoviews/ipython/display_hooks.py index 954aa6f9e6..9dba8248e4 100644 --- a/holoviews/ipython/display_hooks.py +++ b/holoviews/ipython/display_hooks.py @@ -79,6 +79,7 @@ def display_video(plot, renderer, holomap_format, dpi, fps, css, **kwargs): def display_widgets(plot, renderer, holomap_format, widget_mode, **kwargs): "Display widgets applicable to the specified element" isuniform = plot.uniform + dynamic = plot.dynamic islinear = bijective(plot.keys) if not isuniform and holomap_format == 'widgets': param.Parameterized.warning("%s is not uniform, falling back to scrubber widget." @@ -89,6 +90,9 @@ def display_widgets(plot, renderer, holomap_format, widget_mode, **kwargs): holomap_format = 'scrubber' if islinear or not isuniform else 'widgets' widget = 'scrubber' if holomap_format == 'scrubber' else 'selection' + if dynamic == 'open': widget = 'scrubber' + if dynamic == 'closed': widget = 'selection' + widget_cls = plot.renderer.widgets[widget] return widget_cls(plot, renderer=renderer, embed=(widget_mode == 'embed'), From 54b6a2fc93a279569ed20172e199a35c782022ae Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 23:07:08 +0100 Subject: [PATCH 32/60] Minor fix to Dimensioned.select Only create clone when no item is found --- holoviews/core/dimension.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 458ddf63a9..3f21af8474 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -676,8 +676,9 @@ def select(self, selection_specs=None, **kwargs): dim = sanitized.get(dim, dim) select[self.get_dimension_index(dim)] = val if self._deep_indexable: - selection = self.get(tuple(select), - self.clone(shared_data=False)) + selection = self.get(tuple(select), None) + if selection is None: + selection = self.clone(shared_data=False) else: selection = self[tuple(select)] else: From 39261eaf8e34097ea823669501112ce315e6b4a4 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 23:07:53 +0100 Subject: [PATCH 33/60] Only initialize open DynamicMap if empty --- holoviews/core/spaces.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 574d0fbec3..913d04902d 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -391,7 +391,8 @@ def __init__(self, initial_items=None, **params): if not len(self): self[tuple(key)] else: - next(self) + if not len(self): + next(self) def clone(self, data=None, shared_data=True, *args, **overrides): From 38f585f3beea961fdf1dd77cdcb74513cb7eddeb Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 23:09:12 +0100 Subject: [PATCH 34/60] Fixed DimensionedPlot._get_frame for open mode --- holoviews/plotting/plot.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 5baadd6fc8..362c6bc2eb 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -417,9 +417,23 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, def _get_frame(self, key): - if self.dynamic == 'open' and key > self.hmap.counter: - return next(self.hmap) - if isinstance(key, int): + if self.dynamic == 'open': + if isinstance(key, tuple): + frame = self.hmap[key] + elif key < self.hmap.counter: + key = self.hmap.keys()[key] + frame = self.hmap[key] + elif key >= self.hmap.counter: + frame = next(self.hmap) + key = self.hmap.keys()[-1] + if not isinstance(key, tuple): key = (key,) + if not key in self.keys: + self.keys.append(key) + self.current_frame = frame + self.current_key = key + return frame + + if not self.dynamic and isinstance(key, int): key = self.hmap.keys()[min([key, len(self.hmap)-1])] if key == self.current_key: @@ -685,6 +699,7 @@ def _get_frame(self, key): Element. """ layout_frame = self.layout.clone(shared_data=False) + keyisint = isinstance(key, int) if not isinstance(key, tuple): key = (key,) nthkey_fn = lambda x: zip(tuple(x.name for x in x.kdims), list(x.data.keys())[min([key[0], len(x)-1])]) @@ -695,10 +710,14 @@ def _get_frame(self, key): for path, item in self.layout.items(): if self.dynamic == 'open': - counts = item.traverse(lambda x: x.counter, (DynamicMap,)) - if key[0] > counts[0]: - item.traverse(lambda x: next(x), (DynamicMap,)) - dim_keys = item.traverse(nthkey_fn, (HoloMap,))[0] + if keyisint: + counts = item.traverse(lambda x: x.counter, (DynamicMap,)) + if key[0] >= counts[0]: + item.traverse(lambda x: next(x), (DynamicMap,)) + dim_keys = item.traverse(nthkey_fn, (DynamicMap,))[0] + else: + dim_keys = zip([d.name for d in self.dimensions + if d in item.dimensions('key')], key) elif self.uniform: dim_keys = zip([d.name for d in self.dimensions if d in item.dimensions('key')], key) From ffb5649a03ce31fc8a4935f097d55a5d6f085087 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 23:09:42 +0100 Subject: [PATCH 35/60] Open mode ScrubberWidget now handles StopIteration --- holoviews/plotting/bokeh/bokehwidgets.js | 3 +++ holoviews/plotting/mpl/mplwidgets.js | 3 +++ holoviews/plotting/widgets/widgets.js | 20 +++++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/bokehwidgets.js b/holoviews/plotting/bokeh/bokehwidgets.js index 52e36695e3..0917003920 100644 --- a/holoviews/plotting/bokeh/bokehwidgets.js +++ b/holoviews/plotting/bokeh/bokehwidgets.js @@ -43,6 +43,9 @@ var BokehMethods = { function callback(initialized, msg){ /* This callback receives data from Python as a string in order to parse it correctly quotes are sliced off*/ + if (msg.content.ename != undefined) { + this.process_error(msg); + } if (msg.msg_type != "execute_result") { console.log("Warning: HoloViews callback returned unexpected data for key: (", current, ") with the following content:", msg.content) return diff --git a/holoviews/plotting/mpl/mplwidgets.js b/holoviews/plotting/mpl/mplwidgets.js index 0e43f633be..344fba41a8 100644 --- a/holoviews/plotting/mpl/mplwidgets.js +++ b/holoviews/plotting/mpl/mplwidgets.js @@ -53,6 +53,9 @@ var MPLMethods = { function callback(msg){ /* This callback receives data from Python as a string in order to parse it correctly quotes are sliced off*/ + if (msg.content.ename != undefined) { + this.process_error(msg); + } if (msg.msg_type != "execute_result") { console.log("Warning: HoloViews callback returned unexpected data for key: (", current, ") with the following content:", msg.content) return diff --git a/holoviews/plotting/widgets/widgets.js b/holoviews/plotting/widgets/widgets.js index 9b7a7eac48..d11d5aeae8 100644 --- a/holoviews/plotting/widgets/widgets.js +++ b/holoviews/plotting/widgets/widgets.js @@ -22,6 +22,12 @@ HoloViewsWidget.prototype.populate_cache = function(idx){ } } +HoloViewsWidget.prototype.process_error = function(msg){ + +} + + + HoloViewsWidget.prototype.dynamic_update = function(current){ function callback(msg){ /* This callback receives data from Python as a string @@ -158,6 +164,18 @@ ScrubberWidget.prototype.set_frame = function(frame){ } +ScrubberWidget.prototype.process_error = function(msg){ + if (msg.content.ename === 'StopIteration') { + this.pause_animation(); + var keys = Object.keys(this.frames) + this.length = keys.length; + document.getElementById(this.slider_id).max = this.length-1; + document.getElementById(this.slider_id).value = this.length-1; + this.current_frame = this.length-1; + } +} + + ScrubberWidget.prototype.get_loop_state = function(){ var button_group = document[this.loop_select_id].state; for (var i = 0; i < button_group.length; i++) { @@ -173,7 +191,7 @@ ScrubberWidget.prototype.get_loop_state = function(){ ScrubberWidget.prototype.next_frame = function() { if (this.dynamic && this.current_frame + 1 >= this.length) { this.length += 1; - document.getElementById(this.slider_id).max = this.length -1 ; + document.getElementById(this.slider_id).max = this.length-1; } this.set_frame(Math.min(this.length - 1, this.current_frame + 1)); } From 6431843d0b7e7cd33b65b8b1b69ed422867cc62e Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 1 Oct 2015 23:30:41 +0100 Subject: [PATCH 36/60] Fixed titles for open mode Layouts --- holoviews/plotting/plot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 362c6bc2eb..0fd2b92580 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -718,6 +718,7 @@ def _get_frame(self, key): else: dim_keys = zip([d.name for d in self.dimensions if d in item.dimensions('key')], key) + self.current_key = tuple(k[1] for k in dim_keys) elif self.uniform: dim_keys = zip([d.name for d in self.dimensions if d in item.dimensions('key')], key) From bd94a4d9fd5710b7e4ea4049ee0539c4d9810dc9 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Fri, 2 Oct 2015 00:55:06 +0100 Subject: [PATCH 37/60] Fixed framewise ranges for bokeh --- holoviews/plotting/bokeh/element.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index e2e86c7a70..ec6eb8db8e 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -307,11 +307,13 @@ def _update_ranges(self, element, ranges): framewise = self.lookup_options(element, 'norm').options.get('framewise') dims = element.dimensions() dim_ranges = dims[0].range + dims[1].range - if framewise or self.dynamic or not None in dim_ranges: + if not framewise and not self.dynamic: return plot = self.handles['plot'] xlow, xhigh = ranges.get(dims[0].name, element.range(0)) ylow, yhigh = ranges.get(dims[1].name, element.range(1)) + if self.invert_axes: + xlow, xhigh, ylow, yhigh = ylow, yhigh, xlow, xhigh plot.x_range.start = xlow plot.x_range.end = xhigh plot.y_range.start = ylow From 78322493da0b2c307fbba856aa6722cdb8044146 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Fri, 2 Oct 2015 01:02:39 +0100 Subject: [PATCH 38/60] Removed unused import --- holoviews/core/element.py | 1 - 1 file changed, 1 deletion(-) diff --git a/holoviews/core/element.py b/holoviews/core/element.py index fd10a5ea2a..a91ebe019a 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -1,5 +1,4 @@ import operator -import inspect from itertools import groupby from numbers import Number import numpy as np From 1ff8fcef90cf346418f9474a0ad00ce53b77d7bf Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 7 Oct 2015 17:41:44 +0100 Subject: [PATCH 39/60] Handled plotting handles on bokeh OverlayPlot --- holoviews/plotting/bokeh/element.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index ec6eb8db8e..72fecf480f 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -549,6 +549,14 @@ def _process_legend(self): plot.legend[0].legends[:] = new_legends + @property + def current_handles(self): + """ + Overlays don't have their own plotting handles. + """ + return [] + + def _init_tools(self, element): """ Processes the list of tools to be supplied to the plot. From 9ae9922dd83b7af6991174878965c1c1e52f6b24 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 8 Oct 2015 14:27:32 +0100 Subject: [PATCH 40/60] Fixed range updating for bokeh OverlayPlot --- holoviews/plotting/bokeh/element.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 72fecf480f..4d14b7a6a6 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -405,8 +405,8 @@ def update_frame(self, key, ranges=None, plot=None): source = self.handles['source'] data, mapping = self.get_data(element, ranges) self._update_datasource(source, data) - self._update_ranges(element, ranges) if not self.overlaid: + self._update_ranges(element, ranges) self._update_plot(key, plot, element) @@ -415,8 +415,13 @@ def current_handles(self): """ Returns a list of the plot objects to update. """ + handles = [] + if 'source' in self.handles: + handles = [self.handles['source']] + if self.overlaid: + return handles plot = self.state - handles = [plot, self.handles['source']] + handles.append(plot) if self.current_frame: framewise = self.lookup_options(self.current_frame, 'norm').options.get('framewise') if framewise or self.dynamic: @@ -549,14 +554,6 @@ def _process_legend(self): plot.legend[0].legends[:] = new_legends - @property - def current_handles(self): - """ - Overlays don't have their own plotting handles. - """ - return [] - - def _init_tools(self, element): """ Processes the list of tools to be supplied to the plot. @@ -609,4 +606,5 @@ def update_frame(self, key, ranges=None): for subplot in self.subplots.values(): subplot.update_frame(key, ranges) if not self.overlaid and not self.tabs: + self._update_ranges(overlay, ranges) self._update_plot(key, self.handles['plot'], overlay) From 2e280b0e315a317af6850078b844bace91704f14 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 8 Oct 2015 14:29:34 +0100 Subject: [PATCH 41/60] Fixed zero range issues in bokeh plots --- holoviews/plotting/bokeh/element.py | 31 +++++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 4d14b7a6a6..24cb416f3a 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -172,8 +172,9 @@ def _axes_props(self, plots, subplots, element, ranges): l, b, r, t = self.get_extents(element, ranges) low, high = (b, t) if self.invert_axes else (l, r) if low == high: - low -= 0.5 - high += 0.5 + offset = low*0.1 if low else 0.5 + low -= offset + high += offset if all(x is not None for x in (low, high)): plot_ranges['x_range'] = [low, high] @@ -187,8 +188,9 @@ def _axes_props(self, plots, subplots, element, ranges): l, b, r, t = self.get_extents(element, ranges) low, high = (l, r) if self.invert_axes else (b, t) if low == high: - low -= 0.5 - high += 0.5 + offset = low*0.1 if low else 0.5 + low -= offset + high += offset if all(y is not None for y in (low, high)): plot_ranges['y_range'] = [low, high] if self.invert_yaxis: @@ -305,19 +307,26 @@ def _update_plot(self, key, plot, element=None): def _update_ranges(self, element, ranges): framewise = self.lookup_options(element, 'norm').options.get('framewise') + l, b, r, t = self.get_extents(element, ranges) dims = element.dimensions() dim_ranges = dims[0].range + dims[1].range if not framewise and not self.dynamic: return plot = self.handles['plot'] - xlow, xhigh = ranges.get(dims[0].name, element.range(0)) - ylow, yhigh = ranges.get(dims[1].name, element.range(1)) if self.invert_axes: - xlow, xhigh, ylow, yhigh = ylow, yhigh, xlow, xhigh - plot.x_range.start = xlow - plot.x_range.end = xhigh - plot.y_range.start = ylow - plot.y_range.end = yhigh + l, b, r, t = b, l, t, r + if l == r: + offset = abs(l*0.1 if l else 0.5) + l -= offset + r += offset + if b == t: + offset = abs(b*0.1 if b else 0.5) + b -= offset + t += offset + plot.x_range.start = l + plot.x_range.end = r + plot.y_range.start = b + plot.y_range.end = t def _process_legend(self): From af80e7a3c571012e9b409062f6e32cbf207ad234 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 14 Oct 2015 15:10:56 +0100 Subject: [PATCH 42/60] Added support for updating bokeh LinearColorMappers dynamically --- holoviews/plotting/bokeh/element.py | 18 ++++++++++++++++-- holoviews/plotting/bokeh/raster.py | 16 +++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 24cb416f3a..6ca9209d85 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -127,6 +127,11 @@ class ElementPlot(BokehPlot, GenericElementPlot): # ElementPlot _plot_method = None + # The plot objects to be updated on each frame + # Any entries should be existing keys in the handles + # instance attribute. + _update_handles = ['source', 'glyph'] + def __init__(self, element, plot=None, invert_axes=False, show_labels=['x', 'y'], **params): self.invert_axes = invert_axes @@ -388,6 +393,7 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): self.handles['glyph'] = glyph # Update plot, source and glyph + self._update_glyph(glyph, properties, mapping) if not self.overlaid: self._update_plot(key, plot, element) self._process_legend() @@ -414,6 +420,9 @@ def update_frame(self, key, ranges=None, plot=None): source = self.handles['source'] data, mapping = self.get_data(element, ranges) self._update_datasource(source, data) + if 'glyph' in self.handles: + properties = self._glyph_properties(plot, element, source, ranges) + self._update_glyph(self.handles['glyph'], properties, mapping) if not self.overlaid: self._update_ranges(element, ranges) self._update_plot(key, plot, element) @@ -425,10 +434,13 @@ def current_handles(self): Returns a list of the plot objects to update. """ handles = [] - if 'source' in self.handles: - handles = [self.handles['source']] + for handle in self._update_handles: + if handle in self.handles: + handles.append(self.handles[handle]) + if self.overlaid: return handles + plot = self.state handles.append(plot) if self.current_frame: @@ -531,6 +543,8 @@ class OverlayPlot(GenericOverlayPlot, ElementPlot): style_opts = legend_dimensions + line_properties + text_properties + _update_handles = ['source'] + def _process_legend(self): plot = self.handles['plot'] if not self.show_legend or len(plot.legend) >= 1: diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index 8279a25fd3..1e84d53b31 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -12,6 +12,7 @@ class RasterPlot(ElementPlot): style_opts = ['cmap'] _plot_method = 'image' + _update_handles = ['source', 'glyph', 'color_mapper'] def __init__(self, *args, **kwargs): super(RasterPlot, self).__init__(*args, **kwargs) @@ -41,10 +42,23 @@ def _glyph_properties(self, plot, element, source, ranges): low, high = ranges.get(val_dim) if 'cmap' in properties: palette = mplcmap_to_palette(properties.pop('cmap', None)) - properties['color_mapper'] = LinearColorMapper(palette, low=low, high=high) + cmap = LinearColorMapper(palette, low=low, high=high) + properties['color_mapper'] = cmap + if 'color_mapper' not in self.handles: + self.handles['color_mapper'] = cmap return properties + def _update_glyph(self, glyph, properties, mapping): + allowed_properties = glyph.properties() + cmap = properties.pop('color_mapper') + glyph.color_mapper.low = cmap.low + glyph.color_mapper.high = cmap.high + merged = dict(properties, **mapping) + glyph.set(**{k: v for k, v in merged.items() + if k in allowed_properties}) + + class RGBPlot(RasterPlot): style_opts = [] From cacc4355158352ee4df8b3d109cc22b73f17ae8d Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 14 Oct 2015 15:52:19 +0100 Subject: [PATCH 43/60] Fixed issue with order of updating bokeh plot objects --- holoviews/plotting/bokeh/raster.py | 2 +- holoviews/plotting/bokeh/renderer.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index 1e84d53b31..2ef2950cb8 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -12,7 +12,7 @@ class RasterPlot(ElementPlot): style_opts = ['cmap'] _plot_method = 'image' - _update_handles = ['source', 'glyph', 'color_mapper'] + _update_handles = ['color_mapper', 'source', 'glyph'] def __init__(self, *args, **kwargs): super(RasterPlot, self).__init__(*args, **kwargs) diff --git a/holoviews/plotting/bokeh/renderer.py b/holoviews/plotting/bokeh/renderer.py index 2b257b2bd8..73195f7458 100644 --- a/holoviews/plotting/bokeh/renderer.py +++ b/holoviews/plotting/bokeh/renderer.py @@ -1,4 +1,4 @@ -from ...core import Store, HoloMap +from ...core import Store, HoloMap, OrderedDict from ..renderer import Renderer, MIME_TYPES from .widgets import BokehScrubberWidget, BokehSelectionWidget @@ -37,7 +37,7 @@ def __call__(self, obj, fmt=None): elif fmt == 'json': plotobjects = [h for handles in plot.traverse(lambda x: x.current_handles) for h in handles] - data = {} + data = OrderedDict() for plotobj in plotobjects: json = plotobj.vm_serialize(changed_only=True) data[plotobj.ref['id']] = {'type': plotobj.ref['type'], From 095c46fad53c6289e80ae9f04c2539f925814047 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 21 Oct 2015 03:02:59 +0100 Subject: [PATCH 44/60] Support for plotting Overlays in DynamicMaps in matplotlib --- holoviews/plotting/mpl/element.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 167b339bb7..e812c8e753 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -375,7 +375,7 @@ def _finalize_ticks(self, axis, view, xticks, yticks, zticks): if tick_fontsize: axis.tick_params(**tick_fontsize) - def update_frame(self, key, ranges=None): + def update_frame(self, key, ranges=None, element=None): """ Set the plot(s) to the given frame number. Operates by manipulating the matplotlib objects held in the self._handles @@ -384,12 +384,15 @@ def update_frame(self, key, ranges=None): If n is greater than the number of available frames, update using the last available frame. """ - view = self._get_frame(key) - if view is not None: - self.set_param(**self.lookup_options(view, 'plot').options) + if element is None: element = self._get_frame(key) + else: + self.current_key = key + self.current_frame = element + if element is not None: + self.set_param(**self.lookup_options(element, 'plot').options) axis = self.handles['axis'] - axes_visible = view is not None or self.overlaid + axes_visible = element is not None or self.overlaid axis.xaxis.set_visible(axes_visible and self.xaxis) axis.yaxis.set_visible(axes_visible and self.yaxis) axis.patch.set_alpha(np.min([int(axes_visible), 1])) @@ -397,13 +400,13 @@ def update_frame(self, key, ranges=None): for hname, handle in self.handles.items(): hideable = hasattr(handle, 'set_visible') if hname not in ['axis', 'fig'] and hideable: - handle.set_visible(view is not None) - if view is None: + handle.set_visible(element is not None) + if element is None: return ranges = self.compute_ranges(self.hmap, key, ranges) if not self.adjoined: - ranges = util.match_spec(view, ranges) - axis_kwargs = self.update_handles(axis, view, key if view is not None else {}, ranges) + ranges = util.match_spec(element, ranges) + axis_kwargs = self.update_handles(axis, element, key if element is not None else {}, ranges) self._finalize_axis(key, ranges=ranges, **(axis_kwargs if axis_kwargs else {})) @@ -653,9 +656,10 @@ def update_frame(self, key, ranges=None): if self.projection == '3d': self.handles['axis'].clear() + overlay = self._get_frame(key) if self.dynamic else {} ranges = self.compute_ranges(self.hmap, key, ranges) - for plot in self.subplots.values(): - plot.update_frame(key, ranges) + for k, plot in self.subplots.items(): + plot.update_frame(key, ranges, overlay.get(k, None)) self._finalize_axis(key, ranges=ranges) From eeb6e5fef39a68f0daa07bf03638e50012f5e730 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 21 Oct 2015 03:03:29 +0100 Subject: [PATCH 45/60] Fixed minor bug in MPLWidget update function --- holoviews/plotting/mpl/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/mpl/widgets.py b/holoviews/plotting/mpl/widgets.py index 93de600255..525f8d5406 100644 --- a/holoviews/plotting/mpl/widgets.py +++ b/holoviews/plotting/mpl/widgets.py @@ -72,7 +72,7 @@ def _plot_figure(self, idx): def update(self, key): - if self.dynamic == 'closed': + if self.dynamic == 'closed' and not isinstance(key, int): key = tuple(key) if self.renderer.mode == 'nbagg': From d0ed9d3937c97082fa0e707db4d7183de744c462 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 21 Oct 2015 03:42:30 +0100 Subject: [PATCH 46/60] Fixed small bug in mpl OverlayPlot --- holoviews/plotting/mpl/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index e812c8e753..f5834034cb 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -652,7 +652,7 @@ def initialize_plot(self, ranges=None): return self._finalize_axis(key, ranges=ranges, title=self._format_title(key)) - def update_frame(self, key, ranges=None): + def update_frame(self, key, ranges=None, element=None): if self.projection == '3d': self.handles['axis'].clear() From abff7ef14ca55c80408a6d414250d55a821fa6a1 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 21 Oct 2015 05:20:44 +0100 Subject: [PATCH 47/60] Implemented dynamic updating of Overlays in bokeh backend --- holoviews/plotting/bokeh/element.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 6ca9209d85..9e686debf0 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -402,12 +402,16 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): return plot - def update_frame(self, key, ranges=None, plot=None): + def update_frame(self, key, ranges=None, plot=None, element=None): """ Updates an existing plot with data corresponding to the key. """ - element = self._get_frame(key) + if not element: + element = self._get_frame(key) + self.current_key = key + self.current_frame = element + if not element: source = self.handles['source'] source.data = {k: [] for k in source.data} @@ -516,7 +520,7 @@ def _render_plot(self, element, plot=None): return rgbplot.initialize_plot(plot=plot) - def update_frame(self, key, ranges=None): + def update_frame(self, key, ranges=None, element=None): element = self.get_frame(key) if key in self.hmap: self.mplplot.update_frame(key, ranges) @@ -619,15 +623,15 @@ def initialize_plot(self, ranges=None, plot=None, plots=None): return self.handles['plot'] - def update_frame(self, key, ranges=None): + def update_frame(self, key, ranges=None, element=None): """ Update the internal state of the Plot to represent the given key tuple (where integers represent frames). Returns this state. """ overlay = self._get_frame(key) - for subplot in self.subplots.values(): - subplot.update_frame(key, ranges) + for k, subplot in self.subplots.items(): + subplot.update_frame(key, ranges, element=overlay.get(k, None)) if not self.overlaid and not self.tabs: self._update_ranges(overlay, ranges) self._update_plot(key, self.handles['plot'], overlay) From b3ff3e81b7794d69ed3344867244b8d33a9bbd4c Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 21 Oct 2015 12:25:19 +0100 Subject: [PATCH 48/60] Added support for plotting nested overlays in DynamicMap --- holoviews/plotting/bokeh/element.py | 24 ++++++++++++++++++------ holoviews/plotting/mpl/element.py | 13 ++++++++++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 9e686debf0..ccbb7ae23c 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -407,8 +407,13 @@ def update_frame(self, key, ranges=None, plot=None, element=None): Updates an existing plot with data corresponding to the key. """ - if not element: - element = self._get_frame(key) + if not element: + if self.dynamic: + self.current_key = key + element = self.current_frame + else: + element = self._get_frame(key) + else: self.current_key = key self.current_frame = element @@ -417,6 +422,7 @@ def update_frame(self, key, ranges=None, plot=None, element=None): source.data = {k: [] for k in source.data} return + self.set_param(**self.lookup_options(element, 'plot').options) ranges = self.compute_ranges(self.hmap, key, ranges) ranges = util.match_spec(element, ranges) @@ -629,9 +635,15 @@ def update_frame(self, key, ranges=None, element=None): key tuple (where integers represent frames). Returns this state. """ - overlay = self._get_frame(key) + if element is None: + element = self._get_frame(key) + else: + self.current_frame = element + self.current_key = key + ranges = self.compute_ranges(self.hmap, key, ranges) + for k, subplot in self.subplots.items(): - subplot.update_frame(key, ranges, element=overlay.get(k, None)) + subplot.update_frame(key, ranges, element=element.get(k, None)) if not self.overlaid and not self.tabs: - self._update_ranges(overlay, ranges) - self._update_plot(key, self.handles['plot'], overlay) + self._update_ranges(element, ranges) + self._update_plot(key, self.handles['plot'], element) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index f5834034cb..260706ef41 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -384,10 +384,16 @@ def update_frame(self, key, ranges=None, element=None): If n is greater than the number of available frames, update using the last available frame. """ - if element is None: element = self._get_frame(key) + if not element: + if self.dynamic: + self.current_key = key + element = self.current_frame + else: + element = self._get_frame(key) else: self.current_key = key self.current_frame = element + if element is not None: self.set_param(**self.lookup_options(element, 'plot').options) axis = self.handles['axis'] @@ -656,10 +662,11 @@ def update_frame(self, key, ranges=None, element=None): if self.projection == '3d': self.handles['axis'].clear() - overlay = self._get_frame(key) if self.dynamic else {} + if element is None and self.dynamic: + element = self._get_frame(key) ranges = self.compute_ranges(self.hmap, key, ranges) for k, plot in self.subplots.items(): - plot.update_frame(key, ranges, overlay.get(k, None)) + plot.update_frame(key, ranges, element.get(k, None)) self._finalize_axis(key, ranges=ranges) From 635e56972b2920338c370fb210f086f8eaf0c2ea Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 21 Oct 2015 13:01:15 +0100 Subject: [PATCH 49/60] Minor fix for dynamic in mpl OverlayPlot --- holoviews/plotting/mpl/element.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 260706ef41..a8c9cf2604 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -662,8 +662,11 @@ def update_frame(self, key, ranges=None, element=None): if self.projection == '3d': self.handles['axis'].clear() - if element is None and self.dynamic: + if element is None: element = self._get_frame(key) + else: + self.current_frame = element + self.current_key = key ranges = self.compute_ranges(self.hmap, key, ranges) for k, plot in self.subplots.items(): plot.update_frame(key, ranges, element.get(k, None)) From 69fe67fcae6f7244e09044f486185f484c7c8e48 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Wed, 21 Oct 2015 13:21:03 +0100 Subject: [PATCH 50/60] Fixed bug not updating correctly when using dynamic --- holoviews/plotting/bokeh/element.py | 6 +++--- holoviews/plotting/mpl/element.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index ccbb7ae23c..3150f2cdc7 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -176,7 +176,7 @@ def _axes_props(self, plots, subplots, element, ranges): else: l, b, r, t = self.get_extents(element, ranges) low, high = (b, t) if self.invert_axes else (l, r) - if low == high: + if low == high and low is not None: offset = low*0.1 if low else 0.5 low -= offset high += offset @@ -192,7 +192,7 @@ def _axes_props(self, plots, subplots, element, ranges): else: l, b, r, t = self.get_extents(element, ranges) low, high = (l, r) if self.invert_axes else (b, t) - if low == high: + if low == high and low is not None: offset = low*0.1 if low else 0.5 low -= offset high += offset @@ -408,7 +408,7 @@ def update_frame(self, key, ranges=None, plot=None, element=None): to the key. """ if not element: - if self.dynamic: + if self.dynamic and self.overlaid: self.current_key = key element = self.current_frame else: diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index a8c9cf2604..f95e1f86ba 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -385,7 +385,7 @@ def update_frame(self, key, ranges=None, element=None): using the last available frame. """ if not element: - if self.dynamic: + if self.dynamic and self.overlaid: self.current_key = key element = self.current_frame else: From c47fb43e79afc3307cb5ad5ec5320a31e31cb693 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 19 Nov 2015 03:42:06 +0000 Subject: [PATCH 51/60] Fixed issue with dynamic ranges --- holoviews/plotting/plot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index d69c1e8f0a..439ab585de 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -287,6 +287,7 @@ def compute_ranges(self, obj, key, ranges): elements = [] # Skip if ranges are cached or already computed by a # higher-level container object. + framewise = framewise or self.dynamic if group in ranges and (not framewise or ranges is not self.ranges): continue elif not framewise: # Traverse to get all elements From f44dbc2af5a3ee59099507ab4c9ecdc40ceff1b6 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Thu, 19 Nov 2015 13:21:41 +0000 Subject: [PATCH 52/60] DynamicMap has been rewritten to use the new proposed API --- holoviews/core/spaces.py | 248 ++++++++++++++++++++++++------------- holoviews/plotting/plot.py | 2 +- holoviews/plotting/util.py | 2 +- 3 files changed, 163 insertions(+), 89 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 7c12bc9246..013a4d4a3c 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -2,6 +2,7 @@ import numpy as np import param +import types from . import traversal, util from .dimension import OrderedDict, Dimension, Dimensioned, ViewableElement @@ -329,89 +330,148 @@ def hist(self, num_bins=20, bin_range=None, adjoin=True, individually=True, **kw class DynamicMap(HoloMap): """ - A DynamicMap is a type of HoloMap where the elements are - dynamically generated by a generator or callable. A DynamicMap - supports two different interval modes 'closed' where the limits of - the parameter space is known ahead of time (as declared by the - ranges on the key dimensions) or 'open' which allows the continual - generation of elements (e.g as data output by a simulator over an - unbounded simulated time dimension). - - Open mode is defined by code using the next() interface (iterators - and generators) whereas closed mode is supported by callable - interfaces. + A DynamicMap is a type of HoloMap where the elements are dynamically + generated by a callback which may be either a callable or a + generator. A DynamicMap supports two different modes depending on + the type of callable supplied and the dimension declarations. + + The 'closed' mode is used when the limits of the parameter space are + known upon declaration (as specified by the ranges on the key + dimensions) or 'open' which allows the continual generation of + elements (e.g as data output by a simulator over an unbounded + simulated time dimension). + + Generators always imply open mode but a callable that has any key + dimension unbounded in any direction will also be in open + mode. Closed mode only applied to callables where all the key + dimensions are fully bounded. """ - gen = param.Parameter(doc=""" - The generator of the elements in the DynamicMap. In the simplest - case, this can be anything supporting next() such as a Python - generator or iterator: - - (Image(np.random.rand(25,25)) for i in range(10)) - - Objects with the next() interface may only be used in 'open' - mode as they accept no arguments. As the above example returns - only returns element, the key dimension is a simple integer - counter. + callback = param.Parameter(doc=""" + The callable or generator used to generate the elements. In the + simplest case where all key dimensions are bounded, this can be + a callable that accepts the key dimension values as arguments + (in the declared order) and returns the corresponding element. + + For open mode where there is an unbounded key dimension, the + return type can specify a key as well as element as the tuple + (key, element). If no key is supplied, a simple counter is used + instead. + + If the callback is a generator, open mode is used and next() is + simply called. If the callback is callable and in open mode, the + element counter value will be supplied as the single + argument. This can be used to avoid issues where multiple + elements in a Layout each call next() leading to uncontrolled + changes in simulator state (the counter can be used to indicate + simulation time across the layout). + """) + + cache_size = param.Integer(default=500, doc=""" + The number of entries to cache for fast access. This is an LRU + cache where the least recently used item is overwritten once + the cache is full.""") - If an integer counter is not appropriate, the generator may - return keys together with the elements as follows: + cache_interval = param.Integer(default=1, doc=""" + When the element counter modulo the cache_interval is zero, the + element will be cached and therefore accessible when casting to a + HoloMap. Applicable in open mode only.""") - ((i, Image(i*np.random.rand(25,25))) for i in np.linspace(0,1,10)) + def __init__(self, initial_items=None, **params): + super(DynamicMap, self).__init__(initial_items, **params) + self.counter = 0 + if self.callback is None: + raise Exception("A suitable callback must be " + "declared to create a DynamicMap") + + self.call_mode = self._validate_mode() + self.mode = 'closed' if self.call_mode == 'key' else 'open' + # Needed to initialize the plotting system + if self.call_mode == 'key': + self[self._initial_key()] + elif self.call_mode == 'counter': + self[self.counter] + self.counter += 1 + else: + next(self) - To support 'closed' mode, the output element must be a callable - that accepts the requested key.. - Note that in 'closed' mode only the element is to be returned as - the key is already known and that the callable is expected to be - a function in the mathematical sense; a particular key should - map to a unique element output. + def _initial_key(self): + """ + Construct an initial key for closed mode based on the lower + range bounds or values on the key dimensions. + """ + key = [] + for kdim in self.kdims: + if kdim.values: + key.append(kdim.values[0]) + elif kdim.range: + key.append(kdim.range[0]) + return tuple(key) - In 'both' mode, both the next() and callable interfaces must be - supported. - """) - interval = param.ObjectSelector(default='closed', - objects=['open', 'closed', 'both'], doc=""" - Whether the dynamic map operates on a closed interval (requests - to the supplied function are guaranteed to be within a known, - finite interval) or an open interval (for instance for a - simulation that can may run for an unknown length of time).""") + def _validate_mode(self): + """ + Check the key dimensions and callback to determine the calling mode. + """ + isgenerator = isinstance(self.callback, types.GeneratorType) + if isgenerator: + return 'generator' + # Any unbounded kdim (any direction) implies open mode + for kdim in self.kdims: + if (kdim.values) and kdim.range != (None,None): + raise Exception('Dimension cannot have both values and ranges.') + elif kdim.values: + continue + if None in kdim.range: + return 'counter' + return 'key' - cache_size = param.Integer(default=10, doc=""" - The number of entries to cache for fast access. This is an LRU - cache where the least recently used item is overwritten once - the cache is full.""") + def _validate_key(self, key): + """ + Make sure the supplied key values are within the bounds + specified by the corresponding dimension range and soft_range. + """ + key = util.wrap_tuple(key) + assert len(key) == len(self.kdims) + for ind, val in enumerate(key): + (minv, maxv) = self.kdims[ind].range + (mins, maxs) = self.kdims[ind].soft_range + if [minv, mins] != [None,None]: + min_thresh = min([el for el in [minv,mins] if el is not None]) + if val < min_thresh: + raise StopIteration("Key value %s below lower bound %s" + % (val, min_thresh)) + if [maxv, maxs] != [None,None]: + max_thresh = max([el for el in [maxv,maxs] if el is not None]) + if val >= max_thresh: + raise StopIteration("Key value %s above upper bound %s" + % (val, max_thresh)) - def __init__(self, initial_items=None, **params): - item_types = (list, dict, OrderedDict) - initial_items = initial_items if isinstance(initial_items, item_types) else [] - super(DynamicMap, self).__init__(initial_items, **params) + def _execute_callback(self, *args): + """ + Execute the callback, validating both the input key and output + key where applicable. + """ + if self.call_mode == 'key': + self._validate_key(args) # Validate input key - if self.gen is None: - raise Exception("A generator or generator function must be " - "supplied to declare a DynamicMap") + if self.call_mode == 'generator': + retval = self.callback.next() + else: + retval = self.callback(*args) - self.counter = 0 if self.interval == 'open' else None + if self.call_mode=='key': + return retval - if self.interval == 'closed': - key = [] - for kdim in self.kdims: - if kdim.values: - key.append(kdim.values[0]) - elif kdim.range: - key.append(kdim.range[0]) - else: - raise Exception("In closed interval mode all key " - "dimensions need specified ranges" - "or values.") - if not len(self): - self[tuple(key)] + if isinstance(retval, tuple): + self._validate_key(retval[0]) # Validated output key + return retval else: - if not len(self): - next(self) + self._validate_key((self.counter,)) + return (self.counter, retval) def clone(self, data=None, shared_data=True, *args, **overrides): @@ -425,53 +485,68 @@ def clone(self, data=None, shared_data=True, *args, **overrides): def reset(self): """ - Clear the cache and reset the counter to zero. + Return a cleared dynamic map with a cleared cached + and a reset counter. """ - self.counter = 0 if self.interval == 'open' else None + if self.call_mode == 'generator': + raise Exception("Cannot reset generators.") + self.counter = 0 self.data = OrderedDict() + return self def __getitem__(self, key): """ - Return an element for any key chosen key (in 'closed mode') or + Return an element for any key chosen key (in'closed mode') or for a previously generated key that is still in the cache - ('open mode') + (for one of the 'open' modes) """ try: - return super(DynamicMap,self).__getitem__(key) + retval = super(DynamicMap,self).__getitem__(key) + if isinstance(retval, DynamicMap): + return HoloMap(retval) + else: + return retval except KeyError as e: - if self.interval == 'open': + if self.mode == 'open' and len(self.data)>0: raise KeyError(str(e) + " Note: Cannot index outside " - "available cache in open interval mode.") - val = self.gen(key) - self.data[key if isinstance(key, tuple) else (key,)] = val + "available cache over an open interval.") + tuple_key = util.wrap_tuple(key) + val = self._execute_callback(*tuple_key) + if self.call_mode == 'counter': + val = val[1] + self.data[tuple_key] = val return val def next(self): """ - Interface for 'open' mode that mirrors the next() interface of - the supplied generator or iterator. Both a key and element are - required (if no key is explicitly supplied, the counter is - used instead). + Interface for 'open' mode. For generators, this simply calls the + next() method. For callables callback, the counter is supplied + as a single argument. """ - if self.interval == 'closed': - raise Exception("The next() method should only be called in open interval mode.") + if self.mode == 'closed': + raise Exception("The next() method should only be called in " + "one of the open modes.") + + args = () if self.call_mode == 'generator' else (self.counter,) + retval = self._execute_callback(*args) - retval = self.gen.next() (key, val) = (retval if isinstance(retval, tuple) - and len(retval) ==2 else (self.counter, retval)) + else (self.counter, retval)) - key = key if isinstance(key, tuple) else (key,) + key = util.wrap_tuple(key) if len(key) != len(self.key_dimensions): raise Exception("Generated key does not match the number of key dimensions") - self.data[key] = val + if (self.counter % self.cache_interval)==0: + self.data[key] = val self.counter += 1 return val + class GridSpace(UniformNdMapping): """ Grids are distinct from Layouts as they ensure all contained @@ -625,4 +700,3 @@ def _item_check(self, dim_vals, data): raise ValueError("HoloMaps dimensions must be consistent in %s." % type(self).__name__) NdMapping._item_check(self, dim_vals, data) - diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 439ab585de..e7bdefa3cc 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -410,7 +410,7 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, self.cyclic_index = cyclic_index self.overlaid = overlaid self.overlay_dims = overlay_dims - dynamic = element.interval if isinstance(element, DynamicMap) else None + dynamic = element.mode if isinstance(element, DynamicMap) else None if not isinstance(element, (HoloMap, DynamicMap)): self.hmap = HoloMap(initial_items=(0, element), kdims=['Frame'], id=element.id) diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index b72f985f7f..ce7dd6ef1c 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -49,7 +49,7 @@ def get_sideplot_ranges(plot, element, main, ranges): def get_dynamic_interval(composite): "Returns interval of dynamic map objects in given composite object" - dynamic_intervals = composite.traverse(lambda x: x.interval, [DynamicMap]) + dynamic_intervals = composite.traverse(lambda x: x.mode, [DynamicMap]) if dynamic_intervals and not composite.traverse(lambda x: x, ['HoloMap']): if len(set(dynamic_intervals)) > 1: raise Exception("Cannot display DynamicMap objects with" From 6069db82959d321a0d90e5bb7e0433f3fdcf5c1e Mon Sep 17 00:00:00 2001 From: jlstevens Date: Thu, 19 Nov 2015 13:51:06 +0000 Subject: [PATCH 53/60] DynamicMap now respects the cache_size parameter --- holoviews/core/spaces.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 013a4d4a3c..cf92c4da38 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -346,6 +346,7 @@ class DynamicMap(HoloMap): mode. Closed mode only applied to callables where all the key dimensions are fully bounded. """ + _sorted = False callback = param.Parameter(doc=""" The callable or generator used to generate the elements. In the @@ -515,10 +516,23 @@ def __getitem__(self, key): val = self._execute_callback(*tuple_key) if self.call_mode == 'counter': val = val[1] - self.data[tuple_key] = val + + self._cache(tuple_key, val) return val + def _cache(self, key, val): + """ + Request that a key/value pair be considered for caching. + """ + if self.mode == 'open' and (self.counter % self.cache_interval)!=0: + return + if len(self) >= self.cache_size: + first_key = self.data.iterkeys().next() + self.data.pop(first_key) + self.data[key] = val + + def next(self): """ Interface for 'open' mode. For generators, this simply calls the @@ -539,8 +553,7 @@ def next(self): if len(key) != len(self.key_dimensions): raise Exception("Generated key does not match the number of key dimensions") - if (self.counter % self.cache_interval)==0: - self.data[key] = val + self._cache(key, val) self.counter += 1 return val From f0dad2276fe6ae1b3db6fd2bcbff28ecc228d576 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 19 Nov 2015 18:56:48 +0000 Subject: [PATCH 54/60] Minor improvements to key range validation in DynamicMap --- holoviews/core/spaces.py | 18 ++++++++---------- holoviews/core/util.py | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index cf92c4da38..d2f67fe8ed 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -437,18 +437,16 @@ def _validate_key(self, key): key = util.wrap_tuple(key) assert len(key) == len(self.kdims) for ind, val in enumerate(key): - (minv, maxv) = self.kdims[ind].range - (mins, maxs) = self.kdims[ind].soft_range - if [minv, mins] != [None,None]: - min_thresh = min([el for el in [minv,mins] if el is not None]) - if val < min_thresh: + kdim = self.kdims[ind] + low, high = util.max_range([kdim.range, kdim.soft_range]) + if low is not np.NaN: + if val < low: raise StopIteration("Key value %s below lower bound %s" - % (val, min_thresh)) - if [maxv, maxs] != [None,None]: - max_thresh = max([el for el in [maxv,maxs] if el is not None]) - if val >= max_thresh: + % (val, low)) + if high is not np.NaN: + if val > high: raise StopIteration("Key value %s above upper bound %s" - % (val, max_thresh)) + % (val, high)) def _execute_callback(self, *args): diff --git a/holoviews/core/util.py b/holoviews/core/util.py index cb9122588c..a8e604758a 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -289,7 +289,7 @@ def max_range(ranges): try: with warnings.catch_warnings(): warnings.filterwarnings('ignore', r'All-NaN (slice|axis) encountered') - arr = np.array(ranges) + arr = np.array([r for r in ranges for v in r if v is not None]) if arr.dtype.kind == 'M': return arr[:, 0].min(), arr[:, 1].max() return (np.nanmin(arr[:, 0]), np.nanmax(arr[:, 1])) From 23fd27cfe4548013440889d10f4bf60e0dc9691f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 19 Nov 2015 18:57:50 +0000 Subject: [PATCH 55/60] Fix to dynamic plot frame access --- holoviews/plotting/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index e7bdefa3cc..23a608561e 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -426,7 +426,7 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, def _get_frame(self, key): - if self.dynamic == 'open': + if self.dynamic: if isinstance(key, tuple): frame = self.hmap[key] elif key < self.hmap.counter: From c3bb0b92ff6f42aed91daf54c785cf4b8f233090 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 19 Nov 2015 19:03:01 +0000 Subject: [PATCH 56/60] Handling of dynamic int and float types --- holoviews/plotting/widgets/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/widgets/__init__.py b/holoviews/plotting/widgets/__init__.py index f7d8bc6f48..e4860e74eb 100644 --- a/holoviews/plotting/widgets/__init__.py +++ b/holoviews/plotting/widgets/__init__.py @@ -216,9 +216,10 @@ def get_widgets(self): widget_type = 'dropdown' else: dim_vals = list(dim.range) + int_type = isinstance(dim.type, object) and issubclass(dim.type, np.int64) widget_type = 'slider' dim_range = dim_vals[1] - dim_vals[0] - if not isinstance(dim_range, int): + if not isinstance(dim_range, int) or int_type: step = 10**(round(math.log10(dim_range))-3) else: dim_vals = dim.values if dim.values else sorted(set(self.mock_obj.dimension_values(dim.name))) From 41b77eee8e69e7cc64fec67596a2b709fead2123 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 19 Nov 2015 19:05:42 +0000 Subject: [PATCH 57/60] Fixed merge issue in testwidgets --- tests/testwidgets.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/testwidgets.py b/tests/testwidgets.py index 9ce2605f51..1355de9910 100644 --- a/tests/testwidgets.py +++ b/tests/testwidgets.py @@ -63,8 +63,4 @@ def test_selection_widget_1(self): def test_selection_widget_2(self): html = normalize(SelectionWidget(self.plot2, display_options={'figure_format': 'png'})()) -<<<<<<< HEAD - self.assertEqual(digest_data(html), 'aa8b2b03c168d74c4fa97bfef2b36e408304b46a37b4d9293f7f8606422db615') -======= self.assertEqual(digest_data(html), '7af5adfdf8a30dbf98f699f462e817255f26bd19bb46dcea1626259054719dd4') ->>>>>>> master From 96a0f297b4d1e0d73662b58daa6b9a54cb9a201d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 19 Nov 2015 19:20:14 +0000 Subject: [PATCH 58/60] Fixed outdated hash in TestWidgets --- tests/testwidgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testwidgets.py b/tests/testwidgets.py index 1355de9910..33faa5895b 100644 --- a/tests/testwidgets.py +++ b/tests/testwidgets.py @@ -63,4 +63,4 @@ def test_selection_widget_1(self): def test_selection_widget_2(self): html = normalize(SelectionWidget(self.plot2, display_options={'figure_format': 'png'})()) - self.assertEqual(digest_data(html), '7af5adfdf8a30dbf98f699f462e817255f26bd19bb46dcea1626259054719dd4') + self.assertEqual(digest_data(html), 'd02b3701c3d90b7f4ad72253aa3225ab7bdd67d0aa6b59077e7f42872aea3c15') From e447f8bcda2aa78cf22b8ecbc794ae19af2e7ba9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 19 Nov 2015 19:57:50 +0000 Subject: [PATCH 59/60] Added support for defining list of numeric values to dynamic slider --- holoviews/plotting/widgets/__init__.py | 14 ++++++++++---- holoviews/plotting/widgets/jsslider.jinja | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/holoviews/plotting/widgets/__init__.py b/holoviews/plotting/widgets/__init__.py index e4860e74eb..1805b389de 100644 --- a/holoviews/plotting/widgets/__init__.py +++ b/holoviews/plotting/widgets/__init__.py @@ -212,13 +212,19 @@ def get_widgets(self): step = 1 if self.dynamic: if dim.values: - dim_vals = dim.values - widget_type = 'dropdown' + if all(isnumeric(v) and not isinstance(v, basestring) + for v in dim.values): + dim_vals = {i: v for i, v in enumerate(dim.values)} + widget_type = 'slider' + else: + dim_vals = dim.values + widget_type = 'dropdown' else: - dim_vals = list(dim.range) + dim_vals = [] + vals = list(dim.range) int_type = isinstance(dim.type, object) and issubclass(dim.type, np.int64) widget_type = 'slider' - dim_range = dim_vals[1] - dim_vals[0] + dim_range = vals[1] - vals[0] if not isinstance(dim_range, int) or int_type: step = 10**(round(math.log10(dim_range))-3) else: diff --git a/holoviews/plotting/widgets/jsslider.jinja b/holoviews/plotting/widgets/jsslider.jinja index 5ad3cfc353..5067841a75 100644 --- a/holoviews/plotting/widgets/jsslider.jinja +++ b/holoviews/plotting/widgets/jsslider.jinja @@ -71,7 +71,7 @@ require(["jQueryUI", "underscore"], function(jUI, _){ if (noConflict) $.noConflict(true); var vals = {{ widget_data['vals'] }}; - if ({{ dynamic }}) { + if ({{ dynamic }} && vals.constructor !== Array) { var min = vals[0]; var max = vals[vals.length-1]; var step = {{ widget_data['step'] }}; @@ -87,7 +87,7 @@ step: step, value: min, slide: _.throttle(function(event, ui) { - if ({{ dynamic }}) { + if ({{ dynamic }} && vals.constructor === Array) { var dim_val = ui.value } else { var dim_val = vals[ui.value]; From 904b926282233bc3dd924cdb4b480a2e5d2c0bb7 Mon Sep 17 00:00:00 2001 From: philippjfr Date: Thu, 19 Nov 2015 20:03:31 +0000 Subject: [PATCH 60/60] Updated SelectionWidget SHA tests --- tests/testwidgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testwidgets.py b/tests/testwidgets.py index 33faa5895b..ed3345e4eb 100644 --- a/tests/testwidgets.py +++ b/tests/testwidgets.py @@ -59,8 +59,8 @@ def test_scrubber_widget_2(self): def test_selection_widget_1(self): html = normalize(SelectionWidget(self.plot1, display_options={'figure_format': 'png'})()) - self.assertEqual(digest_data(html), 'ca10c0ce18017eb09b98a0153d0262293c75f89f2091e9a74d105c21f0ad353e') + self.assertEqual(digest_data(html), '6783419ddf6c77fbff2f7cdf2afb632704337c2185fcb247491ef1c8d58fb778') def test_selection_widget_2(self): html = normalize(SelectionWidget(self.plot2, display_options={'figure_format': 'png'})()) - self.assertEqual(digest_data(html), 'd02b3701c3d90b7f4ad72253aa3225ab7bdd67d0aa6b59077e7f42872aea3c15') + self.assertEqual(digest_data(html), '8571ca63d1c6abe11d004dbb9432f6e7ce5dd4b60b8e6eff7081f21dcbf3baa8')