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

Unified redim implementation with convenience methods #1302

Merged
merged 7 commits into from
Apr 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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