Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DynamicMap with streams force callback on first lookup after refresh #858

Merged
merged 11 commits into from
Sep 12, 2016
4 changes: 3 additions & 1 deletion holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ def __init__(self, callback, initial_items=None, **params):

self.call_mode = self._validate_mode()
self.mode = 'bounded' if self.call_mode == 'key' else 'open'
self._stream_cache_lookup = False


def _initial_key(self):
Expand Down Expand Up @@ -672,7 +673,8 @@ def __getitem__(self, key):

# Cache lookup
try:
if util.dimensionless_contents(self.streams, self.kdims):
if (util.dimensionless_contents(self.streams, self.kdims) and
not self._stream_cache_lookup):
raise KeyError('Using dimensionless streams disables DynamicMap cache')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From our discussion, we think it ought to be something like:

if (util.dimensionless_contents(self.streams, self.kdims)
                  raise KeyError('Using dimensionless streams disables DynamicMap cache')
elif self._disable_cache_lookup:
                  raise KeyError('Cache lookup disabled')

That way the variable isn't about streams and you can disable cache lookup on dynamic maps not matter how they are (or aren't) using streams.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we decided in the end that it is about streams.

cache = super(DynamicMap,self).__getitem__(key)
# Return selected cache items in a new DynamicMap
Expand Down
26 changes: 26 additions & 0 deletions holoviews/core/traversal.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,29 @@ def hierarchical(keys):
store1[v2].append(v1)
hierarchies.append(store2 if hierarchy else {})
return hierarchies


class enable_streams_cache(object):
"""
Copy link
Contributor

@jlstevens jlstevens Sep 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the discussion about, I would call it disable_cache_lookup with constructor:

def __init__(self, obj):

I don't think it needs an enable or disable flag as enable is the default behaviour. Unless you can think of situations where you want to be able to disable the context manager with a boolean? If you can think of such a situation (or just think it makes it more flexible) then

def __init__(self, obj, disable=True):

Would be reasonable. The issue with using either enable or disable as an argument name is that it isn't clear if you are enabling/disabling the cache lookup of enabling/disabling the context manager that is enabling/disabling the cache lookup!

For this reason, maybe something like this would be clearer:

def __init__(self, obj, allow_cache_lookup=False):

Just some things to think about...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it needs an enable or disable flag as enable is the default behaviour. Unless you can think of situations where you want to be able to disable the context manager with a boolean? If you can think of such a situation (or just think it makes it more flexible) then

The issue is that making context manager conditional is awkward, which is why I moved the boolean onto the object. I agree with renaming the argument and making it optional though.

Context manager which temporarily enables lookup of frame in cache
on a DynamicMap with streams. Allows passing any Dimensioned
object which might contain a DynamicMap and whether to enable the
cache. This allows looking up an item without triggering the
callback. Useful when the object is looked up multiple times as
part of some processing pipeline.
"""

def __init__(self, obj, enable):
self.obj = obj
self._enable = enable

def __enter__(self):
self.set_cache_flag(self._enable)

def __exit__(self, exc_type, exc_val, exc_tb):
self.set_cache_flag(False)

def set_cache_flag(self, value):
self.obj.traverse(lambda x: setattr(x, '_stream_cache_lookup', value),
['DynamicMap'])

2 changes: 1 addition & 1 deletion holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,7 @@ def stream_parameters(streams, no_duplicates=True, exclude=['name']):
If no_duplicates is enabled, a KeyError will be raised if there are
parameter name clashes across the streams.
"""
param_groups = [s.params().keys() for s in streams]
param_groups = [s.contents.keys() for s in streams]
names = [name for group in param_groups for name in group]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix!


if no_duplicates:
Expand Down
28 changes: 23 additions & 5 deletions holoviews/plotting/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from ..core.options import Store, Compositor, SkipRendering
from ..core.overlay import NdOverlay
from ..core.spaces import HoloMap, DynamicMap
from ..core.traversal import enable_streams_cache
from ..element import Table
from .util import (get_dynamic_mode, initialize_sampled, dim_axis_label,
attach_streams)
Expand Down Expand Up @@ -198,6 +199,7 @@ def __init__(self, keys=None, dimensions=None, layout_dimensions=None,
self.ranges = {}
self.renderer = renderer if renderer else Store.renderers[self.backend].instance()
self.comm = None
self._force = True

params = {k: v for k, v in params.items()
if k in self.params()}
Expand Down Expand Up @@ -258,6 +260,15 @@ def traverse(self, fn=None, specs=None, full_breadth=True):
if not full_breadth: break
return accumulator

@property
def force(self):
return self._force


@force.setter
def force(self, value):
self.traverse(lambda x: setattr(x, '_force', value))

Copy link
Contributor

@jlstevens jlstevens Sep 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I see below, I think having self.force as a plain attribute without self._force would be simpler (i.e no properties). The reason is that in the context_manager you just need to accessself.force. Then you set self.force to True and False in one place in this one file. As a result, I'm not sure it is worth introducing properties and an underscore attribute when you can just inline two fairly short traversal expressions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decided on replacing this with a traverse_setter utility.


def _frame_title(self, key, group_size=2, separator='\n'):
"""
Expand Down Expand Up @@ -481,6 +492,7 @@ def refresh(self, **kwargs):
Refreshes the plot by rerendering it and then pushing
the updated data if the plot has an associated Comm.
"""
self.force = True
if self.current_key:
self.update(self.current_key)
else:
Expand Down Expand Up @@ -600,7 +612,9 @@ def _get_frame(self, key):
self.current_key = key
return self.current_frame
elif self.dynamic:
key, frame = util.get_dynamic_item(self.hmap, self.dimensions, key)
with enable_streams_cache(self.hmap, not self.force or not self.drawn):
key, frame = util.get_dynamic_item(self.hmap, self.dimensions, key)
self.force = False
if not isinstance(key, tuple): key = (key,)
key_map = dict(zip([d.name for d in self.hmap.kdims], key))
key = tuple(key_map.get(d.name, None) for d in self.dimensions)
Expand Down Expand Up @@ -898,17 +912,16 @@ def get_extents(self, overlay, ranges):
class GenericCompositePlot(DimensionedPlot):

def __init__(self, layout, keys=None, dimensions=None, **params):
dynamic, sampled = get_dynamic_mode(layout)
if sampled:
initialize_sampled(layout, dimensions, keys[0])

if 'uniform' not in params:
params['uniform'] = traversal.uniform(layout)

top_level = keys is None
if top_level:
dimensions, keys = traversal.unique_dimkeys(layout)

dynamic, sampled = get_dynamic_mode(layout)
if sampled:
initialize_sampled(layout, dimensions, keys[0])
self.layout = layout
super(GenericCompositePlot, self).__init__(keys=keys,
dynamic=dynamic,
Expand Down Expand Up @@ -944,6 +957,11 @@ def _get_frame(self, key):
dim_keys = zip([d.name for d in self.dimensions
if d in item.dimensions('key')], key)
self.current_key = tuple(k[1] for k in dim_keys)
elif item.traverse(lambda x: x, [DynamicMap]):
with enable_streams_cache(item, not self.force or not self.drawn):
key, frame = util.get_dynamic_item(item, self.dimensions, key)
layout_frame[path] = frame
continue
elif self.uniform:
dim_keys = zip([d.name for d in self.dimensions
if d in item.dimensions('key')], key)
Expand Down