diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 9e00a837c1..a159f297a1 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -393,13 +393,15 @@ def groupby(self, dimensions=[], container_type=HoloMap, group_type=None, if dynamic: group_dims = [d.name for d in self.kdims if d not in dimensions] + group_kwargs = dict(util.get_param_values(self), **kwargs) + group_kwargs['kdims'] = [self.get_dimension(d) for d in group_dims] def load_subset(*args): constraint = dict(zip(dim_names, args)) group = self.select(**constraint) if np.isscalar(group): return group_type(([group],), group=self.group, label=self.label, vdims=self.vdims) - return group_type(group.reindex(group_dims)) + return group_type(group.reindex(group_dims), **group_kwargs) dynamic_dims = [d(values=list(self.interface.values(self, d.name, False))) for d in dimensions] return DynamicMap(load_subset, kdims=dynamic_dims) diff --git a/holoviews/core/data/array.py b/holoviews/core/data/array.py index 435319a77c..4c15432327 100644 --- a/holoviews/core/data/array.py +++ b/holoviews/core/data/array.py @@ -72,7 +72,7 @@ def validate(cls, dataset): ncols = dataset.data.shape[1] if dataset.data.ndim > 1 else 1 if ncols < ndims: raise ValueError("Supplied data does not match specified " - "dimensions, expected at least %s dataset." % ndims) + "dimensions, expected at least %s columns." % ndims) @classmethod def array(cls, dataset, dimensions): diff --git a/holoviews/core/data/dictionary.py b/holoviews/core/data/dictionary.py index 06574d0b20..7a31b04054 100644 --- a/holoviews/core/data/dictionary.py +++ b/holoviews/core/data/dictionary.py @@ -56,7 +56,10 @@ def init(cls, eltype, data, kdims, vdims): data = {k: data[:,i] for i,k in enumerate(dimensions)} elif isinstance(data, list) and np.isscalar(data[0]): data = {dimensions[0]: np.arange(len(data)), dimensions[1]: data} - elif not isinstance(data, dict): + # Ensure that interface does not consume data of other types + # with an iterator interface + elif not any(isinstance(data, tuple(t for t in interface.types if t is not None)) + for interface in cls.interfaces.values()): data = {k: v for k, v in zip(dimensions, zip(*data))} elif isinstance(data, dict) and not all(d in data for d in dimensions): dict_data = zip(*((util.wrap_tuple(k)+util.wrap_tuple(v)) @@ -84,7 +87,7 @@ def validate(cls, dataset): raise ValueError('Following dimensions not found in data: %s' % not_found) lengths = [len(dataset.data[dim]) for dim in dimensions] if len({l for l in lengths if l > 1}) > 1: - raise ValueError('Length of dataset do not match') + raise ValueError('Length of columns do not match') @classmethod @@ -132,8 +135,8 @@ def concat(cls, dataset_objs): cast_objs = cls.cast(dataset_objs) cols = set(tuple(c.data.keys()) for c in cast_objs) if len(cols) != 1: - raise Exception("In order to concatenate, all Column objects " - "should have matching set of dataset.") + raise Exception("In order to concatenate, all Dataset objects " + "should have matching set of columns.") concatenated = OrderedDict() for column in cols.pop(): concatenated[column] = np.concatenate([obj[column] for obj in cast_objs]) diff --git a/holoviews/core/data/grid.py b/holoviews/core/data/grid.py index 683007c86c..36c533b1b7 100644 --- a/holoviews/core/data/grid.py +++ b/holoviews/core/data/grid.py @@ -37,6 +37,8 @@ class GridInterface(DictInterface): datatype = 'grid' + gridded = True + @classmethod def init(cls, eltype, data, kdims, vdims): if kdims is None: @@ -53,8 +55,8 @@ def init(cls, eltype, data, kdims, vdims): if isinstance(data, tuple): data = {d: v for d, v in zip(dimensions, data)} elif not isinstance(data, dict): - raise ValueError('GridInterface must be instantiated as a ' - 'dictionary or tuple') + raise TypeError('GridInterface must be instantiated as a ' + 'dictionary or tuple') for dim in kdims+vdims: name = dim.name if isinstance(dim, Dimension) else dim @@ -71,7 +73,7 @@ def init(cls, eltype, data, kdims, vdims): if shape != expected[::-1] and not (not expected and shape == (1,)): raise ValueError('Key dimension values and value array %s ' 'shape do not match. Expected shape %s, ' - 'actual shape: %s' % (vdim, expected, shape)) + 'actual shape: %s' % (vdim, expected[::-1], shape)) return data, {'kdims':kdims, 'vdims':vdims}, {} @@ -101,19 +103,76 @@ def length(cls, dataset): return np.product([len(dataset.data[d.name]) for d in dataset.kdims]) + @classmethod + def coords(cls, dataset, dim, ordered=False, expanded=False): + """ + Returns the coordinates along a dimension. Ordered ensures + coordinates are in ascending order and expanded creates + ND-array matching the dimensionality of the dataset. + """ + if expanded: + return util.expand_grid_coords(dataset, dim) + data = dataset.data[dim] + if ordered and np.all(data[1:] < data[:-1]): + data = data[::-1] + return data + + + @classmethod + def canonicalize(cls, dataset, data, coord_dims=None): + """ + Canonicalize takes an array of values as input and + reorients and transposes it to match the canonical + format expected by plotting functions. In addition + to the dataset and the particular array to apply + transforms to a list of coord_dims may be supplied + in case the array indexing does not match the key + dimensions of the dataset. + """ + if coord_dims is None: + coord_dims = dataset.dimensions('key', True) + + # Reorient data + invert = False + slices = [] + for d in coord_dims: + coords = cls.coords(dataset, d) + if np.all(coords[1:] < coords[:-1]): + slices.append(slice(None, None, -1)) + invert = True + else: + slices.append(slice(None)) + data = data.__getitem__(slices[::-1]) if invert else data + + # Transpose data + dims = [name for name in coord_dims[::-1] + if isinstance(cls.coords(dataset, name), np.ndarray)] + dropped = [dims.index(d) for d in dims if d not in dataset.kdims] + inds = [dims.index(kd.name) for kd in dataset.kdims] + inds += dropped + if inds: + data = data.transpose(inds[::-1]) + + # Allow lower dimensional views into data + if len(dataset.kdims) < 2: + data = data.flatten() + elif dropped: + data = data.squeeze(axis=tuple(range(len(dropped)))) + return data + + @classmethod def values(cls, dataset, dim, expanded=True, flat=True): - if dim in dataset.kdims: - if not expanded: - return dataset.data[dim] - prod = util.cartesian_product([dataset.data[d.name] for d in dataset.kdims]) - idx = dataset.get_dimension_index(dim) - values = prod[idx] - return values.flatten() if flat else values - else: + if dim in dataset.vdims: dim = dataset.get_dimension(dim) - values = dataset.data.get(dim.name) - return values.T.flatten() if flat else values + data = dataset.data.get(dim.name) + data = cls.canonicalize(dataset, data) + return data.T.flatten() if flat else data + elif expanded: + data = cls.coords(dataset, dim, expanded=True) + return data.flatten() if flat else data + else: + return cls.coords(dataset, dim, ordered=True) @classmethod @@ -282,17 +341,17 @@ def reindex(cls, dataset, kdims, vdims): if k not in dropped_kdims+dropped_vdims} if kdims != dataset.kdims: - dropped_axes = tuple(dataset.ndims-dataset.kdims.index(d)-1 + joined_dims = kdims+dropped_kdims + axes = tuple(dataset.ndims-dataset.kdims.index(d)-1 + for d in joined_dims) + dropped_axes = tuple(dataset.ndims-joined_dims.index(d)-1 for d in dropped_kdims) - old_kdims = [d for d in dataset.kdims if not d in dropped_kdims] - axes = tuple(dataset.ndims-old_kdims.index(d)-1 - for d in kdims) for vdim in vdims: vdata = data[vdim.name] + if len(axes) > 1: + vdata = vdata.transpose(axes[::-1]) if dropped_axes: vdata = vdata.squeeze(axis=dropped_axes) - if len(axes) > 1: - vdata = np.transpose(vdata, axes) data[vdim.name] = vdata return data diff --git a/holoviews/core/data/interface.py b/holoviews/core/data/interface.py index c9fceb348f..ab5a0b5278 100644 --- a/holoviews/core/data/interface.py +++ b/holoviews/core/data/interface.py @@ -11,6 +11,8 @@ class Interface(param.Parameterized): datatype = None + gridded = False + @classmethod def register(cls, interface): cls.interfaces[interface.datatype] = interface diff --git a/holoviews/core/data/iris.py b/holoviews/core/data/iris.py index d4c13c1abf..732e3f44dc 100644 --- a/holoviews/core/data/iris.py +++ b/holoviews/core/data/iris.py @@ -93,7 +93,7 @@ def init(cls, eltype, data, kdims, vdims): except: pass if not isinstance(data, iris.cube.Cube): - raise TypeError('Data must be be an iris dataset type.') + raise TypeError('Data must be be an iris Cube type.') if kdims: coords = [] @@ -118,6 +118,16 @@ def validate(cls, dataset): pass + @classmethod + def coords(cls, dataset, dim, ordered=False, expanded=False): + if expanded: + return util.expand_grid_coords(dataset, dim) + data = dataset.data.coords(dim)[0].points + if ordered and np.all(data[1:] < data[:-1]): + data = data[::-1] + return data + + @classmethod def values(cls, dataset, dim, expanded=True, flat=True): """ @@ -125,23 +135,15 @@ def values(cls, dataset, dim, expanded=True, flat=True): """ dim = dataset.get_dimension(dim) if dim in dataset.vdims: - data = dataset.data.copy().data - coord_names = [c.name() for c in dataset.data.dim_coords - if c.name() in dataset.kdims] - if flat: - dim_inds = [coord_names.index(d.name) for d in dataset.kdims] - dim_inds += [i for i in range(len(dataset.data.dim_coords)) - if i not in dim_inds] - data = data.transpose(dim_inds) - else: - data = np.flipud(data) + coord_names = [c.name() for c in dataset.data.dim_coords] + data = dataset.data.copy().data.T + data = cls.canonicalize(dataset, data, coord_names) + return data.T.flatten() if flat else data elif expanded: - idx = dataset.get_dimension_index(dim) - data = util.cartesian_product([dataset.data.coords(d.name)[0].points - for d in dataset.kdims])[idx] + data = cls.coords(dataset, dim, expanded=True) + return data.flatten() if flat else data else: - data = dataset.data.coords(dim.name)[0].points - return data.flatten() if flat else data + return cls.coords(dataset, dim.name, ordered=True) @classmethod diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index abbd2bcb12..6cca1a83b9 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -40,37 +40,40 @@ def init(cls, eltype, data, kdims, vdims): vdim_param = element_params['vdims'] if kdims: - kdim_names = [kd.name if isinstance(kd, Dimension) else kd for kd in kdims] + kdim_names = [kd.name if isinstance(kd, Dimension) + else kd for kd in kdims] else: kdim_names = [kd.name for kd in eltype.kdims] if not isinstance(data, xr.Dataset): - ndims = len(kdim_names) kdims = [kd if isinstance(kd, Dimension) else Dimension(kd) for kd in kdims] - vdim = vdims[0].name if isinstance(vdims[0], Dimension) else vdims[0] + vdims = [vd if isinstance(vd, Dimension) else Dimension(vd) + for vd in vdims] if isinstance(data, tuple): - value_array = np.array(data[-1]) - data = {d: vals for d, vals in zip(kdim_names + [vdim], data)} - elif isinstance(data, dict): - value_array = np.array(data[vdim]) - if value_array.ndim > 1: - value_array = value_array.T - dims, coords = zip(*[(kd.name, data[kd.name]) - for kd in kdims]) + data = {d.name: vals for d, vals in zip(kdims + vdims, data)} + if not isinstance(data, dict): + raise TypeError('XArrayInterface could not interpret data type') + coords = [(kd.name, data[kd.name]) for kd in kdims][::-1] + arrays = {} + for vdim in vdims: + arr = data[vdim.name] + if not isinstance(arr, xr.DataArray): + arr = xr.DataArray(arr, coords=coords) + arrays[vdim.name] = arr try: - arr = xr.DataArray(value_array, coords=coords, dims=dims) - data = xr.Dataset({vdim: arr}) + data = xr.Dataset(arrays) except: pass - if not isinstance(data, xr.Dataset): - raise TypeError('Data must be be an xarray Dataset type.') - - if isinstance(data, xr.Dataset): + else: if vdims is None: vdims = list(data.data_vars.keys()) if kdims is None: - kdims = list(data.dims.keys()) + kdims = [name for name in data.dims + if isinstance(data[name].data, np.ndarray)] + + if not isinstance(data, xr.Dataset): + raise TypeError('Data must be be an xarray Dataset type.') return data, {'kdims': kdims, 'vdims': vdims}, {} @@ -116,20 +119,28 @@ def groupby(cls, dataset, dimensions, container_type, group_type, **kwargs): return container_type(data) + @classmethod + def coords(cls, dataset, dim, ordered=False, expanded=False): + if expanded: + return util.expand_grid_coords(dataset, dim) + data = dataset.data[dim].data + if ordered and np.all(data[1:] < data[:-1]): + data = data[::-1] + return data + + @classmethod def values(cls, dataset, dim, expanded=True, flat=True): data = dataset.data[dim].data if dim in dataset.vdims: - if data.ndim == 1: - return np.array(data) - else: - return data.T.flatten() if flat else data - elif not expanded: - return data + coord_dims = dataset.data[dim].dims[::-1] + data = cls.canonicalize(dataset, data, coord_dims=coord_dims) + return data.T.flatten() if flat else data + elif expanded: + data = cls.coords(dataset, dim, expanded=True) + return data.flatten() if flat else data else: - arrays = [dataset.data[d.name].data for d in dataset.kdims] - product = util.cartesian_product(arrays)[dataset.get_dimension_index(dim)] - return product.flatten() if flat else product + return cls.coords(dataset, dim, ordered=True) @classmethod diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 2f2afe91ea..7e558c268f 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -904,3 +904,16 @@ def get_dynamic_item(map_obj, dimensions, key): else: el = None return key, el + + +def expand_grid_coords(dataset, dim): + """ + Expand the coordinates along a dimension of the gridded + dataset into an ND-array matching the dimensionality of + the dataset. + """ + arrays = [dataset.interface.coords(dataset, d.name, True) + for d in dataset.kdims] + idx = dataset.get_dimension_index(dim) + return cartesian_product(arrays)[idx] + diff --git a/holoviews/element/raster.py b/holoviews/element/raster.py index b2bf91e21a..2d780d728e 100644 --- a/holoviews/element/raster.py +++ b/holoviews/element/raster.py @@ -6,7 +6,8 @@ import param from ..core import util -from ..core.data import (ArrayInterface, NdElementInterface, DictInterface) +from ..core.data import (ArrayInterface, NdElementInterface, + DictInterface, GridInterface) from ..core import (Dimension, NdMapping, Element2D, Overlay, Element, Dataset, NdElement) from ..core.boundingregion import BoundingRegion, BoundingBox @@ -396,6 +397,8 @@ def __init__(self, data, extents=None, **params): def _compute_raster(self): + if self.interface.gridded: + return self, np.flipud(self.dimension_values(2, flat=False)) d1keys = self.dimension_values(0, False) d2keys = self.dimension_values(1, False) coords = [(d1, d2, np.NaN) for d1 in d1keys for d2 in d2keys] @@ -648,16 +651,17 @@ class GridImage(Dataset, Element2D): group = param.String(default='GridImage', constant=True) - kdims = param.List(default=['x', 'y'], bounds=(2, 2)) + kdims = param.List(default=[Dimension('x'), Dimension('y')], + bounds=(2, 2)) - vdims = param.List(default=['z'], bounds=(1, 1)) + vdims = param.List(default=[Dimension('z')], bounds=(1, 1)) def __init__(self, data, **params): super(GridImage, self).__init__(data, **params) (l, r), (b, t) = self.interface.range(self, 0), self.interface.range(self, 1) (ys, xs) = self.dimension_values(2, flat=False).shape - xsampling = (float(r-l)/xs)/2. - ysampling = (float(t-b)/ys)/2. + xsampling = (float(r-l)/(xs-1))/2. + ysampling = (float(t-b)/(ys-1))/2. l, r = l-xsampling, r+xsampling b, t = b-ysampling, t+ysampling self.bounds = BoundingBox(points=((l, b), (r, t))) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index 6763832114..a6183c927c 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -82,7 +82,7 @@ def _update_glyph(self, glyph, properties, mapping): class ImagePlot(RasterPlot): def get_data(self, element, ranges=None, empty=False): - img = np.flipud(element.dimension_values(2, flat=False)) + img = element.dimension_values(2, flat=False) l, b, r, t = element.bounds.lbrt() dh, dw = t-b, r-l mapping = dict(image='image', x='x', y='y', dw='dw', dh='dh') diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index c6d137f801..a734fecf39 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -183,7 +183,7 @@ def update_handles(self, key, axis, element, ranges, style): class ImagePlot(RasterPlot): def get_data(self, element, ranges, style): - data = element.dimension_values(2, flat=False) + data = np.flipud(element.dimension_values(2, flat=False)) data = np.ma.array(data, mask=np.logical_not(np.isfinite(data))) vdim = element.vdims[0] self._norm_kwargs(element, ranges, style, vdim) diff --git a/tests/testdataset.py b/tests/testdataset.py index 097b69e1b7..5690919300 100644 --- a/tests/testdataset.py +++ b/tests/testdataset.py @@ -437,6 +437,95 @@ def init_data(self): self.y_ints = [i*2 for i in range(11)] self.dataset_hm = Dataset((self.xs, self.y_ints), kdims=['x'], vdims=['y']) + self.grid_xs = [0, 1] + self.grid_ys = [0.1, 0.2, 0.3] + self.grid_zs = [[0, 1], [2, 3], [4, 5]] + self.dataset_grid = Dataset((self.grid_xs, self.grid_ys, + self.grid_zs), kdims=['x', 'y'], + vdims=['z']) + self.dataset_grid_inv = Dataset((self.grid_xs[::-1], self.grid_ys[::-1], + self.grid_zs), kdims=['x', 'y'], + vdims=['z']) + + def test_dataset_dim_vals_grid_kdims_xs(self): + self.assertEqual(self.dataset_grid.dimension_values(0, expanded=False), + np.array([0, 1])) + + def test_dataset_dim_vals_grid_kdims_xs_inv(self): + self.assertEqual(self.dataset_grid_inv.dimension_values(0, expanded=False), + np.array([0, 1])) + + def test_dataset_dim_vals_grid_kdims_expanded_xs_flat(self): + expanded_xs = np.array([0, 0, 0, 1, 1, 1]) + self.assertEqual(self.dataset_grid.dimension_values(0), + expanded_xs) + + def test_dataset_dim_vals_grid_kdims_expanded_xs_flat_inv(self): + expanded_xs = np.array([0, 0, 0, 1, 1, 1]) + self.assertEqual(self.dataset_grid_inv.dimension_values(0), + expanded_xs) + + def test_dataset_dim_vals_grid_kdims_expanded_xs(self): + expanded_xs = np.array([[0, 0, 0], [1, 1, 1]]) + self.assertEqual(self.dataset_grid.dimension_values(0, flat=False), + expanded_xs) + + def test_dataset_dim_vals_grid_kdims_expanded_xs_inv(self): + expanded_xs = np.array([[0, 0, 0], [1, 1, 1]]) + self.assertEqual(self.dataset_grid_inv.dimension_values(0, flat=False), + expanded_xs) + + def test_dataset_dim_vals_grid_kdims_ys(self): + self.assertEqual(self.dataset_grid.dimension_values(1, expanded=False), + np.array([0.1, 0.2, 0.3])) + + def test_dataset_dim_vals_grid_kdims_ys_inv(self): + self.assertEqual(self.dataset_grid_inv.dimension_values(1, expanded=False), + np.array([0.1, 0.2, 0.3])) + + def test_dataset_dim_vals_grid_kdims_expanded_ys_flat(self): + expanded_ys = np.array([0.1, 0.2, 0.3, + 0.1, 0.2, 0.3]) + self.assertEqual(self.dataset_grid.dimension_values(1), + expanded_ys) + + def test_dataset_dim_vals_grid_kdims_expanded_ys_flat_inv(self): + expanded_ys = np.array([0.1, 0.2, 0.3, + 0.1, 0.2, 0.3]) + self.assertEqual(self.dataset_grid_inv.dimension_values(1), + expanded_ys) + + def test_dataset_dim_vals_grid_kdims_expanded_ys(self): + expanded_ys = np.array([[0.1, 0.2, 0.3], + [0.1, 0.2, 0.3]]) + self.assertEqual(self.dataset_grid.dimension_values(1, flat=False), + expanded_ys) + + def test_dataset_dim_vals_grid_kdims_expanded_ys_inv(self): + expanded_ys = np.array([[0.1, 0.2, 0.3], + [0.1, 0.2, 0.3]]) + self.assertEqual(self.dataset_grid_inv.dimension_values(1, flat=False), + expanded_ys) + + def test_dataset_dim_vals_grid_vdims_zs_flat(self): + expanded_zs = np.array([0, 2, 4, 1, 3, 5]) + self.assertEqual(self.dataset_grid.dimension_values(2), + expanded_zs) + + def test_dataset_dim_vals_grid_vdims_zs_flat_inv(self): + expanded_zs = np.array([5, 3, 1, 4, 2, 0]) + self.assertEqual(self.dataset_grid_inv.dimension_values(2), + expanded_zs) + + def test_dataset_dim_vals_grid_vdims_zs(self): + expanded_zs = np.array([[0, 1], [2, 3], [4, 5]]) + self.assertEqual(self.dataset_grid.dimension_values(2, flat=False), + expanded_zs) + + def test_dataset_dim_vals_grid_vdims_zs_inv(self): + expanded_zs = np.array([[5, 4], [3, 2], [1, 0]]) + self.assertEqual(self.dataset_grid_inv.dimension_values(2, flat=False), + expanded_zs) def test_dataset_array_init_hm(self): "Tests support for arrays (homogeneous)" diff --git a/tests/testirisinterface.py b/tests/testirisinterface.py index 9c44859955..7386c8cf85 100644 --- a/tests/testirisinterface.py +++ b/tests/testirisinterface.py @@ -51,10 +51,10 @@ def test_dimension_values_kdim(self): def test_dimension_values_vdim(self): cube = Dataset(self.cube, kdims=['longitude', 'latitude']) self.assertEqual(cube.dimension_values('unknown', flat=False), - np.flipud(np.array([[ 0, 4, 8], - [ 1, 5, 9], - [ 2, 6, 10], - [ 3, 7, 11]], dtype=np.int32).T)) + np.array([[ 0, 4, 8], + [ 1, 5, 9], + [ 2, 6, 10], + [ 3, 7, 11]], dtype=np.int32).T) def test_range_kdim(self): cube = Dataset(self.cube, kdims=['longitude', 'latitude'])