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):