From b00888582cc561c8bf0430c90e8b3ed045246ca9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 18 Dec 2015 20:26:05 +0000 Subject: [PATCH 1/6] Split out get_overlay_spec function --- holoviews/core/util.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 057a5710cd..a09806e687 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -611,6 +611,14 @@ def walk_depth_first(name): (names_by_level.get(i, None) for i in itertools.count()))) +def get_overlay_spec(o, k, v): + """ + Gets the type.group.label + key spec from an Element in an Overlay. + """ + k = (wrap_tuple(k),) + return ((type(v).__name__, v.group, v.label) + k if len(o.kdims) else + (type(v).__name__,) + k) + def layer_sort(hmap): """ @@ -619,8 +627,7 @@ def layer_sort(hmap): """ orderings = {} for o in hmap: - okeys = [(type(v).__name__, v.group, v.label) + k if len(o.kdims) else - (type(v).__name__,) + k for k, v in o.data.items()] + okeys = [get_overlay_spec(o, k, v) for k, v in o.data.items()] if len(okeys) == 1 and not okeys[0] in orderings: orderings[okeys[0]] = [] else: From ea97e023a90de1a6c1990518a041e76452497a06 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 18 Dec 2015 20:27:05 +0000 Subject: [PATCH 2/6] Made GenericOverlayPlot compatible with DynamicMaps --- holoviews/plotting/plot.py | 65 +++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 7201b42605..47c73125cd 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -441,7 +441,10 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, def _get_frame(self, key): - if self.dynamic: + if isinstance(self.hmap, DynamicMap) and self.overlaid and self.current_frame: + self.current_key = key + return self.current_frame + elif self.dynamic: if isinstance(key, tuple): frame = self.hmap[key] elif key < self.hmap.counter: @@ -457,7 +460,7 @@ def _get_frame(self, key): self.current_key = key return frame - if not self.dynamic and isinstance(key, int): + if isinstance(key, int): key = self.hmap.keys()[min([key, len(self.hmap)-1])] if key == self.current_key: @@ -657,10 +660,10 @@ def _create_subplots(self, ranges): continue if self.hmap.type == Overlay: - style_key = (vmap.type.__name__,) + key + style_key = (vmap.type.__name__,) + (key,) else: if not isinstance(key, tuple): key = (key,) - style_key = group_fn(vmap) + key + style_key = group_fn(vmap) + (key,) group_key = style_key[:length] zorder = ordering.index(style_key) + zoffset cyclic_index = group_counter[group_key] @@ -688,8 +691,17 @@ def _create_subplots(self, ranges): def get_extents(self, overlay, ranges): extents = [] + items = overlay.items() for key, subplot in self.subplots.items(): - layer = overlay.data.get(key, False) + layer = overlay.data.get(key, None) + found = False + if isinstance(self.hmap, DynamicMap) and layer is None: + for i, (k, layer) in enumerate(items): + if isinstance(layer, subplot.hmap.type): + found = True + break + if not found: + layer = None if layer and subplot.apply_ranges: if isinstance(layer, CompositeOverlay): sp_ranges = ranges @@ -722,6 +734,49 @@ def _format_title(self, key, separator='\n'): return separator.join([title, dim_title]) + def dynamic_update(self, subplot, key, overlay, items): + """ + Function to assign layers in an Overlay to a new plot. + """ + layer = overlay.get(key, None) + if layer is None: + match_spec = util.get_overlay_spec(self.current_frame, + util.wrap_tuple(key), + subplot.current_frame) + specs = [(i, util.get_overlay_spec(overlay, util.wrap_tuple(k), el)) + for i, (k, el) in enumerate(items)] + idx = self.closest_match(match_spec, specs) + k, layer = items.pop(idx) + return layer + + + def closest_match(self, match, specs, depth=0): + new_specs = [] + match_lengths = [] + for i, spec in specs: + if spec[0] == match[0]: + new_specs.append((i, spec[1:])) + else: + match_length = max(i for i in range(len(match[0])) + if (isinstance(match[0], tuple) + and match[0][:i] == spec[0][:i]) + or (isinstance(match[0], util.basestring) + and match[0].startswith(spec[0][:i]))) + match_lengths.append((i, match_length, spec[0])) + if not new_specs: + if depth == 0: + raise Exception("No plot with matching type found, ensure " + "the first frame of the DynamicMap initializes " + "all required plots.") + else: + return sorted(match_lengths, key=lambda x: -x[1])[0][0] + elif new_specs == 1: + return new_specs[0][0] + else: + depth = depth+1 + return self.closest_match(match[1:], new_specs, depth) + + class GenericCompositePlot(DimensionedPlot): From 6ce7bf382408daafe938ac328a6e1fa8a091db23 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 18 Dec 2015 20:28:16 +0000 Subject: [PATCH 3/6] Fixed handling of dynamic Overlay in bokeh plots --- holoviews/plotting/bokeh/element.py | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 359461877c..65af8bc426 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -14,7 +14,7 @@ mpl = None import param -from ...core import Store, HoloMap, Overlay, CompositeOverlay +from ...core import Store, HoloMap, Overlay, CompositeOverlay, DynamicMap from ...core import util from ...element import RGB from ..plot import GenericElementPlot, GenericOverlayPlot @@ -441,19 +441,14 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): return plot - def update_frame(self, key, ranges=None, plot=None, element=None): + def update_frame(self, key, ranges=None, plot=None, element=None, empty=False): """ Updates an existing plot with data corresponding to the key. """ - if element is None: + reused = isinstance(self.hmap, DynamicMap) and self.overlaid + if not reused and element is None: element = self._get_frame(key) - if not element: - if self.dynamic and self.overlaid: - self.current_key = key - element = self.current_frame - else: - element = self._get_frame(key) else: self.current_key = key self.current_frame = element @@ -464,13 +459,12 @@ def update_frame(self, key, ranges=None, plot=None, element=None): return self.set_param(**self.lookup_options(element, 'plot').options) - ranges = self.compute_ranges(self.hmap, key, ranges) ranges = util.match_spec(element, ranges) self.current_ranges = ranges plot = self.handles['plot'] source = self.handles['source'] - empty = self.callbacks and self.callbacks.downsample + empty = (self.callbacks and self.callbacks.downsample) or empty data, mapping = self.get_data(element, ranges, empty) self._update_datasource(source, data) @@ -689,15 +683,21 @@ def update_frame(self, key, ranges=None, element=None): """ if element is None: element = self._get_frame(key) - ranges = self.compute_ranges(element, key, ranges) else: self.current_frame = element self.current_key = key - ranges = self.compute_ranges(self.hmap, key, ranges) + range_obj = element if isinstance(self.hmap, DynamicMap) else self.hmap + ranges = self.compute_ranges(range_obj, key, ranges) + + items = element.items() for k, subplot in self.subplots.items(): - el = element.get(k, None) if isinstance(element, CompositeOverlay) else None - subplot.update_frame(key, ranges, element=el) + empty = False + if isinstance(self.hmap, DynamicMap): + el = self.dynamic_update(subplot, k, element, items) + empty = el is None + subplot.update_frame(key, ranges, element=el, empty=empty) + if not self.overlaid and not self.tabs: self._update_ranges(element, ranges) self._update_plot(key, self.handles['plot'], element) From 4c06fe3f3d76603c5a5ac72bbc0230765bd11bd8 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 18 Dec 2015 20:37:48 +0000 Subject: [PATCH 4/6] Fixed handling of dynamic Overlays in matplotlib backend --- holoviews/plotting/mpl/element.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 5838d759d9..83e689da19 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -7,7 +7,7 @@ import param from ...core import util -from ...core import (OrderedDict, Collator, NdOverlay, HoloMap, +from ...core import (OrderedDict, Collator, NdOverlay, HoloMap, DynamicMap, CompositeOverlay, Element3D, Columns, NdElement) from ...element import Table, ItemTable, Raster from ..plot import GenericElementPlot, GenericOverlayPlot @@ -124,12 +124,12 @@ def _finalize_axis(self, key, title=None, ranges=None, xticks=None, yticks=None, When the number of the frame is supplied as n, this method looks up and computes the appropriate title, axis labels and axis bounds. """ - + element = self._get_frame(key) + self.current_frame = element axis = self.handles['axis'] if self.bgcolor: axis.set_axis_bgcolor(self.bgcolor) - element = self._get_frame(key) subplots = list(self.subplots.values()) if self.subplots else [] if self.zorder == 0 and key is not None: title = None if self.zorder > 0 else self._format_title(key) @@ -391,12 +391,9 @@ 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 not element: - if self.dynamic and self.overlaid: - self.current_key = key - element = self.current_frame - else: - element = self._get_frame(key) + reused = isinstance(self.hmap, DynamicMap) and self.overlaid + if not reused and element is None: + element = self._get_frame(key) else: self.current_key = key self.current_frame = element @@ -679,9 +676,16 @@ def update_frame(self, key, ranges=None, element=None): 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)) + + range_obj = element if isinstance(self.hmap, DynamicMap) else self.hmap + ranges = self.compute_ranges(range_obj, key, ranges) + + items = element.items() + for k, subplot in self.subplots.items(): + el = element.get(k, None) + if isinstance(self.hmap, DynamicMap): + el = self.dynamic_update(subplot, k, element, items) + subplot.update_frame(key, ranges, el) self._finalize_axis(key, ranges=ranges) From 53d9f58cf1c51a2cd67cffdc5be1288e5828000a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 18 Dec 2015 23:05:49 +0000 Subject: [PATCH 5/6] Fix for matching overlay keys correctly --- holoviews/core/util.py | 2 +- holoviews/plotting/plot.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index a09806e687..5d6022bd51 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -615,7 +615,7 @@ def get_overlay_spec(o, k, v): """ Gets the type.group.label + key spec from an Element in an Overlay. """ - k = (wrap_tuple(k),) + k = wrap_tuple(k) return ((type(v).__name__, v.group, v.label) + k if len(o.kdims) else (type(v).__name__,) + k) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 47c73125cd..5c441b7fa2 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -660,10 +660,10 @@ def _create_subplots(self, ranges): continue if self.hmap.type == Overlay: - style_key = (vmap.type.__name__,) + (key,) + style_key = (vmap.type.__name__,) + key else: if not isinstance(key, tuple): key = (key,) - style_key = group_fn(vmap) + (key,) + style_key = group_fn(vmap) + key group_key = style_key[:length] zorder = ordering.index(style_key) + zoffset cyclic_index = group_counter[group_key] @@ -757,11 +757,13 @@ def closest_match(self, match, specs, depth=0): if spec[0] == match[0]: new_specs.append((i, spec[1:])) else: - match_length = max(i for i in range(len(match[0])) - if (isinstance(match[0], tuple) - and match[0][:i] == spec[0][:i]) - or (isinstance(match[0], util.basestring) - and match[0].startswith(spec[0][:i]))) + if util.isnumber(match[0]) and util.isnumber(spec[0]): + match_length = -abs(match[0]-spec[0]) + elif all(isinstance(s[0], basestring) for s in [spec, match]): + match_length = max(i for i in range(len(match[0])) + if match[0].startswith(spec[0][:i])) + else: + match_length = 0 match_lengths.append((i, match_length, spec[0])) if not new_specs: if depth == 0: From 3bf77dc37561cc7f97e6840cb3c541978f8d6b02 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 19 Dec 2015 01:07:00 +0000 Subject: [PATCH 6/6] Factored out functions to find closest matching overlaid Element --- holoviews/plotting/bokeh/element.py | 13 ++++++-- holoviews/plotting/mpl/element.py | 10 +++++- holoviews/plotting/plot.py | 45 --------------------------- holoviews/plotting/util.py | 48 ++++++++++++++++++++++++++++- 4 files changed, 67 insertions(+), 49 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 65af8bc426..b9be5f4edd 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -18,6 +18,7 @@ from ...core import util from ...element import RGB from ..plot import GenericElementPlot, GenericOverlayPlot +from ..util import dynamic_update from .callbacks import Callbacks from .plot import BokehPlot from .renderer import old_bokeh @@ -694,10 +695,18 @@ def update_frame(self, key, ranges=None, element=None): for k, subplot in self.subplots.items(): empty = False if isinstance(self.hmap, DynamicMap): - el = self.dynamic_update(subplot, k, element, items) - empty = el is None + idx = dynamic_update(self, subplot, k, element, items) + empty = idx is None + if empty: + _, el = items.pop(idx) subplot.update_frame(key, ranges, element=el, empty=empty) + + if isinstance(self.hmap, DynamicMap) and items: + raise Exception("Some Elements returned by the dynamic callback " + "were not initialized correctly and could not be " + "rendered.") + if not self.overlaid and not self.tabs: 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 83e689da19..357b35e944 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -11,6 +11,7 @@ CompositeOverlay, Element3D, Columns, NdElement) from ...element import Table, ItemTable, Raster from ..plot import GenericElementPlot, GenericOverlayPlot +from ..util import dynamic_update from .plot import MPLPlot from .util import wrap_formatter @@ -684,9 +685,16 @@ def update_frame(self, key, ranges=None, element=None): for k, subplot in self.subplots.items(): el = element.get(k, None) if isinstance(self.hmap, DynamicMap): - el = self.dynamic_update(subplot, k, element, items) + idx = dynamic_update(self, subplot, k, element, items) + if idx is not None: + _, el = items.pop(idx) subplot.update_frame(key, ranges, el) + if isinstance(self.hmap, DynamicMap) and items: + raise Exception("Some Elements returned by the dynamic callback " + "were not initialized correctly and could not be " + "rendered.") + self._finalize_axis(key, ranges=ranges) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 5c441b7fa2..6af24a05da 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -734,51 +734,6 @@ def _format_title(self, key, separator='\n'): return separator.join([title, dim_title]) - def dynamic_update(self, subplot, key, overlay, items): - """ - Function to assign layers in an Overlay to a new plot. - """ - layer = overlay.get(key, None) - if layer is None: - match_spec = util.get_overlay_spec(self.current_frame, - util.wrap_tuple(key), - subplot.current_frame) - specs = [(i, util.get_overlay_spec(overlay, util.wrap_tuple(k), el)) - for i, (k, el) in enumerate(items)] - idx = self.closest_match(match_spec, specs) - k, layer = items.pop(idx) - return layer - - - def closest_match(self, match, specs, depth=0): - new_specs = [] - match_lengths = [] - for i, spec in specs: - if spec[0] == match[0]: - new_specs.append((i, spec[1:])) - else: - if util.isnumber(match[0]) and util.isnumber(spec[0]): - match_length = -abs(match[0]-spec[0]) - elif all(isinstance(s[0], basestring) for s in [spec, match]): - match_length = max(i for i in range(len(match[0])) - if match[0].startswith(spec[0][:i])) - else: - match_length = 0 - match_lengths.append((i, match_length, spec[0])) - if not new_specs: - if depth == 0: - raise Exception("No plot with matching type found, ensure " - "the first frame of the DynamicMap initializes " - "all required plots.") - else: - return sorted(match_lengths, key=lambda x: -x[1])[0][0] - elif new_specs == 1: - return new_specs[0][0] - else: - depth = depth+1 - return self.closest_match(match[1:], new_specs, depth) - - class GenericCompositePlot(DimensionedPlot): diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 331bc67590..82f6aa1b5e 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -2,7 +2,7 @@ from ..core import (HoloMap, DynamicMap, CompositeOverlay, Layout, GridSpace, NdLayout, Store) -from ..core.util import match_spec +from ..core.util import match_spec, is_number, wrap_tuple, get_overlay_spec def displayable(obj): @@ -159,3 +159,49 @@ def save_frames(obj, filename, fmt=None, backend=None, options=None): for i in range(len(plot)): plot.update(i) renderer.save(plot, '%s_%s' % (filename, i), fmt=fmt, options=options) + + +def dynamic_update(plot, subplot, key, overlay, items): + """ + Given a plot, subplot and dynamically generated (Nd)Overlay + find the closest matching Element for that plot. + """ + match_spec = get_overlay_spec(overlay, + wrap_tuple(key), + subplot.current_frame) + specs = [(i, get_overlay_spec(overlay, wrap_tuple(k), el)) + for i, (k, el) in enumerate(items)] + return closest_match(match_spec, specs) + + +def closest_match(match, specs, depth=0): + """ + Recursively iterates over type, group, label and overlay key, + finding the closest matching spec. + """ + new_specs = [] + match_lengths = [] + for i, spec in specs: + if spec[0] == match[0]: + new_specs.append((i, spec[1:])) + else: + if is_number(match[0]) and is_number(spec[0]): + match_length = -abs(match[0]-spec[0]) + elif all(isinstance(s[0], basestring) for s in [spec, match]): + match_length = max(i for i in range(len(match[0])) + if match[0].startswith(spec[0][:i])) + else: + match_length = 0 + match_lengths.append((i, match_length, spec[0])) + + if len(new_specs) == 1: + return new_specs[0][0] + elif new_specs: + depth = depth+1 + return closest_match(match[1:], new_specs, depth) + else: + if depth == 0 or not match_lengths: + return None + else: + return sorted(match_lengths, key=lambda x: -x[1])[0][0] +