-
-
Notifications
You must be signed in to change notification settings - Fork 402
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
Implement dynamic relabel, redim and select methods #1029
Changes from 10 commits
8441ab2
1e9dc79
246452d
483b5e0
d4d127e
1cd5e35
caef9d3
bf2e1c8
2130be4
fa5d797
1e47f97
6559cbd
2afc781
39ff1f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -344,13 +344,15 @@ def relabel(self, label=None, group=None, depth=0): | |
Assign a new label and/or group to an existing LabelledData | ||
object, creating a clone of the object with the new settings. | ||
""" | ||
keywords = [('label',label), ('group',group)] | ||
obj = self.clone(self.data, | ||
**{k:v for k,v in keywords if v is not None}) | ||
if (depth > 0) and getattr(obj, '_deep_indexable', False): | ||
for k, v in obj.items(): | ||
obj[k] = v.relabel(group=group, label=label, depth=depth-1) | ||
return obj | ||
new_data = self.data | ||
if (depth > 0) and getattr(self, '_deep_indexable', False): | ||
new_data = [] | ||
for k, v in self.data.items(): | ||
relabelled = v.relabel(group=group, label=label, depth=depth-1) | ||
new_data.append((k, relabelled)) | ||
keywords = [('label', label), ('group', group)] | ||
kwargs = {k: v for k, v in keywords if v is not None} | ||
return self.clone(new_data, **kwargs) | ||
|
||
|
||
def matches(self, spec): | ||
|
@@ -417,6 +419,7 @@ def map(self, map_fn, specs=None, clone=True): | |
Recursively replaces elements using a map function when the | ||
specification applies. | ||
""" | ||
if specs and not isinstance(specs, list): specs = [specs] | ||
applies = specs is None or any(self.matches(spec) for spec in specs) | ||
|
||
if self._deep_indexable: | ||
|
@@ -767,8 +770,9 @@ def select(self, selection_specs=None, **kwargs): | |
|
||
# Apply selection to self | ||
if local_kwargs and matches: | ||
ndims = (len(self.dimensions()) if any(d in self.vdims for d in kwargs) | ||
else self.ndims) | ||
ndims = self.ndims | ||
if any(d in self.vdims for d in kwargs): | ||
ndims = len(self.kdims+self.vdims) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, because it can include the deep dimensions as well which should be processed below. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. Makes sense... |
||
select = [slice(None) for _ in range(ndims)] | ||
for dim, val in local_kwargs.items(): | ||
if dim == 'value': | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -360,7 +360,6 @@ def relabel(self, label=None, group=None, depth=1): | |
return super(HoloMap, self).relabel(label=label, group=group, depth=depth) | ||
|
||
|
||
|
||
def hist(self, num_bins=20, bin_range=None, adjoin=True, individually=True, **kwargs): | ||
histmaps = [self.clone(shared_data=False) for _ in | ||
kwargs.get('dimension', range(1))] | ||
|
@@ -655,7 +654,7 @@ def reset(self): | |
return self | ||
|
||
|
||
def _cross_product(self, tuple_key, cache): | ||
def _cross_product(self, tuple_key, cache, data_slice): | ||
""" | ||
Returns a new DynamicMap if the key (tuple form) expresses a | ||
cross product, otherwise returns None. The cache argument is a | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be good to explain the new There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, docstrings still need updating. I'd love it if there was some way to inherit method docstrings easily, when you override a method. |
||
|
@@ -683,23 +682,52 @@ def _cross_product(self, tuple_key, cache): | |
val = cache[key] | ||
else: | ||
val = self._execute_callback(*key) | ||
if data_slice: | ||
val = self._dataslice(val, data_slice) | ||
data.append((key, val)) | ||
return self.clone(data) | ||
product = self.clone(data) | ||
|
||
if data_slice: | ||
from ..util import Dynamic | ||
def dynamic_slice(obj): | ||
return obj[data_slice] | ||
return Dynamic(product, operation=dynamic_slice, shared_data=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I forget...is there a good reason why this can't be a lambda? (replacing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No probably could be. |
||
return product | ||
|
||
|
||
def _slice_bounded(self, tuple_key): | ||
def _slice_bounded(self, tuple_key, data_slice): | ||
""" | ||
Slices bounded DynamicMaps by setting the soft_ranges on key dimensions. | ||
""" | ||
cloned = self.clone(self) | ||
slices = [el for el in tuple_key if isinstance(el, slice)] | ||
if any(el.step for el in slices): | ||
raise Exception("Slices cannot have a step argument " | ||
"in DynamicMap bounded mode ") | ||
elif len(slices) not in [0, len(tuple_key)]: | ||
raise Exception("Slices must be used exclusively or not at all") | ||
elif not slices: | ||
return None | ||
|
||
sliced = self.clone(self) | ||
for i, slc in enumerate(tuple_key): | ||
(start, stop) = slc.start, slc.stop | ||
if start is not None and start < cloned.kdims[i].range[0]: | ||
if start is not None and start < sliced.kdims[i].range[0]: | ||
raise Exception("Requested slice below defined dimension range.") | ||
if stop is not None and stop > cloned.kdims[i].range[1]: | ||
if stop is not None and stop > sliced.kdims[i].range[1]: | ||
raise Exception("Requested slice above defined dimension range.") | ||
cloned.kdims[i].soft_range = (start, stop) | ||
return cloned | ||
sliced.kdims[i].soft_range = (start, stop) | ||
if data_slice: | ||
if not isinstance(sliced, DynamicMap): | ||
return self._dataslice(sliced, data_slice) | ||
else: | ||
from ..util import Dynamic | ||
def dynamic_slice(obj): | ||
return obj[data_slice] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can also be replaced with a lambda... |
||
if len(self): | ||
slices = [slice(None) for _ in range(self.ndims)] + list(data_slice) | ||
sliced = super(DynamicMap, sliced).__getitem__(tuple(slices)) | ||
return Dynamic(sliced, operation=dynamic_slice, shared_data=True) | ||
return sliced | ||
|
||
|
||
def __getitem__(self, key): | ||
|
@@ -708,22 +736,22 @@ 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_streams(key, self.kdims, self.streams) | ||
# Split key dimensions and data slices | ||
if key is Ellipsis: | ||
return self | ||
elif key == (): | ||
map_slice, data_slice = (), () | ||
else: | ||
map_slice, data_slice = self._split_index(key) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any chance if key is Ellipsis:
return self
map_slice, data_slice = self._split_index(key) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
elif indexslice == () and not self.kdims:
return self.data[()] Won't work here unfortunately, but I guess I could still handle it in |
||
tuple_key = util.wrap_tuple_streams(map_slice, self.kdims, self.streams) | ||
|
||
# Validation for bounded mode | ||
if self.mode == 'bounded': | ||
# DynamicMap(...)[:] returns a new DynamicMap with the same cache | ||
if key == slice(None, None, None): | ||
return self.clone(self) | ||
|
||
slices = [el for el in tuple_key if isinstance(el, slice)] | ||
if any(el.step for el in slices): | ||
raise Exception("Slices cannot have a step argument " | ||
"in DynamicMap bounded mode ") | ||
if len(slices) not in [0, len(tuple_key)]: | ||
raise Exception("Slices must be used exclusively or not at all") | ||
if slices: | ||
return self._slice_bounded(tuple_key) | ||
sliced = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stray line that can be removed? ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, thanks. |
||
sliced = self._slice_bounded(tuple_key, data_slice) | ||
if sliced is not None: | ||
return sliced | ||
|
||
# Cache lookup | ||
try: | ||
|
@@ -742,7 +770,7 @@ def __getitem__(self, key): | |
"available cache in open interval mode.") | ||
|
||
# If the key expresses a cross product, compute the elements and return | ||
product = self._cross_product(tuple_key, cache.data if cache else {}) | ||
product = self._cross_product(tuple_key, cache.data if cache else {}, data_slice) | ||
if product is not None: | ||
return product | ||
|
||
|
@@ -752,10 +780,31 @@ def __getitem__(self, key): | |
if self.call_mode == 'counter': | ||
val = val[1] | ||
|
||
if data_slice: | ||
val = self._dataslice(val, data_slice) | ||
self._cache(tuple_key, val) | ||
return val | ||
|
||
|
||
def select(self, selection_specs=None, **kwargs): | ||
selection = super(DynamicMap, self).select(selection_specs, **kwargs) | ||
def dynamic_select(obj): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would |
||
if selection_specs is not None: | ||
matches = any(obj.matches(spec) for spec in selection_specs) | ||
else: | ||
matches = True | ||
if matches: | ||
return obj.select(**kwargs) | ||
return obj | ||
|
||
if not isinstance(selection, DynamicMap): | ||
return dynamic_select(selection) | ||
else: | ||
from ..util import Dynamic | ||
return Dynamic(selection, operation=dynamic_select, | ||
shared_data=True) | ||
|
||
|
||
def _cache(self, key, val): | ||
""" | ||
Request that a key/value pair be considered for caching. | ||
|
@@ -795,6 +844,35 @@ def next(self): | |
return val | ||
|
||
|
||
def relabel(self, label=None, group=None, depth=1): | ||
""" | ||
Assign a new label and/or group to an existing LabelledData | ||
object, creating a clone of the object with the new settings. | ||
""" | ||
relabelled = super(DynamicMap, self).relabel(label, group, depth) | ||
if depth > 0: | ||
from ..util import Dynamic | ||
def dynamic_relabel(obj): | ||
return obj.relabel(group=group, label=label, depth=depth-1) | ||
return Dynamic(relabelled, shared_data=True, operation=dynamic_relabel) | ||
return relabelled | ||
|
||
|
||
def redim(self, specs=None, **dimensions): | ||
""" | ||
Replaces existing dimensions in an object with new dimensions | ||
or changing specific attributes of a dimensions. Dimension | ||
mapping should map between the old dimension name and a | ||
dictionary of the new attributes, a completely new dimension | ||
or a new string name. | ||
""" | ||
redimmed = super(DynamicMap, self).redim(specs, **dimensions) | ||
from ..util import Dynamic | ||
def dynamic_redim(obj): | ||
return obj.redim(specs, **dimensions) | ||
return Dynamic(redimmed, shared_data=True, operation=dynamic_redim) | ||
|
||
|
||
def groupby(self, dimensions=None, container_type=None, group_type=None, **kwargs): | ||
""" | ||
Implements a dynamic version of a groupby, which will | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,14 +28,17 @@ class Dynamic(param.ParameterizedFunction): | |
kwargs = param.Dict(default={}, doc=""" | ||
Keyword arguments passed to the function.""") | ||
|
||
shared_data = param.Boolean(default=False, doc=""" | ||
Whether the cloned DynamicMap will share the same data.""") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yes, thanks! I suppose you might want to copy the cache in some cases (e.g for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, it's probably mostly going to be used internally. |
||
|
||
streams = param.List(default=[], doc=""" | ||
List of streams to attach to the returned DynamicMap""") | ||
|
||
def __call__(self, map_obj, **params): | ||
self.p = param.ParamOverrides(self, params) | ||
callback = self._dynamic_operation(map_obj) | ||
if isinstance(map_obj, DynamicMap): | ||
dmap = map_obj.clone(callback=callback, shared_data=False, | ||
dmap = map_obj.clone(callback=callback, shared_data=self.p.shared_data, | ||
streams=[]) | ||
else: | ||
dmap = self._make_dynamic(map_obj, callback) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reimplemented this because assigning this way is inefficient for an internal operation.