diff --git a/holoviews/core/operation.py b/holoviews/core/operation.py index 55f0f847bf..d561a31822 100644 --- a/holoviews/core/operation.py +++ b/holoviews/core/operation.py @@ -103,6 +103,16 @@ class ElementOperation(Operation): first component is a Normalization.ranges list and the second component is Normalization.keys. """) + link_inputs = param.Boolean(default=False, doc=""" + If the operation is dynamic, whether or not linked streams + should be transferred from the operation inputs for backends + that support linked streams. + + For example if an operation is applied to a DynamicMap with an + RangeXY, this switch determines whether the corresponding + visualization should update this stream with range changes + originating from the newly generated axes.""") + streams = param.List(default=[], doc=""" List of streams that are applied if dynamic=True, allowing for dynamic interaction with the plot.""") @@ -139,8 +149,8 @@ def __call__(self, element, **params): processed = element.clone(grid_data) elif dynamic: from ..util import Dynamic - streams = getattr(self.p, 'streams', []) - processed = Dynamic(element, streams=streams, + processed = Dynamic(element, streams=self.p.streams, + link_inputs=self.p.link_inputs, operation=self, kwargs=params) elif isinstance(element, ViewableElement): processed = self._process(element) diff --git a/holoviews/core/overlay.py b/holoviews/core/overlay.py index 344e03b968..c5ae61119a 100644 --- a/holoviews/core/overlay.py +++ b/holoviews/core/overlay.py @@ -29,6 +29,7 @@ def dynamic_mul(*args, **kwargs): element = other[args] return self * element callback = Callable(dynamic_mul, inputs=[self, other]) + callback._is_overlay = True return other.clone(shared_data=False, callback=callback, streams=[]) if isinstance(other, UniformNdMapping) and not isinstance(other, CompositeOverlay): @@ -41,7 +42,6 @@ def dynamic_mul(*args, **kwargs): - class CompositeOverlay(ViewableElement, Composable): """ CompositeOverlay provides a common baseclass for Overlay classes. @@ -136,7 +136,16 @@ def __add__(self, other): def __mul__(self, other): - if not isinstance(other, ViewableElement): + if type(other).__name__ == 'DynamicMap': + from .spaces import Callable + def dynamic_mul(*args, **kwargs): + element = other[args] + return self * element + callback = Callable(dynamic_mul, inputs=[self, other]) + callback._is_overlay = True + return other.clone(shared_data=False, callback=callback, + streams=[]) + elif not isinstance(other, ViewableElement): raise NotImplementedError return Overlay.from_values([self, other]) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 2cf630a533..4c12263a83 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -117,29 +117,21 @@ def _dynamic_mul(self, dimensions, other, keys): map_obj = self if isinstance(self, DynamicMap) else other def dynamic_mul(*key, **kwargs): + key_map = {d.name: k for d, k in zip(dimensions, key)} layers = [] try: - if isinstance(self, DynamicMap): - safe_key = () if not self.kdims else key - _, self_el = util.get_dynamic_item(self, dimensions, safe_key) - if self_el is not None: - layers.append(self_el) - else: - layers.append(self[key]) + self_el = self.select(**key_map) if self.kdims else self[()] + layers.append(self_el) except KeyError: pass try: - if isinstance(other, DynamicMap): - safe_key = () if not other.kdims else key - _, other_el = util.get_dynamic_item(other, dimensions, safe_key) - if other_el is not None: - layers.append(other_el) - else: - layers.append(other[key]) + other_el = other.select(**key_map) if other.kdims else other[()] + layers.append(other_el) except KeyError: pass return Overlay(layers) callback = Callable(dynamic_mul, inputs=[self, other]) + callback._is_overlay = True if map_obj: return map_obj.clone(callback=callback, shared_data=False, kdims=dimensions, streams=[]) @@ -207,6 +199,7 @@ def dynamic_mul(*args, **kwargs): element = self[args] return element * other callback = Callable(dynamic_mul, inputs=[self, other]) + callback._is_overlay = True return self.clone(shared_data=False, callback=callback, streams=[]) items = [(k, v * other) for (k, v) in self.data.items()] @@ -413,7 +406,11 @@ class Callable(param.Parameterized): when composite objects such as Layouts are returned from the callback. This is required for building interactive, linked visualizations (for the backends that support them) when returning - Layouts, NdLayouts or GridSpace objects. + Layouts, NdLayouts or GridSpace objects. When chaining multiple + DynamicMaps into a pipeline, the link_inputs parameter declares + whether the visualization generated using this Callable will + inherit the linked streams. This parameter is used as a hint by + the applicable backend. The mapping should map from an appropriate key to a list of streams associated with the selected object. The appropriate key @@ -429,6 +426,16 @@ class Callable(param.Parameterized): The list of inputs the callable function is wrapping. Used to allow deep access to streams in chained Callables.""") + link_inputs = param.Boolean(default=True, doc=""" + If the Callable wraps around other DynamicMaps in its inputs, + determines whether linked streams attached to the inputs are + transferred to the objects returned by the Callable. + + For example the Callable wraps a DynamicMap with an RangeXY + stream, this switch determines whether the corresponding + visualization should update this stream with range changes + originating from the newly generated axes.""") + memoize = param.Boolean(default=True, doc=""" Whether the return value of the callable should be memoized based on the call arguments and any streams attached to the @@ -441,6 +448,8 @@ class Callable(param.Parameterized): def __init__(self, callable, **params): super(Callable, self).__init__(callable=callable, **params) self._memoized = {} + self._is_overlay = False + @property def argspec(self): @@ -687,10 +696,7 @@ def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): # Ensure the clone references this object to ensure # stream sources are inherited if clone.callback is self.callback: - clone.callback = self.callback.clone() - if self not in clone.callback.inputs: - with util.disable_constant(clone.callback): - clone.callback.inputs = clone.callback.inputs+[self] + clone.callback = clone.callback.clone(inputs=[self]) return clone @@ -1104,7 +1110,7 @@ def dynamic_hist(obj): adjoin=False, **kwargs) from ..util import Dynamic - hist = Dynamic(self, operation=dynamic_hist) + hist = Dynamic(self, link_inputs=False, operation=dynamic_hist) if adjoin: return self << hist else: diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 9d8e35c761..52e189fc19 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -264,6 +264,11 @@ class shade(ElementOperation): and any valid transfer function that accepts data, mask, nbins arguments.""") + link_inputs = param.Boolean(default=True, doc=""" + By default, the link_inputs parameter is set to True so that + when applying shade, backends that support linked streams + update RangeXY streams on the inputs of the shade operation.""") + @classmethod def concatenate(cls, overlay): """ @@ -395,6 +400,11 @@ class dynspread(ElementOperation): Higher values give more spreading, up to the max_px allowed.""") + link_inputs = param.Boolean(default=True, doc=""" + By default, the link_inputs parameter is set to True so that + when applying dynspread, backends that support linked streams + update RangeXY streams on the inputs of the dynspread operation.""") + @classmethod def uint8_to_uint32(cls, img): shape = img.shape diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index d73546557d..1763b8fb7d 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -500,7 +500,7 @@ def initialize(self): if cb_hash in self._callbacks: # Merge callbacks if another callback has already been attached cb = self._callbacks[cb_hash] - cb.streams += self.streams + cb.streams = list(set(cb.streams+self.streams)) for k, v in self.handle_ids.items(): cb.handle_ids[k].update(v) continue diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index e084cd1839..9dc0bc5afe 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -27,7 +27,7 @@ from ...element import RGB from ...streams import Stream, RangeXY, RangeX, RangeY from ..plot import GenericElementPlot, GenericOverlayPlot -from ..util import dynamic_update, get_sources, attach_streams +from ..util import dynamic_update, attach_streams from .plot import BokehPlot, TOOLS from .util import (mpl_to_bokeh, convert_datetime, update_plot, get_tab_title, bokeh_version, mplcmap_to_palette, py2js_tickformatter, @@ -190,9 +190,18 @@ def _construct_callbacks(self): Initializes any callbacks for streams which have defined the plotted object as a source. """ + if isinstance(self, OverlayPlot): + zorders = [] + elif self.batched: + zorders = list(range(self.zorder, self.zorder+len(self.hmap.last))) + else: + zorders = [self.zorder] + + if isinstance(self, OverlayPlot) and not self.batched: + sources = [] if not self.static or isinstance(self.hmap, DynamicMap): - sources = [(i, o) for i, o in get_sources(self.hmap) - if i in [None, self.zorder]] + sources = [(i, o) for i, inputs in self.stream_sources.items() + for o in inputs if i in zorders] else: sources = [(self.zorder, self.hmap.last)] cb_classes = set() @@ -208,6 +217,7 @@ def _construct_callbacks(self): cbs.append(cb(self, cb_streams, source)) return cbs + def _hover_opts(self, element): if self.batched: dims = list(self.hmap.last.kdims) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index f0996a4e30..53e556c684 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -21,7 +21,8 @@ from ..core.util import stream_parameters from ..element import Table from .util import (get_dynamic_mode, initialize_sampled, dim_axis_label, - attach_streams, traverse_setter, get_nested_streams) + attach_streams, traverse_setter, get_nested_streams, + compute_overlayable_zorders) class Plot(param.Parameterized): @@ -554,7 +555,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={}, **params): + overlay_dims={}, stream_sources=[], **params): self.zorder = zorder self.cyclic_index = cyclic_index self.overlaid = overlaid @@ -567,6 +568,11 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None, else: self.hmap = element + if overlaid: + self.stream_sources = stream_sources + else: + self.stream_sources = compute_overlayable_zorders(self.hmap) + plot_element = self.hmap.last if self.batched and not isinstance(self, GenericOverlayPlot): plot_element = [el for el in plot_element if el][-1] @@ -836,6 +842,7 @@ def _create_subplots(self, ranges): ordering = util.layer_sort(self.hmap) registry = Store.registry[self.renderer.backend] batched = self.batched and type(self.hmap.last) is NdOverlay + stream_sources = self.stream_sources if batched: batchedplot = registry.get(type(self.hmap.last.last)) if (batched and batchedplot and 'batched' in batchedplot._plot_methods and @@ -902,7 +909,7 @@ def _create_subplots(self, ranges): layout_dimensions=self.layout_dimensions, ranges=ranges, show_title=self.show_title, style=style, uniform=self.uniform, - renderer=self.renderer, + renderer=self.renderer, stream_sources=stream_sources, zorder=zorder, **passed_handles) if not isinstance(key, tuple): key = (key,) diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index f35534f491..4a057b13d9 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from collections import defaultdict import numpy as np import param @@ -7,7 +8,7 @@ Overlay, GridSpace, NdLayout, Store, Dataset) from ..core.spaces import get_nested_streams, Callable from ..core.util import (match_spec, is_number, wrap_tuple, basestring, - get_overlay_spec, unique_iterator) + get_overlay_spec, unique_iterator, unique_iterator) def displayable(obj): @@ -72,6 +73,91 @@ def collate(obj): raise Exception(undisplayable_info(obj)) +def isoverlay_fn(obj): + """ + Determines whether object is a DynamicMap returning (Nd)Overlay types. + """ + return isinstance(obj, DynamicMap) and (isinstance(obj.last, CompositeOverlay)) + + +def overlay_depth(obj): + """ + Computes the depth of a DynamicMap overlay if it can be determined + otherwise return None. + """ + if isinstance(obj, DynamicMap): + if isinstance(obj.last, CompositeOverlay): + return len(obj.last) + elif obj.last is None: + return None + return 1 + else: + return 1 + + +def compute_overlayable_zorders(obj, path=[]): + """ + Traverses an overlayable composite container to determine which + objects are associated with specific (Nd)Overlay layers by + z-order, making sure to take DynamicMap Callables into + account. Returns a mapping between the zorders of each layer and a + corresponding lists of objects. + + Used to determine which overlaid subplots should be linked with + Stream callbacks. + """ + path = path+[obj] + zorder_map = defaultdict(list) + + # Process non-dynamic layers + if not isinstance(obj, DynamicMap): + if isinstance(obj, CompositeOverlay): + for z, o in enumerate(obj): + zorder_map[z] = [o, obj] + else: + if obj not in zorder_map[0]: + zorder_map[0].append(obj) + return zorder_map + + isoverlay = isinstance(obj.last, CompositeOverlay) + isdynoverlay = obj.callback._is_overlay + if obj not in zorder_map[0] and not isoverlay: + zorder_map[0].append(obj) + depth = overlay_depth(obj) + + # Process the inputs of the DynamicMap callback + dmap_inputs = obj.callback.inputs if obj.callback.link_inputs else [] + for z, inp in enumerate(dmap_inputs): + if any(not (isoverlay_fn(p) or p.last is None) for p in path) and isoverlay_fn(inp): + # Skips branches of graph that collapse Overlay layers + # to avoid adding layers that have been reduced or removed + continue + + input_depth = overlay_depth(inp) + if depth is not None and input_depth is not None and depth < input_depth: + # Skips branch of graph where the number of elements in an + # overlay has been reduced + continue + + # Recurse into DynamicMap.callback.inputs and update zorder_map + z = z if isdynoverlay else 0 + deep_zorders = compute_overlayable_zorders(inp, path=path) + offset = max(zorder_map.keys()) + for dz, objs in deep_zorders.items(): + global_z = offset+dz+z + zorder_map[global_z] = list(unique_iterator(zorder_map[global_z]+objs)) + + # If object branches but does not declare inputs (e.g. user defined + # DynamicMaps returning (Nd)Overlay) add the items on the DynamicMap.last + found = any(isinstance(p, DynamicMap) and p.callback._is_overlay for p in path) + if found and isoverlay and not isdynoverlay: + offset = max(zorder_map.keys()) + for z, o in enumerate(obj.last): + if o not in zorder_map[offset+z]: + zorder_map[offset+z].append(o) + return zorder_map + + def initialize_dynamic(obj): """ Initializes all DynamicMap objects contained by the object @@ -311,31 +397,6 @@ def append_refresh(dmap): return obj.traverse(append_refresh, [DynamicMap]) -def get_sources(obj, index=None): - """ - Traverses Callable graph to resolve sources on - DynamicMap objects, returning a list of sources - indexed by the Overlay layer. - """ - layers = [(index, obj)] - if not isinstance(obj, DynamicMap) or not isinstance(obj.callback, Callable): - return layers - index = 0 if index is None else int(index) - for o in obj.callback.inputs: - if isinstance(o, Overlay): - layers.append((None, o)) - for i, o in enumerate(overlay): - layers.append((index+i, o)) - index += len(o) - elif isinstance(o, DynamicMap): - layers += get_sources(o, index) - index = layers[-1][0]+1 - else: - layers.append((index, o)) - index += 1 - return layers - - def traverse_setter(obj, attribute, value): """ Traverses the object and sets the supplied attribute on the diff --git a/holoviews/util.py b/holoviews/util.py index 32dd8ad229..e622070cd2 100644 --- a/holoviews/util.py +++ b/holoviews/util.py @@ -28,6 +28,16 @@ class Dynamic(param.ParameterizedFunction): kwargs = param.Dict(default={}, doc=""" Keyword arguments passed to the function.""") + link_inputs = param.Boolean(default=True, doc=""" + If Dynamic is applied to another DynamicMap, determines whether + linked streams attached to its Callable inputs are + transferred to the output of the utility. + + For example if the Dynamic utility is applied to a DynamicMap + with an RangeXY, this switch determines whether the + corresponding visualization should update this stream with + range changes originating from the newly generated axes.""") + shared_data = param.Boolean(default=False, doc=""" Whether the cloned DynamicMap will share the same cache.""") @@ -80,13 +90,15 @@ def dynamic_operation(*key, **kwargs): else: def dynamic_operation(*key, **kwargs): self.p.kwargs.update(kwargs) - _, el = util.get_dynamic_item(map_obj, map_obj.kdims, key) - return self._process(el, key) + safe_key = () if not map_obj.kdims else key + return self._process(map_obj[key], key) if isinstance(self.p.operation, ElementOperation): return OperationCallable(dynamic_operation, inputs=[map_obj], + link_inputs=self.p.link_inputs, operation=self.p.operation) else: - return Callable(dynamic_operation, inputs=[map_obj]) + return Callable(dynamic_operation, inputs=[map_obj], + link_inputs=self.p.link_inputs) def _make_dynamic(self, hmap, dynamic_fn): diff --git a/tests/testplotutils.py b/tests/testplotutils.py index fe170ff489..b426ab74a1 100644 --- a/tests/testplotutils.py +++ b/tests/testplotutils.py @@ -1,7 +1,11 @@ from unittest import SkipTest +from holoviews import NdOverlay, Overlay +from holoviews.core.spaces import DynamicMap from holoviews.core.options import Store from holoviews.element.comparison import ComparisonTestCase +from holoviews.element import Curve, Area +from holoviews.plotting.util import compute_overlayable_zorders try: from holoviews.plotting.bokeh import util @@ -10,6 +14,246 @@ bokeh_renderer = None +class TestPlotUtils(ComparisonTestCase): + + def test_dynamic_compute_overlayable_zorders_two_mixed_layers(self): + area = Area(range(10)) + dmap = DynamicMap(lambda: Curve(range(10)), kdims=[]) + combined = area*dmap + combined[()] + sources = compute_overlayable_zorders(combined) + self.assertEqual(sources[0], [area]) + self.assertEqual(sources[1], [dmap]) + + def test_dynamic_compute_overlayable_zorders_two_mixed_layers_reverse(self): + area = Area(range(10)) + dmap = DynamicMap(lambda: Curve(range(10)), kdims=[]) + combined = dmap*area + combined[()] + sources = compute_overlayable_zorders(combined) + self.assertEqual(sources[0], [dmap]) + self.assertEqual(sources[1], [area]) + + def test_dynamic_compute_overlayable_zorders_two_dynamic_layers(self): + area = DynamicMap(lambda: Area(range(10)), kdims=[]) + dmap = DynamicMap(lambda: Curve(range(10)), kdims=[]) + combined = area*dmap + combined[()] + sources = compute_overlayable_zorders(combined) + self.assertEqual(sources[0], [area]) + self.assertEqual(sources[1], [dmap]) + + def test_dynamic_compute_overlayable_zorders_two_deep_dynamic_layers(self): + area = DynamicMap(lambda: Area(range(10)), kdims=[]) + curve = DynamicMap(lambda: Curve(range(10)), kdims=[]) + area_redim = area.redim(x='x2') + curve_redim = curve.redim(x='x2') + combined = area_redim*curve_redim + combined[()] + sources = compute_overlayable_zorders(combined) + self.assertIn(area_redim, sources[0]) + self.assertIn(area, sources[0]) + self.assertNotIn(curve_redim, sources[0]) + self.assertNotIn(curve, sources[0]) + self.assertIn(curve_redim, sources[1]) + self.assertIn(curve, sources[1]) + self.assertNotIn(area_redim, sources[1]) + self.assertNotIn(area, sources[1]) + + def test_dynamic_compute_overlayable_zorders_three_deep_dynamic_layers(self): + area = DynamicMap(lambda: Area(range(10)), kdims=[]) + curve = DynamicMap(lambda: Curve(range(10)), kdims=[]) + curve2 = DynamicMap(lambda: Curve(range(10)), kdims=[]) + area_redim = area.redim(x='x2') + curve_redim = curve.redim(x='x2') + curve2_redim = curve2.redim(x='x3') + combined = area_redim*curve_redim + combined1 = (combined*curve2_redim) + combined1[()] + sources = compute_overlayable_zorders(combined1) + self.assertIn(area_redim, sources[0]) + self.assertIn(area, sources[0]) + self.assertNotIn(curve_redim, sources[0]) + self.assertNotIn(curve, sources[0]) + self.assertNotIn(curve2_redim, sources[0]) + self.assertNotIn(curve2, sources[0]) + + self.assertIn(curve_redim, sources[1]) + self.assertIn(curve, sources[1]) + self.assertNotIn(area_redim, sources[1]) + self.assertNotIn(area, sources[1]) + self.assertNotIn(curve2_redim, sources[1]) + self.assertNotIn(curve2, sources[1]) + + self.assertIn(curve2_redim, sources[2]) + self.assertIn(curve2, sources[2]) + self.assertNotIn(area_redim, sources[2]) + self.assertNotIn(area, sources[2]) + self.assertNotIn(curve_redim, sources[2]) + self.assertNotIn(curve, sources[2]) + + def test_dynamic_compute_overlayable_zorders_three_deep_dynamic_layers_cloned(self): + area = DynamicMap(lambda: Area(range(10)), kdims=[]) + curve = DynamicMap(lambda: Curve(range(10)), kdims=[]) + curve2 = DynamicMap(lambda: Curve(range(10)), kdims=[]) + area_redim = area.redim(x='x2') + curve_redim = curve.redim(x='x2') + curve2_redim = curve2.redim(x='x3') + combined = area_redim*curve_redim + combined1 = (combined*curve2_redim).redim(y='y2') + combined1[()] + sources = compute_overlayable_zorders(combined1) + + self.assertIn(area_redim, sources[0]) + self.assertIn(area, sources[0]) + self.assertNotIn(curve_redim, sources[0]) + self.assertNotIn(curve, sources[0]) + self.assertNotIn(curve2_redim, sources[0]) + self.assertNotIn(curve2, sources[0]) + + self.assertIn(curve_redim, sources[1]) + self.assertIn(curve, sources[1]) + self.assertNotIn(area_redim, sources[1]) + self.assertNotIn(area, sources[1]) + self.assertNotIn(curve2_redim, sources[1]) + self.assertNotIn(curve2, sources[1]) + + self.assertIn(curve2_redim, sources[2]) + self.assertIn(curve2, sources[2]) + self.assertNotIn(area_redim, sources[2]) + self.assertNotIn(area, sources[2]) + self.assertNotIn(curve_redim, sources[2]) + self.assertNotIn(curve, sources[2]) + + def test_dynamic_compute_overlayable_zorders_mixed_dynamic_and_non_dynamic_overlays_reverse(self): + area1 = Area(range(10)) + area2 = Area(range(10)) + overlay = area1 * area2 + curve = DynamicMap(lambda: Curve(range(10)), kdims=[]) + curve_redim = curve.redim(x='x2') + combined = curve_redim*overlay + combined[()] + sources = compute_overlayable_zorders(combined) + + self.assertIn(curve_redim, sources[0]) + self.assertIn(curve, sources[0]) + self.assertNotIn(overlay, sources[0]) + + self.assertIn(area1, sources[1]) + self.assertIn(overlay, sources[1]) + self.assertNotIn(curve_redim, sources[1]) + self.assertNotIn(curve, sources[1]) + + self.assertIn(area2, sources[2]) + self.assertIn(overlay, sources[2]) + self.assertNotIn(curve_redim, sources[2]) + self.assertNotIn(curve, sources[2]) + + def test_dynamic_compute_overlayable_zorders_mixed_dynamic_and_non_dynamic_ndoverlays(self): + ndoverlay = NdOverlay({i: Area(range(10+i)) for i in range(2)}) + curve = DynamicMap(lambda: Curve(range(10)), kdims=[]) + curve_redim = curve.redim(x='x2') + combined = ndoverlay*curve_redim + combined[()] + sources = compute_overlayable_zorders(combined) + + self.assertIn(ndoverlay[0], sources[0]) + self.assertIn(ndoverlay, sources[0]) + self.assertNotIn(curve_redim, sources[0]) + self.assertNotIn(curve, sources[0]) + + self.assertIn(ndoverlay[1], sources[1]) + self.assertIn(ndoverlay, sources[1]) + self.assertNotIn(curve_redim, sources[1]) + self.assertNotIn(curve, sources[1]) + + self.assertIn(curve_redim, sources[2]) + self.assertIn(curve, sources[2]) + self.assertNotIn(ndoverlay, sources[2]) + + def test_dynamic_compute_overlayable_zorders_mixed_dynamic_and_non_dynamic_ndoverlays_reverse(self): + ndoverlay = NdOverlay({i: Area(range(10+i)) for i in range(2)}) + curve = DynamicMap(lambda: Curve(range(10)), kdims=[]) + curve_redim = curve.redim(x='x2') + combined = curve_redim*ndoverlay + combined[()] + sources = compute_overlayable_zorders(combined) + + self.assertIn(curve_redim, sources[0]) + self.assertIn(curve, sources[0]) + self.assertNotIn(ndoverlay, sources[0]) + + self.assertIn(ndoverlay[0], sources[1]) + self.assertIn(ndoverlay, sources[1]) + self.assertNotIn(curve_redim, sources[1]) + self.assertNotIn(curve, sources[1]) + + self.assertIn(ndoverlay[1], sources[2]) + self.assertIn(ndoverlay, sources[2]) + self.assertNotIn(curve_redim, sources[2]) + self.assertNotIn(curve, sources[2]) + + def test_dynamic_compute_overlayable_zorders_three_deep_dynamic_layers_reduced(self): + area = DynamicMap(lambda: Area(range(10)), kdims=[]) + curve = DynamicMap(lambda: Curve(range(10)), kdims=[]) + curve2 = DynamicMap(lambda: Curve(range(10)), kdims=[]) + area_redim = area.redim(x='x2') + curve_redim = curve.redim(x='x2') + curve2_redim = curve2.redim(x='x3') + combined = (area_redim*curve_redim).map(lambda x: x.get(0), Overlay) + combined1 = combined*curve2_redim + combined1[()] + sources = compute_overlayable_zorders(combined1) + + self.assertNotIn(curve_redim, sources[0]) + self.assertNotIn(curve, sources[0]) + self.assertNotIn(curve2_redim, sources[0]) + self.assertNotIn(curve2, sources[0]) + + self.assertIn(curve2_redim, sources[1]) + self.assertIn(curve2, sources[1]) + self.assertNotIn(area_redim, sources[1]) + self.assertNotIn(area, sources[1]) + self.assertNotIn(curve_redim, sources[1]) + self.assertNotIn(curve, sources[1]) + + + def test_dynamic_compute_overlayable_zorders_three_deep_dynamic_layers_reduced_layers_by_one(self): + area = DynamicMap(lambda: Area(range(10)), kdims=[]) + area2 = DynamicMap(lambda: Area(range(10)), kdims=[]) + curve = DynamicMap(lambda: Curve(range(10)), kdims=[]) + curve2 = DynamicMap(lambda: Curve(range(10)), kdims=[]) + area_redim = area.redim(x='x2') + curve_redim = curve.redim(x='x2') + curve2_redim = curve2.redim(x='x3') + combined = (area_redim*curve_redim*area2).map(lambda x: x.clone(x.items()[:2]), Overlay) + combined1 = combined*curve2_redim + combined1[()] + sources = compute_overlayable_zorders(combined1) + + self.assertNotIn(curve_redim, sources[0]) + self.assertNotIn(curve, sources[0]) + self.assertNotIn(curve2_redim, sources[0]) + self.assertNotIn(curve2, sources[0]) + self.assertNotIn(area, sources[0]) + self.assertNotIn(area_redim, sources[0]) + self.assertNotIn(area2, sources[0]) + + self.assertNotIn(area_redim, sources[1]) + self.assertNotIn(area, sources[1]) + self.assertNotIn(curve2_redim, sources[1]) + self.assertNotIn(curve2, sources[1]) + self.assertNotIn(area2, sources[0]) + + self.assertIn(curve2_redim, sources[2]) + self.assertIn(curve2, sources[2]) + self.assertNotIn(area_redim, sources[2]) + self.assertNotIn(area, sources[2]) + self.assertNotIn(area2, sources[0]) + self.assertNotIn(curve_redim, sources[2]) + self.assertNotIn(curve, sources[2]) + + class TestBokehUtils(ComparisonTestCase): def setUp(self):