Skip to content

Commit

Permalink
Avoid rerendering of overlaid DynamicMaps with non-triggered streams
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Jan 10, 2018
1 parent 2e4e81d commit c49da56
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 15 deletions.
56 changes: 54 additions & 2 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))


Expand All @@ -585,6 +586,47 @@ def get_nested_streams(dmap):
return list({s for dmap in get_nested_dmaps(dmap) for s in dmap.streams})


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(obj, depth=0):
"""
Splits a DynamicMap into the original component layers it was
constructed from.
"""
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(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


@contextmanager
def dynamicmap_memoization(callable_obj, streams):
"""
Expand Down Expand Up @@ -1342,6 +1384,16 @@ def next(self):
raise Exception('The next method can only be used for DynamicMaps using'
'generators (or callables without arguments)')

def split_overlays(self):
"""
Splits the constituents of a DynamicMap into individual components.
"""
if not issubclass(self.type, CompositeOverlay):
return None, self.clone()
keys, maps = super(DynamicMap, self).split_overlays()
return keys, split_dmap(self)


# For Python 2 and 3 compatibility
__next__ = next

Expand Down
13 changes: 12 additions & 1 deletion holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from ...core import DynamicMap, CompositeOverlay, Element, Dimension
from ...core.options import abbreviated_exception, SkipRendering
from ...core.spaces import get_nested_streams
from ...core import util
from ...streams import Stream, Buffer
from ..plot import GenericElementPlot, GenericOverlayPlot
Expand Down Expand Up @@ -1461,9 +1462,19 @@ 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 get_nested_streams(self.hmap)
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 get_nested_streams(subplot.hmap)):
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
Expand Down
30 changes: 21 additions & 9 deletions holoviews/plotting/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -850,31 +850,40 @@ def _create_subplots(self, ranges):
self.batched = False
keys, vmaps = self.hmap.split_overlays()

if isinstance(self.hmap, DynamicMap) and not self.batched:
elements = self.hmap.last.values()
else:
elements = vmaps

# Compute global ordering
length = self.style_grouping
group_fn = lambda x: (x.type.__name__, x.last.group, x.last.label)
group_fn = lambda x: ((x.type.__name__, x.last.group, x.last.label)
if isinstance(x, HoloMap) else
(type(x).__name__, x.group, x.label))
map_lengths = Counter()
for m in vmaps:
for m in elements:
map_lengths[group_fn(m)[:length]] += 1
zoffset = 0
overlay_type = 1 if self.hmap.type == Overlay else 2
group_counter = Counter()

subplots = OrderedDict()
for (key, vmap) in zip(keys, vmaps):
for (key, vmap, el) in zip(keys, vmaps, elements):
opts = {'overlaid': overlay_type}
vobj = el.last if isinstance(el, HoloMap) else el
if self.hmap.type == Overlay:
style_key = (vmap.type.__name__,) + key
type_name = type(vobj).__name__
style_key = (type_name,) + key
else:
if not isinstance(key, tuple): key = (key,)
style_key = group_fn(vmap) + key
style_key = group_fn(vobj) + key
opts['overlay_dims'] = OrderedDict(zip(self.hmap.last.kdims, key))

if self.batched:
vtype = type(vmap.last.last)
vtype = type(vobj.last)
oidx = 0
else:
vtype = type(vmap.last)
vtype = type(vobj)
oidx = ordering.index(style_key)

plottype = registry.get(vtype, None)
Expand All @@ -892,7 +901,7 @@ def _create_subplots(self, ranges):

if issubclass(plottype, GenericOverlayPlot):
opts['show_legend'] = self.show_legend
if not any(len(frame) for frame in vmap):
if isinstance(vmap, HoloMap) and not any(len(frame) for frame in vmap):
self.warning('%s is empty and will be skipped during plotting'
% vmap.last)
continue
Expand All @@ -902,9 +911,12 @@ def _create_subplots(self, ranges):
opts.update(propagate)
if len(ordering) > self.legend_limit:
opts['show_legend'] = False
style = self.lookup_options(vmap.last, 'style').max_cycles(group_length)

style = self.lookup_options(vobj, 'style').max_cycles(group_length)

passed_handles = {k: v for k, v in self.handles.items()
if k in self._passed_handles}

plotopts = dict(opts, cyclic_index=cyclic_index,
invert_axes=self.invert_axes,
dimensions=self.dimensions, keys=self.keys,
Expand Down
124 changes: 121 additions & 3 deletions tests/testdynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import time

import numpy as np
from holoviews import Dimension, NdLayout, GridSpace, Layout
from holoviews import Dimension, NdLayout, GridSpace, Layout, NdOverlay, Overlay
from holoviews.core.spaces import DynamicMap, HoloMap, Callable
from holoviews.element import Image, Scatter, Curve, Text, Points
from holoviews.operation import histogram
from holoviews.element import Image, Scatter, Curve, Text, Points, VectorField, HLine, Path
from holoviews.operation import histogram, operation
from holoviews.plotting.util import initialize_dynamic
from holoviews.streams import Stream, PointerXY, PointerX, PointerY, RangeX
from holoviews.util import Dynamic
from holoviews.element.comparison import ComparisonTestCase
Expand Down Expand Up @@ -230,6 +231,123 @@ def history_callback(x, y, history=deque(maxlen=10)):
dmap.reindex(['x'])


class DynamicMapSplitOverlays(ComparisonTestCase):

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)
keys = [0, 1]
layers = [self.dmap_ndoverlay, self.dmap_ndoverlay]
self.assertEqual(test.split_overlays(), (keys, layers))

def test_dmap_overlay(self):
test = self.dmap_overlay
initialize_dynamic(test)
keys = [('Curve', 'I'), ('Points', 'I')]
layers = [self.dmap_overlay, self.dmap_overlay]
self.assertEqual(test.split_overlays(), (keys, layers))

def test_dmap_element_mul_dmap_overlay(self):
test = self.dmap_element * self.dmap_overlay
initialize_dynamic(test)
keys = [('Image', 'I'), ('Curve', 'I'), ('Points', 'I')]
layers = [self.dmap_element, self.dmap_overlay, self.dmap_overlay]
self.assertEqual(test.split_overlays(), (keys, layers))

def test_dmap_element_mul_dmap_ndoverlay(self):
test = self.dmap_element * self.dmap_ndoverlay
initialize_dynamic(test)
keys = [('Image', 'I'), ('NdOverlay', 'I')]
layers = [self.dmap_element, self.dmap_ndoverlay]
self.assertEqual(test.split_overlays(), (keys, layers))

def test_dmap_element_mul_element(self):
test = self.dmap_element * self.element
initialize_dynamic(test)
keys = [('Image', 'I'), ('Scatter', 'I')]
layers = [self.dmap_element, self.element]
self.assertEqual(test.split_overlays(), (keys, layers))

def test_dmap_element_mul_overlay(self):
test = self.dmap_element * self.overlay
initialize_dynamic(test)
keys = [('Image', 'I'), ('Path', 'I'), ('HLine', 'I')]
layers = [self.dmap_element, self.el1, self.el2]
self.assertEqual(test.split_overlays(), (keys, layers))

def test_dmap_element_mul_ndoverlay(self):
test = self.dmap_element * self.ndoverlay
initialize_dynamic(test)
keys = [('Image', 'I'), ('NdOverlay', 'I')]
layers = [self.dmap_element, self.ndoverlay]
self.assertEqual(test.split_overlays(), (keys, layers))

def test_dmap_overlay_mul_dmap_ndoverlay(self):
test = self.dmap_overlay * self.dmap_ndoverlay
initialize_dynamic(test)
keys = [('Curve', 'I'), ('Points', 'I'), ('NdOverlay', 'I')]
layers = [self.dmap_overlay, self.dmap_overlay, self.dmap_ndoverlay]
self.assertEqual(test.split_overlays(), (keys, layers))

def test_dmap_overlay_mul_element(self):
test = self.dmap_overlay * self.element
initialize_dynamic(test)
keys = [('Curve', 'I'), ('Points', 'I'), ('Scatter', 'I')]
layers = [self.dmap_overlay, self.dmap_overlay, self.element]
self.assertEqual(test.split_overlays(), (keys, layers))

def test_dmap_overlay_mul_overlay(self):
test = self.dmap_overlay * self.overlay
initialize_dynamic(test)
keys = [('Curve', 'I'), ('Points', 'I'), ('Path', 'I'), ('HLine', 'I')]
layers = [self.dmap_overlay, self.dmap_overlay, self.el1, self.el2]
self.assertEqual(test.split_overlays(), (keys, 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)
keys = [('Curve', 'I'), ('Points', 'I'), ('Scatter', 'I'), ('NdOverlay', 'I'),
('Path', 'I'), ('HLine', 'I'), ('Image', 'I'), ('NdOverlay', 'II')]
layers = [self.dmap_overlay, self.dmap_overlay, self.element,
self.dmap_ndoverlay, self.el1, self.el2, self.dmap_element,
self.ndoverlay]
self.assertEqual(test.split_overlays(), (keys, layers))

def test_dmap_overlay_operation_mul_dmap_ndoverlay(self):
mapped = operation(self.dmap_overlay)
test = mapped * self.dmap_ndoverlay
initialize_dynamic(test)
keys = [('Curve', 'I'), ('Points', 'I'), ('NdOverlay', 'I')]
layers = [mapped, mapped, self.dmap_ndoverlay]
self.assertEqual(test.split_overlays(), (keys, 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)
keys = [('Curve', 'I'), ('Points', 'I'), ('NdOverlay', 'I')]
layers = [mapped, mapped, self.dmap_ndoverlay]
self.assertEqual(test.split_overlays(), (keys, 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)
keys = [('Curve', 'I'), ('Scatter', 'I'), ('NdOverlay', 'I')]
layers = [mapped, self.element, self.dmap_ndoverlay]
self.assertEqual(test.split_overlays(), (keys, layers))


class DynamicMapUnboundedProperty(ComparisonTestCase):

def test_callable_bounded_init(self):
Expand Down

0 comments on commit c49da56

Please sign in to comment.