diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 66258850d4..02c88dc5b5 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -9,7 +9,7 @@ import numpy as np import param -from ..dimension import replace_dimensions +from ..dimension import redim from .interface import Interface from .array import ArrayInterface from .dictionary import DictInterface @@ -188,6 +188,8 @@ def __init__(self, data, **kwargs): super(Dataset, self).__init__(data, **dict(kwargs, **dict(dims, **extra_kws))) self.interface.validate(self) + self.redim = redim(self, mode='dataset') + def __setstate__(self, state): """ @@ -564,29 +566,6 @@ def shape(self): return self.interface.shape(self) - def redim(self, specs=None, **dimensions): - """ - Replace dimensions on the dataset and allows renaming - dimensions in the dataset. 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. - """ - if specs is not None: - if not isinstance(specs, list): - specs = [specs] - if not any(self.matches(spec) for spec in specs): - return self - - kdims = replace_dimensions(self.kdims, dimensions) - vdims = replace_dimensions(self.vdims, dimensions) - zipped_dims = zip(self.kdims+self.vdims, kdims+vdims) - renames = {pk.name: nk for pk, nk in zipped_dims if pk != nk} - data = self.data - if renames: - data = self.interface.redim(self, renames) - return self.clone(data, kdims=kdims, vdims=vdims) - - def dimension_values(self, dim, expanded=True, flat=True): """ Returns the values along a particular dimension. If unique diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index ece24a9ad6..fc465fcd3e 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -40,36 +40,124 @@ def param_aliases(d): return d -def replace_dimensions(dimensions, overrides): +class redim(object): """ - Replaces dimensions in a list with a dictionary of overrides. - Overrides should be indexed by the dimension name with values that - is either a Dimension object, a string name or a dictionary - specifying the dimension parameters to override. + Utility that supports re-dimensioning any HoloViews object via the + redim method. """ - replaced = [] - for d in dimensions: - if d.name in overrides: - override = overrides[d.name] - elif d.label in overrides: - override = overrides[d.label] - else: - override = None - - if override is None: - replaced.append(d) - elif isinstance(override, (basestring, tuple)): - replaced.append(d(override)) - elif isinstance(override, Dimension): - replaced.append(override) - elif isinstance(override, dict): - replaced.append(d.clone(override.get('name',None), - **{k:v for k,v in override.items() if k != 'name'})) - else: - raise ValueError('Dimension can only be overridden ' - 'with another dimension or a dictionary ' - 'of attributes') - return replaced + + def __init__(self, parent, mode=None): + self.parent = parent + # Can be 'dataset', 'dynamic' or None + self.mode = mode + + def __str__(self): + return "" + + @classmethod + def replace_dimensions(cls, dimensions, overrides): + """ + Replaces dimensions in a list with a dictionary of overrides. + Overrides should be indexed by the dimension name with values that + is either a Dimension object, a string name or a dictionary + specifying the dimension parameters to override. + """ + replaced = [] + for d in dimensions: + if d.name in overrides: + override = overrides[d.name] + elif d.label in overrides: + override = overrides[d.label] + else: + override = None + + if override is None: + replaced.append(d) + elif isinstance(override, (basestring, tuple)): + replaced.append(d(override)) + elif isinstance(override, Dimension): + replaced.append(override) + elif isinstance(override, dict): + replaced.append(d.clone(override.get('name',None), + **{k:v for k,v in override.items() if k != 'name'})) + else: + raise ValueError('Dimension can only be overridden ' + 'with another dimension or a dictionary ' + 'of attributes') + return replaced + + + def __call__(self, specs=None, **dimensions): + """ + Replace dimensions on the dataset and allows renaming + dimensions in the dataset. 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. + """ + parent = self.parent + redimmed = parent + if parent._deep_indexable and self.mode != 'dataset': + deep_mapped = [(k, v.redim(specs, **dimensions)) + for k, v in parent.items()] + redimmed = parent.clone(deep_mapped) + + if specs is not None: + if not isinstance(specs, list): + specs = [specs] + matches = any(parent.matches(spec) for spec in specs) + if self.mode != 'dynamic' and not matches: + return redimmed + + + kdims = self.replace_dimensions(parent.kdims, dimensions) + vdims = self.replace_dimensions(parent.vdims, dimensions) + zipped_dims = zip(parent.kdims+parent.vdims, kdims+vdims) + renames = {pk.name: nk for pk, nk in zipped_dims if pk != nk} + + if self.mode == 'dataset': + data = parent.data + if renames: + data = parent.interface.redim(parent, renames) + return parent.clone(data, kdims=kdims, vdims=vdims) + + redimmed = redimmed.clone(kdims=kdims, vdims=vdims) + if self.mode != 'dynamic': + return redimmed + + from ..util import Dynamic + def dynamic_redim(obj): + return obj.redim(specs, **dimensions) + return Dynamic(redimmed, shared_data=True, operation=dynamic_redim) + + + def _redim(self, name, specs, **dims): + dimensions = {k:{name:v} for k,v in dims.items()} + return self(specs, **dimensions) + + def cyclic(self, specs=None, **values): + return self._redim('cyclic', specs, **values) + + def value_format(self, specs=None, **values): + return self._redim('value_format', specs, **values) + + def range(self, specs=None, **values): + return self._redim('range', specs, **values) + + def soft_range(self, specs=None, **values): + return self._redim('soft_range', specs, **values) + + def type(self, specs=None, **values): + return self._redim('type', specs, **values) + + def step(self, specs=None, **values): + return self._redim('step', specs, **values) + + def unit(self, specs=None, **values): + return self._redim('unit', specs, **values) + + def values(self, specs=None, **ranges): + return self._redim('values', specs, **ranges) + class Dimension(param.Parameterized): @@ -699,6 +787,7 @@ def __init__(self, data, **params): cdims = [(d.name, val) for d, val in self.cdims.items()] self._cached_constants = OrderedDict(cdims) self._settings = None + self.redim = redim(self) def _valid_dimensions(self, dimensions): @@ -910,35 +999,6 @@ def select(self, selection_specs=None, **kwargs): return selection - 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. - """ - if specs is None: - applies = True - else: - if not isinstance(specs, list): - specs = [specs] - applies = any(self.matches(spec) for spec in specs) - - redimmed = self - if self._deep_indexable: - deep_mapped = [(k, v.redim(specs, **dimensions)) - for k, v in self.items()] - redimmed = self.clone(deep_mapped) - - if applies: - kdims = replace_dimensions(self.kdims, dimensions) - vdims = replace_dimensions(self.vdims, dimensions) - return redimmed.clone(kdims=kdims, vdims=vdims) - else: - return redimmed - - def dimension_values(self, dimension, expanded=True, flat=True): """ Returns the values along the specified dimension. This method diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 4c12263a83..5ce2cb46d7 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -9,7 +9,7 @@ import param from . import traversal, util -from .dimension import OrderedDict, Dimension, ViewableElement +from .dimension import OrderedDict, Dimension, ViewableElement, redim from .layout import Layout, AdjointLayout, NdLayout from .ndmapping import UniformNdMapping, NdMapping, item_check from .overlay import Overlay, CompositeOverlay, NdOverlay, Overlayable @@ -585,6 +585,7 @@ def __init__(self, callback, initial_items=None, **params): for stream in self.streams: if stream.source is None: stream.source = self + self.redim = redim(self, mode='dynamic') def _initial_key(self): """ @@ -906,21 +907,6 @@ def dynamic_relabel(obj): 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 collate(self): """ Collation allows reorganizing DynamicMaps with invalid nesting diff --git a/tests/testannotations.py b/tests/testannotations.py index 71e22ce29a..183a71061c 100644 --- a/tests/testannotations.py +++ b/tests/testannotations.py @@ -19,6 +19,11 @@ def test_vline_dimension_values(self): self.assertEqual(hline.range(0), (0, 0)) self.assertEqual(hline.range(1), (None, None)) + def test_arrow_redim_range_aux(self): + annotations = Arrow(0, 0) + redimmed = annotations.redim.range(x=(-0.5,0.5)) + self.assertEqual(redimmed.kdims[0].range, (-0.5,0.5)) + def test_deep_clone_map_select_redim(self): annotations = (Text(0, 0, 'A') + Arrow(0, 0) + HLine(0) + VLine(0)) selected = annotations.select(x=(0, 5)) diff --git a/tests/testdataset.py b/tests/testdataset.py index 4a34a5a285..da2d891011 100644 --- a/tests/testdataset.py +++ b/tests/testdataset.py @@ -137,6 +137,14 @@ def test_dataset_redim_hm_kdim(self): self.assertEqual(redimmed.dimension_values('Time'), self.dataset_hm.dimension_values('x')) + def test_dataset_redim_hm_kdim_range_aux(self): + redimmed = self.dataset_hm.redim.range(x=(-100,3)) + self.assertEqual(redimmed.kdims[0].range, (-100,3)) + + def test_dataset_redim_hm_kdim_soft_range_aux(self): + redimmed = self.dataset_hm.redim.soft_range(x=(-100,30)) + self.assertEqual(redimmed.kdims[0].soft_range, (-100,30)) + def test_dataset_redim_hm_kdim_alias(self): redimmed = self.dataset_hm_alias.redim(x='Time') self.assertEqual(redimmed.dimension_values('Time'), diff --git a/tests/testdimensions.py b/tests/testdimensions.py index 172a1c2cbf..2306eed451 100644 --- a/tests/testdimensions.py +++ b/tests/testdimensions.py @@ -258,3 +258,13 @@ def test_dimensioned_redim_dict(self): def test_dimensioned_redim_dict_range(self): redimensioned = Dimensioned('Arbitrary Data', kdims=['x']).redim(x={'range': (0, 10)}) self.assertEqual(redimensioned.kdims[0].range, (0, 10)) + + def test_dimensioned_redim_range_aux(self): + dimensioned = Dimensioned('Arbitrary Data', kdims=['x']) + redimensioned = dimensioned.redim.range(x=(-10,42)) + self.assertEqual(redimensioned.kdims[0].range, (-10,42)) + + def test_dimensioned_redim_cyclic_aux(self): + dimensioned = Dimensioned('Arbitrary Data', kdims=['x']) + redimensioned = dimensioned.redim.cyclic(x=True) + self.assertEqual(redimensioned.kdims[0].cyclic, True) diff --git a/tests/testdynamic.py b/tests/testdynamic.py index 212fdc423b..691a7d9190 100644 --- a/tests/testdynamic.py +++ b/tests/testdynamic.py @@ -33,6 +33,21 @@ def test_redim_dimension_name(self): dmap = DynamicMap(fn).redim(Default='New') self.assertEqual(dmap.kdims[0].name, 'New') + def test_redim_dimension_range_aux(self): + fn = lambda i: Image(sine_array(0,i)) + dmap = DynamicMap(fn).redim.range(Default=(0,1)) + self.assertEqual(dmap.kdims[0].range, (0,1)) + + def test_redim_dimension_unit_aux(self): + fn = lambda i: Image(sine_array(0,i)) + dmap = DynamicMap(fn).redim.unit(Default='m/s') + self.assertEqual(dmap.kdims[0].unit, 'm/s') + + def test_redim_dimension_type_aux(self): + fn = lambda i: Image(sine_array(0,i)) + dmap = DynamicMap(fn).redim.type(Default=int) + self.assertEqual(dmap.kdims[0].type, int) + def test_deep_redim_dimension_name(self): fn = lambda i: Image(sine_array(0,i)) dmap = DynamicMap(fn).redim(x='X') diff --git a/tests/testndmapping.py b/tests/testndmapping.py index 181ca3010b..c0f7338098 100644 --- a/tests/testndmapping.py +++ b/tests/testndmapping.py @@ -121,6 +121,21 @@ def test_idxmapping_redim(self): self.assertEqual(redimmed.kdims, [Dimension('Integer', type=int), Dimension('floatdim', type=float)]) + def test_idxmapping_redim_range_aux(self): + data = [((0, 0.5), 'a'), ((1, 0.5), 'b')] + ndmap = MultiDimensionalMapping(data, kdims=[self.dim1, self.dim2]) + redimmed = ndmap.redim.range(intdim=(-9,9)) + self.assertEqual(redimmed.kdims, [Dimension('intdim', type=int, range=(-9,9)), + Dimension('floatdim', type=float)]) + + def test_idxmapping_redim_type_aux(self): + data = [((0, 0.5), 'a'), ((1, 0.5), 'b')] + ndmap = MultiDimensionalMapping(data, kdims=[self.dim1, self.dim2]) + redimmed = ndmap.redim.type(intdim=str) + self.assertEqual(redimmed.kdims, [Dimension('intdim', type=str), + Dimension('floatdim', type=float)]) + + def test_idxmapping_add_dimension(self): ndmap = MultiDimensionalMapping(self.init_items_1D_list, kdims=[self.dim1]) ndmap2d = ndmap.add_dimension(self.dim2, 0, 0.5)