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..4e23c8a28c 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,50 @@ 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: + print spec, match, depth + 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] +