Skip to content

Commit

Permalink
Merge pull request #1302 from ioam/redim_improvements
Browse files Browse the repository at this point in the history
Unified redim implementation with convenience methods
  • Loading branch information
philippjfr authored Apr 17, 2017
2 parents 1b15b98 + b1f662d commit f2bf8d6
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 97 deletions.
27 changes: 3 additions & 24 deletions holoviews/core/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down
174 changes: 117 additions & 57 deletions holoviews/core/dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<holoviews.core.dimension.redim method>"

@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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
18 changes: 2 additions & 16 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions tests/testannotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
8 changes: 8 additions & 0 deletions tests/testdataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
10 changes: 10 additions & 0 deletions tests/testdimensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
15 changes: 15 additions & 0 deletions tests/testdynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
15 changes: 15 additions & 0 deletions tests/testndmapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit f2bf8d6

Please sign in to comment.