Repository: holoviz/holoviews
Pull request #6415 (2024-10-17 14:41:04)

Fixes to ensure streams are correctly mapped to plots
This fix addresses a tricky situation where nested overlays were not resolved correctly because it would simply keep recursing until it got to the last layer. This meant the resulting subplot(s) would not update. We now still do the recursing (though if I spent a lot more time on this we maybe wouldn't have to do so), but also record all the streams along the way.

Also fixes a separate bug where during zorder assignment we did not take into account DynamicMaps that had an (Nd)Overlay as input, which would confuse the zorder assignment and therefore meant that streams would be assigned to the wrong plot (or no plot at all). 

Fixes https://github.com/holoviz/holoviews/issues/5595
Fixes https://github.com/holoviz/holoviews/issues/5434

- [x] Add test

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/holoviz/holoviews/pull/6415?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 88.50%. Comparing base [(`f9c7f05`)](https://app.codecov.io/gh/holoviz/holoviews/commit/f9c7f05c4078cc7ebb571c6b5aa10c1cc2662962?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) to head [(`2582ec4`)](https://app.codecov.io/gh/holoviz/holoviews/commit/2582ec4f8316d79f77284dfc2ef8b08c02922907?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).
> Report is 8 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #6415      +/-   ##
==========================================
+ Coverage   88.48%   88.50%   +0.01%     
==========================================
  Files         323      323              
  Lines       68469    68599     +130     
==========================================
+ Hits        60588    60711     +123     
- Misses       7881     7888       +7     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/holoviz/holoviews/pull/6415?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).

philippjfr: >I was not able to see whether the OP example in #5434 is fixed as there seems to be some issues with the box edit tool, I can't create any selection.

The UX of drawing boxes has changed in Bokeh (in quite a non-obvious way). You have to hold down shift and then draw.
philippjfr: I'm going to merge this and follow up with tests in a bit as I have to get the RC out asap.

Patch:
diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py
index 444dd6812..34c121324 100644
--- a/holoviews/plotting/plot.py
+++ b/holoviews/plotting/plot.py
@@ -25,7 +25,7 @@ from ..core.layout import Empty, Layout, NdLayout
 from ..core.options import Compositor, SkipRendering, Store, lookup_options
 from ..core.overlay import CompositeOverlay, NdOverlay, Overlay
 from ..core.spaces import DynamicMap, HoloMap
-from ..core.util import isfinite, stream_parameters
+from ..core.util import isfinite, stream_parameters, unique_iterator
 from ..element import Graph, Table
 from ..selection import NoOpSelectionDisplay
 from ..streams import RangeX, RangeXY, RangeY, Stream
@@ -1817,8 +1817,8 @@ class GenericOverlayPlot(GenericElementPlot):
             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)]
+            dmap_streams = [streams+get_nested_streams(layer) for layer, streams in
+                            zip(*split_dmap_overlay(self.hmap))]
         else:
             dmap_streams = [None]*len(keys)
 
@@ -1830,6 +1830,8 @@ class GenericOverlayPlot(GenericElementPlot):
 
         subplots = {}
         for (key, vmap, streams) in zip(keys, vmaps, dmap_streams):
+            if streams:
+                streams = list(unique_iterator(streams))
             subplot = self._create_subplot(key, vmap, streams, ranges)
             if subplot is None:
                 continue
diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py
index 94f086645..1924d1e92 100644
--- a/holoviews/plotting/util.py
+++ b/holoviews/plotting/util.py
@@ -114,7 +114,7 @@ def isoverlay_fn(obj):
     """
     Determines whether object is a DynamicMap returning (Nd)Overlay types.
     """
-    return isinstance(obj, DynamicMap) and (isinstance(obj.last, CompositeOverlay))
+    return isinstance(obj, CompositeOverlay) or (isinstance(obj, DynamicMap) and (isinstance(obj.last, CompositeOverlay)))
 
 
 def overlay_depth(obj):
@@ -233,28 +233,35 @@ def split_dmap_overlay(obj, depth=0):
     to determine if a stream update should redraw a particular
     subplot.
     """
-    layers = []
+    layers, streams = [], []
     if isinstance(obj, DynamicMap):
         initialize_dynamic(obj)
         if issubclass(obj.type, NdOverlay) and not depth:
             for _ in obj.last.values():
                 layers.append(obj)
+                streams.append(obj.streams)
         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)
+                    split, sub_streams = split_dmap_overlay(inp, depth+1)
+                    layers += split
+                    streams += [s+obj.streams for s in sub_streams]
             else:
                 for _ in obj.last.values():
                     layers.append(obj)
+                    streams.append(obj.streams)
         else:
             layers.append(obj)
-        return layers
+            streams.append(obj.streams)
+        return layers, streams
     if isinstance(obj, Overlay):
         for _k, v in obj.items():
             layers.append(v)
+            streams.append([])
     else:
         layers.append(obj)
-    return layers
+        streams.append([])
+    return layers, streams
 
 
 def initialize_dynamic(obj):


Test patch:
diff --git a/holoviews/tests/plotting/test_plotutils.py b/holoviews/tests/plotting/test_plotutils.py
index 1651607db..4f77a53c2 100644
--- a/holoviews/tests/plotting/test_plotutils.py
+++ b/holoviews/tests/plotting/test_plotutils.py
@@ -202,6 +202,13 @@ class TestOverlayableZorders(ComparisonTestCase):
         self.assertIn(curve, sources[2])
         self.assertNotIn(ndoverlay, sources[2])
 
+    def test_dynamic_compute_overlayable_zorders_ndoverlays_as_input(self):
+        ndoverlay1 = NdOverlay({i: Area(range(10+i)) for i in range(2)}).apply(lambda el: el.get(0), dynamic=True)
+        ndoverlay2 = NdOverlay({i: Area((range(15, 25+i), range(10+i))) for i in range(2)}).apply(lambda el: el.get(0), dynamic=True)
+        combined = ndoverlay1*ndoverlay2
+        combined[()]
+        sources = compute_overlayable_zorders(combined)
+        assert len(sources) == 2
 
     def test_dynamic_compute_overlayable_zorders_mixed_dynamic_and_dynamic_ndoverlay_with_streams(self):
         ndoverlay = DynamicMap(lambda x: NdOverlay({i: Area(range(10+i)) for i in range(2)}),
@@ -364,61 +371,61 @@ class TestSplitDynamicMapOverlay(ComparisonTestCase):
         test = self.dmap_ndoverlay
         initialize_dynamic(test)
         layers = [self.dmap_ndoverlay, self.dmap_ndoverlay]
-        self.assertEqual(split_dmap_overlay(test), layers)
+        self.assertEqual(split_dmap_overlay(test)[0], 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)
+        self.assertEqual(split_dmap_overlay(test)[0], 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)
+        self.assertEqual(split_dmap_overlay(test)[0], 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)
+        self.assertEqual(split_dmap_overlay(test)[0], 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)
+        self.assertEqual(split_dmap_overlay(test)[0], 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)
+        self.assertEqual(split_dmap_overlay(test)[0], 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)
+        self.assertEqual(split_dmap_overlay(test)[0], 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)
+        self.assertEqual(split_dmap_overlay(test)[0], 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)
+        self.assertEqual(split_dmap_overlay(test)[0], 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)
+        self.assertEqual(split_dmap_overlay(test)[0], layers)
 
     def test_dmap_all_combinations(self):
         test = (self.dmap_overlay * self.element * self.dmap_ndoverlay *
@@ -427,28 +434,28 @@ class TestSplitDynamicMapOverlay(ComparisonTestCase):
         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)
+        self.assertEqual(split_dmap_overlay(test)[0], 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)
+        self.assertEqual(split_dmap_overlay(test)[0], 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)
+        self.assertEqual(split_dmap_overlay(test)[0], layers)
 
     def test_dmap_overlay_linked_operation_mul_dmap_element_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)
+        self.assertEqual(split_dmap_overlay(test)[0], layers)
 
 
 class TestPlotColorUtils(ComparisonTestCase):