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

Dimensioned streams #846

Merged
merged 12 commits into from
Sep 9, 2016
Merged
23 changes: 12 additions & 11 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,8 @@ def _validate_mode(self):
return 'key'
# Any unbounded kdim (any direction) implies open mode
for kdim in self.kdims:
if kdim.name in util.stream_parameters(self.streams):
return 'key'
if kdim.values:
continue
if None in kdim.range:
Expand Down Expand Up @@ -557,13 +559,10 @@ def _execute_callback(self, *args):
retval = next(self.callback)
else:
# Additional validation needed to ensure kwargs don't clash
kdims = [kdim.name for kdim in self.kdims]
kwarg_items = [s.contents.items() for s in self.streams]
flattened = [el for kws in kwarg_items for el in kws]
klist = [k for k,_ in flattened]
clashes = set([k for k in klist if klist.count(k) > 1])
if clashes:
self.warning('Parameter name clashes for keys: %r' % clashes)

flattened = [(k,v) for kws in kwarg_items for (k,v) in kws
if k not in kdims]
retval = self.callback(*args, **dict(flattened))
if self.call_mode=='key':
return self._style(retval)
Expand Down Expand Up @@ -654,7 +653,7 @@ def __getitem__(self, key):
for a previously generated key that is still in the cache
(for one of the 'open' modes)
"""
tuple_key = util.wrap_tuple(key)
tuple_key = util.wrap_tuple_streams(key, self.kdims, self.streams)

# Validation for bounded mode
if self.mode == 'bounded':
Expand All @@ -673,8 +672,8 @@ def __getitem__(self, key):

# Cache lookup
try:
if self.streams:
raise KeyError('Using streams disables DynamicMap cache')
if util.dimensionless_contents(self.streams, self.kdims):
raise KeyError('Using dimensionless streams disables DynamicMap cache')
cache = super(DynamicMap,self).__getitem__(key)
# Return selected cache items in a new DynamicMap
if isinstance(cache, DynamicMap) and self.mode=='open':
Expand Down Expand Up @@ -704,9 +703,11 @@ def _cache(self, key, val):
"""
Request that a key/value pair be considered for caching.
"""
cache_size = (1 if util.dimensionless_contents(self.streams, self.kdims)
else self.cache_size)
if self.mode == 'open' and (self.counter % self.cache_interval)!=0:
return
if len(self) >= self.cache_size:
if len(self) >= cache_size:
first_key = next(self.data.iterkeys())
self.data.pop(first_key)
self.data[key] = val
Expand All @@ -728,7 +729,7 @@ def next(self):
(key, val) = (retval if isinstance(retval, tuple)
else (self.counter, retval))

key = util.wrap_tuple(key)
key = util.wrap_tuple_streams(key, self.kdims, self.streams)
if len(key) != len(self.key_dimensions):
raise Exception("Generated key does not match the number of key dimensions")

Expand Down
46 changes: 46 additions & 0 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,52 @@ def wrap_tuple(unwrapped):
return (unwrapped if isinstance(unwrapped, tuple) else (unwrapped,))



def stream_parameters(streams, no_duplicates=True, exclude=['name']):
"""
Given a list of streams, return a flat list of parameter name,
excluding those listed in the exclude list.

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]
names = [name for group in param_groups for name in group]

if no_duplicates:
clashes = set([n for n in names if names.count(n) > 1])
if clashes:
raise KeyError('Parameter name clashes for keys: %r' % clashes)
return [name for name in names if name not in exclude]


def dimensionless_contents(streams, kdims):
"""
Return a list of stream parameters that have not been associated
with any of the key dimensions.
"""
names = stream_parameters(streams)
kdim_names = [kdim.name for kdim in kdims]
return [name for name in names if name not in kdim_names]


def wrap_tuple_streams(unwrapped, kdims, streams):
"""
Fills in tuple keys with dimensioned stream values as appropriate.
"""
param_groups = [(s.params().keys(), s) for s in streams]
pairs = [(name,s) for (group, s) in param_groups for name in group]
substituted = []
for pos,el in enumerate(wrap_tuple(unwrapped)):
if el is None and pos < len(kdims):
matches = [(name,s) for (name,s) in pairs if name==kdims[pos].name]
if len(matches) == 1:
(name, stream) = matches[0]
el = stream.contents[name]
substituted.append(el)
return tuple(substituted)


def itervalues(obj):
"Get value iterator from dictionary for Python 2 and 3"
return iter(obj.values()) if sys.version_info.major == 3 else obj.itervalues()
Expand Down
37 changes: 36 additions & 1 deletion tests/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

import numpy as np

from holoviews.core.util import sanitize_identifier_fn, find_range, max_range
from holoviews.core.util import sanitize_identifier_fn, find_range, max_range, wrap_tuple_streams
from holoviews import Dimension
from holoviews.streams import PositionXY
from holoviews.element.comparison import ComparisonTestCase

py_version = sys.version_info.major
Expand Down Expand Up @@ -298,3 +300,36 @@ def test_max_range2(self):
lower, upper = max_range(self.ranges2)
self.assertTrue(math.isnan(lower))
self.assertTrue(math.isnan(upper))



class TestWrapTupleStreams(unittest.TestCase):


def test_no_streams(self):
result = wrap_tuple_streams((1,2), [],[])
self.assertEqual(result, (1,2))

def test_no_streams_two_kdims(self):
result = wrap_tuple_streams((1,2),
[Dimension('x'), Dimension('y')],
[])
self.assertEqual(result, (1,2))

def test_no_streams_none_value(self):
result = wrap_tuple_streams((1,None),
[Dimension('x'), Dimension('y')],
[])
self.assertEqual(result, (1,None))

def test_no_streams_one_stream_substitution(self):
result = wrap_tuple_streams((None,3),
[Dimension('x'), Dimension('y')],
[PositionXY(x=-5,y=10)])
self.assertEqual(result, (-5,3))

def test_no_streams_two_stream_substitution(self):
result = wrap_tuple_streams((None,None),
[Dimension('x'), Dimension('y')],
[PositionXY(x=0,y=5)])
self.assertEqual(result, (0,5))