From c20fbec8baf407121e25a09e35d0c30f0e675c1d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 11 Jan 2018 01:09:59 +0000 Subject: [PATCH] Avoid rerendering of overlaid DynamicMaps with non-triggered streams --- holoviews/core/spaces.py | 5 +- holoviews/plotting/bokeh/element.py | 11 ++- holoviews/plotting/plot.py | 20 +++-- holoviews/plotting/util.py | 48 +++++++++++- tests/testplotutils.py | 110 +++++++++++++++++++++++++++- 5 files changed, 180 insertions(+), 14 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index f350937248..fb94d7ee16 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -570,10 +570,11 @@ def get_nested_dmaps(dmap): """ Get all DynamicMaps referenced by the supplied DynamicMap's callback. """ + if not isinstance(dmap, DynamicMap): + return [] dmaps = [dmap] for o in dmap.callback.inputs: - if isinstance(o, DynamicMap): - dmaps.extend(get_nested_dmaps(o)) + dmaps.extend(get_nested_dmaps(o)) return list(set(dmaps)) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 2f9ba7b65f..5d6d6bf57b 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1461,9 +1461,18 @@ def update_frame(self, key, ranges=None, element=None): if element and not self.overlaid and not self.tabs and not self.batched: self._update_ranges(element, ranges) - + + # Determine which stream (if any) triggered the update + triggering = [stream for stream in self.streams if stream._triggering] + for k, subplot in self.subplots.items(): el = None + + # Skip updates to subplots when its streams is not one of + # the streams that initiated the update + if triggering and all(s not in triggering for s in subplot.streams): + continue + # If in Dynamic mode propagate elements to subplots if isinstance(self.hmap, DynamicMap) and element: # In batched mode NdOverlay is passed to subplot directly diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 7794eac7f0..7810855272 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -22,7 +22,8 @@ from ..element import Table from .util import (get_dynamic_mode, initialize_unbounded, dim_axis_label, attach_streams, traverse_setter, get_nested_streams, - compute_overlayable_zorders, get_plot_frame) + compute_overlayable_zorders, get_plot_frame, + split_dmap_overlay) class Plot(param.Parameterized): @@ -565,7 +566,7 @@ class GenericElementPlot(DimensionedPlot): def __init__(self, element, keys=None, ranges=None, dimensions=None, batched=False, overlaid=0, cyclic_index=0, zorder=0, style=None, - overlay_dims={}, stream_sources=[], **params): + overlay_dims={}, stream_sources=[], streams=None, **params): self.zorder = zorder self.cyclic_index = cyclic_index self.overlaid = overlaid @@ -608,10 +609,7 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, super(GenericElementPlot, self).__init__(keys=keys, dimensions=dimensions, dynamic=dynamic, **dict(params, **plot_opts)) - streams = [] - if isinstance(self.hmap, DynamicMap): - streams = get_nested_streams(self.hmap) - self.streams = streams + self.streams = get_nested_streams(self.hmap) if streams is None else streams if self.top_level: self.comm = self.init_comm() self.traverse(lambda x: setattr(x, 'comm', self.comm)) @@ -850,6 +848,12 @@ def _create_subplots(self, ranges): self.batched = False keys, vmaps = self.hmap.split_overlays() + if isinstance(self.hmap, DynamicMap): + dmap_streams = [get_nested_streams(layer) for layer in + split_dmap_overlay(self.hmap)] + else: + dmap_streams = [None]*len(keys) + # Compute global ordering length = self.style_grouping group_fn = lambda x: (x.type.__name__, x.last.group, x.last.label) @@ -861,7 +865,7 @@ def _create_subplots(self, ranges): group_counter = Counter() subplots = OrderedDict() - for (key, vmap) in zip(keys, vmaps): + for (key, vmap, streams) in zip(keys, vmaps, dmap_streams): opts = {'overlaid': overlay_type} if self.hmap.type == Overlay: style_key = (vmap.type.__name__,) + key @@ -911,7 +915,7 @@ def _create_subplots(self, ranges): layout_dimensions=self.layout_dimensions, ranges=ranges, show_title=self.show_title, style=style, uniform=self.uniform, - fontsize=self.fontsize, + fontsize=self.fontsize, streams=streams, renderer=self.renderer, stream_sources=stream_sources, zorder=zorder, adjoined=self.adjoined, **passed_handles) diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 63d5698370..a8cd828a34 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -6,7 +6,7 @@ import param from ..core import (HoloMap, DynamicMap, CompositeOverlay, Layout, - Overlay, GridSpace, NdLayout, Store) + Overlay, GridSpace, NdLayout, Store, NdOverlay) from ..core.options import Cycle from ..core.spaces import get_nested_streams from ..core.util import (match_spec, is_number, wrap_tuple, basestring, @@ -174,6 +174,52 @@ def compute_overlayable_zorders(obj, path=[]): return zorder_map +def is_dynamic_overlay(dmap): + """ + Traverses a DynamicMap graph and determines if any components + were overlaid dynamically (i.e. by * on a DynamicMap). + """ + if not isinstance(dmap, DynamicMap): + return False + elif dmap.callback._is_overlay: + return True + else: + return any(is_dynamic_overlay(dm) for dm in dmap.callback.inputs) + + +def split_dmap_overlay(obj, depth=0): + """ + Splits a DynamicMap into the original component layers it was + constructed from by traversing the graph to search for dynamically + overlaid components (i.e. constructed by using * on a DynamicMap). + Useful for assigning subplots of an OverlayPlot the streams that + are responsible for driving their updates. Allows the OverlayPlot + to determine if a stream update should redraw a particular + subplot. + """ + layers = [] + if isinstance(obj, DynamicMap): + if issubclass(obj.type, NdOverlay) and not depth: + for v in obj.last.values(): + layers.append(obj) + elif issubclass(obj.type, Overlay): + if obj.callback.inputs and is_dynamic_overlay(obj): + for inp in obj.callback.inputs: + layers += split_dmap_overlay(inp, depth+1) + else: + for v in obj.last.values(): + layers.append(obj) + else: + layers.append(obj) + return layers + if isinstance(obj, Overlay): + for k, v in obj.items(): + layers.append(v) + else: + layers.append(obj) + return layers + + def initialize_dynamic(obj): """ Initializes all DynamicMap objects contained by the object diff --git a/tests/testplotutils.py b/tests/testplotutils.py index ce2b77aad6..0e9a886049 100644 --- a/tests/testplotutils.py +++ b/tests/testplotutils.py @@ -7,9 +7,12 @@ from holoviews.core.spaces import DynamicMap, HoloMap from holoviews.core.options import Store, Cycle from holoviews.element.comparison import ComparisonTestCase -from holoviews.element import Curve, Area, Points +from holoviews.element import (Image, Scatter, Curve, Text, Points, + Area, VectorField, HLine, Path) +from holoviews.operation import operation from holoviews.plotting.util import ( - compute_overlayable_zorders, get_min_distance, process_cmap) + compute_overlayable_zorders, get_min_distance, process_cmap, + initialize_dynamic, split_dmap_overlay) from holoviews.streams import PointerX try: @@ -317,6 +320,109 @@ def test_dynamic_compute_overlayable_zorders_three_deep_dynamic_layers_reduced_l self.assertNotIn(curve, sources[2]) +class TestSplitDynamicMapOverlay(ComparisonTestCase): + """ + Tests the split_dmap_overlay utility + """ + + def setUp(self): + self.dmap_element = DynamicMap(lambda: Image([])) + self.dmap_overlay = DynamicMap(lambda: Overlay([Curve([]), Points([])])) + self.dmap_ndoverlay = DynamicMap(lambda: NdOverlay({0: Curve([]), 1: Curve([])})) + self.element = Scatter([]) + self.el1, self.el2 = Path([]), HLine(0) + self.overlay = Overlay([self.el1, self.el2]) + self.ndoverlay = NdOverlay({0: VectorField([]), 1: VectorField([])}) + + def test_dmap_ndoverlay(self): + test = self.dmap_ndoverlay + initialize_dynamic(test) + layers = [self.dmap_ndoverlay, self.dmap_ndoverlay] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_overlay(self): + test = self.dmap_overlay + initialize_dynamic(test) + layers = [self.dmap_overlay, self.dmap_overlay] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_element_mul_dmap_overlay(self): + test = self.dmap_element * self.dmap_overlay + initialize_dynamic(test) + layers = [self.dmap_element, self.dmap_overlay, self.dmap_overlay] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_element_mul_dmap_ndoverlay(self): + test = self.dmap_element * self.dmap_ndoverlay + initialize_dynamic(test) + layers = [self.dmap_element, self.dmap_ndoverlay] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_element_mul_element(self): + test = self.dmap_element * self.element + initialize_dynamic(test) + layers = [self.dmap_element, self.element] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_element_mul_overlay(self): + test = self.dmap_element * self.overlay + initialize_dynamic(test) + layers = [self.dmap_element, self.el1, self.el2] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_element_mul_ndoverlay(self): + test = self.dmap_element * self.ndoverlay + initialize_dynamic(test) + layers = [self.dmap_element, self.ndoverlay] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_overlay_mul_dmap_ndoverlay(self): + test = self.dmap_overlay * self.dmap_ndoverlay + initialize_dynamic(test) + layers = [self.dmap_overlay, self.dmap_overlay, self.dmap_ndoverlay] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_overlay_mul_element(self): + test = self.dmap_overlay * self.element + initialize_dynamic(test) + layers = [self.dmap_overlay, self.dmap_overlay, self.element] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_overlay_mul_overlay(self): + test = self.dmap_overlay * self.overlay + initialize_dynamic(test) + layers = [self.dmap_overlay, self.dmap_overlay, self.el1, self.el2] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_all_combinations(self): + test = (self.dmap_overlay * self.element * self.dmap_ndoverlay * + self.overlay * self.dmap_element * self.ndoverlay) + initialize_dynamic(test) + layers = [self.dmap_overlay, self.dmap_overlay, self.element, + self.dmap_ndoverlay, self.el1, self.el2, self.dmap_element, + self.ndoverlay] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_overlay_operation_mul_dmap_ndoverlay(self): + mapped = operation(self.dmap_overlay) + test = mapped * self.dmap_ndoverlay + initialize_dynamic(test) + layers = [mapped, mapped, self.dmap_ndoverlay] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_overlay_linked_operation_mul_dmap_ndoverlay(self): + mapped = operation(self.dmap_overlay, link_inputs=True) + test = mapped * self.dmap_ndoverlay + initialize_dynamic(test) + layers = [mapped, mapped, self.dmap_ndoverlay] + self.assertEqual(split_dmap_overlay(test), layers) + + def test_dmap_overlay_linked_operation_mul_dmap_ndoverlay(self): + mapped = self.dmap_overlay.map(lambda x: x.get(0), Overlay) + test = mapped * self.element * self.dmap_ndoverlay + initialize_dynamic(test) + layers = [mapped, self.element, self.dmap_ndoverlay] + self.assertEqual(split_dmap_overlay(test), layers) class TestPlotColorUtils(ComparisonTestCase):