Source code for holoviews.element.path

"""
The path module provides a set of elements to draw paths and polygon
geometries in 2D space. In addition to three general elements are
Path, Contours and Polygons, it defines a number of elements to
quickly draw common shapes.
"""

import numpy as np

import param
from ..core import Dataset
from ..core.data import MultiInterface
from ..core.dimension import Dimension, asdim
from ..core.util import OrderedDict, disable_constant
from .geom import Geometry
from .selection import SelectionPolyExpr


[docs]class Path(SelectionPolyExpr, Geometry): """ The Path element represents one or more of path geometries with associated values. Each path geometry may be split into sub-geometries on NaN-values and may be associated with scalar values or array values varying along its length. In analogy to GEOS geometry types a Path is a collection of LineString and MultiLineString geometries with associated values. Like all other elements a Path may be defined through an extensible list of interfaces. Natively, HoloViews provides the MultiInterface which allows representing paths as lists of regular columnar data objects including arrays, dataframes and dictionaries of column arrays and scalars. The canonical representation is a list of dictionaries storing the x- and y-coordinates along with any other values: [{'x': 1d-array, 'y': 1d-array, 'value': scalar, 'continuous': 1d-array}, ...] Alternatively Path also supports a single columnar data-structure to specify an individual path: {'x': 1d-array, 'y': 1d-array, 'value': scalar, 'continuous': 1d-array} Both scalar values and values continuously varying along the geometries coordinates a Path may be used vary visual properties of the paths such as the color. Since not all formats allow storing scalar values as actual scalars, arrays that are the same length as the coordinates but have only one unique value are also considered scalar. The easiest way of accessing the individual geometries is using the `Path.split` method, which returns each path geometry as a separate entity, while the other methods assume a flattened representation where all paths are separated by NaN values. """ group = param.String(default="Path", constant=True) datatype = param.ObjectSelector(default=[ 'multitabular', 'spatialpandas', 'dask_spatialpandas'] ) def __init__(self, data, kdims=None, vdims=None, **params): if isinstance(data, tuple) and len(data) == 2: # Add support for (x, ys) where ys defines multiple paths x, y = map(np.asarray, data) if y.ndim > 1: if len(x) != y.shape[0]: raise ValueError("Path x and y values must be the same length.") data = [np.column_stack((x, y[:, i])) for i in range(y.shape[1])] elif isinstance(data, list) and all(isinstance(path, Path) for path in data): # Allow unpacking of a list of Path elements kdims = kdims or self.kdims paths = [] for path in data: if path.kdims != kdims: redim = {okd.name: nkd for okd, nkd in zip(path.kdims, kdims)} path = path.redim(**redim) if path.interface.multi and isinstance(path.data, list): paths += path.data else: paths.append(path.data) data = paths super(Path, self).__init__(data, kdims=kdims, vdims=vdims, **params) def __getitem__(self, key): if isinstance(key, np.ndarray): return self.select(selection_mask=np.squeeze(key)) if key in self.dimensions(): return self.dimension_values(key) if not isinstance(key, tuple) or len(key) == 1: key = (key, slice(None)) elif len(key) == 0: return self.clone() if not all(isinstance(k, slice) for k in key): raise KeyError("%s only support slice indexing" % self.__class__.__name__) xkey, ykey = key xstart, xstop = xkey.start, xkey.stop ystart, ystop = ykey.start, ykey.stop return self.clone(extents=(xstart, ystart, xstop, ystop)) def select(self, selection_expr=None, selection_specs=None, **selection): """Applies selection by dimension name Applies a selection along the dimensions of the object using keyword arguments. The selection may be narrowed to certain objects using selection_specs. For container objects the selection will be applied to all children as well. Selections may select a specific value, slice or set of values: * value: Scalar values will select rows along with an exact match, e.g.: ds.select(x=3) * slice: Slices may be declared as tuples of the upper and lower bound, e.g.: ds.select(x=(0, 3)) * values: A list of values may be selected using a list or set, e.g.: ds.select(x=[0, 1, 2]) * predicate expression: A holoviews.dim expression, e.g.: from holoviews import dim ds.select(selection_expr=dim('x') % 2 == 0) Args: selection_expr: holoviews.dim predicate expression specifying selection. selection_specs: List of specs to match on A list of types, functions, or type[.group][.label] strings specifying which objects to apply the selection on. **selection: Dictionary declaring selections by dimension Selections can be scalar values, tuple ranges, lists of discrete values and boolean arrays Returns: Returns an Dimensioned object containing the selected data or a scalar if a single value was selected """ xdim, ydim = self.kdims[:2] x_range = selection.pop(xdim.name, None) y_range = selection.pop(ydim.name, None) sel = super(Path, self).select(selection_expr, selection_specs, **selection) if x_range is None and y_range is None: return sel x_range = x_range if isinstance(x_range, slice) else slice(None) y_range = y_range if isinstance(y_range, slice) else slice(None) return sel[x_range, y_range] def split(self, start=None, end=None, datatype=None, **kwargs): """ The split method allows splitting a Path type into a list of subpaths of the same type. A start and/or end may be supplied to select a subset of paths. """ if not self.interface.multi: if not len(self): return [] elif datatype == 'array': obj = self.array(**kwargs) elif datatype == 'dataframe': obj = self.dframe(**kwargs) elif datatype in ('columns', 'dictionary'): obj = self.columns(**kwargs) elif datatype is None: obj = self.clone([self.data]) else: raise ValueError("%s datatype not support" % datatype) return [obj] return self.interface.split(self, start, end, datatype, **kwargs) # Deprecated methods
[docs] @classmethod def collapse_data(cls, data_list, function=None, kdims=None, **kwargs): param.main.param.warning( 'Path.collapse_data is deprecated, collapsing may now ' 'be performed through concatenation and aggregation.') if function is None: return [path for paths in data_list for path in paths] else: raise Exception("Path types are not uniformly sampled and" "therefore cannot be collapsed with a function.")
def __setstate__(self, state): """ Ensures old-style unpickled Path types without an interface use the MultiInterface. Note: Deprecate as part of 2.0 """ self.__dict__ = state if 'interface' not in state: self.interface = MultiInterface super(Dataset, self).__setstate__(state)
[docs]class Contours(Path): """ The Contours element is a subtype of a Path which is characterized by the fact that each path geometry may only be associated with scalar values. It supports all the same data formats as a `Path` but does not allow continuously varying values along the path geometry's coordinates. Conceptually Contours therefore represent iso-contours or isoclines, i.e. a function of two variables which describes a curve along which the function has a constant value. The canonical representation is a list of dictionaries storing the x- and y-coordinates along with any other (scalar) values: [{'x': 1d-array, 'y': 1d-array, 'value': scalar}, ...] Alternatively Contours also supports a single columnar data-structure to specify an individual contour: {'x': 1d-array, 'y': 1d-array, 'value': scalar, 'continuous': 1d-array} Since not all formats allow storing scalar values as actual scalars arrays which are the same length as the coordinates but have only one unique value are also considered scalar. This is strictly enforced, ensuring that each path geometry represents a valid iso-contour. The easiest way of accessing the individual geometries is using the `Contours.split` method, which returns each path geometry as a separate entity, while the other methods assume a flattened representation where all paths are separated by NaN values. """ level = param.Number(default=None, doc=""" Optional level associated with the set of Contours.""") vdims = param.List(default=[], constant=True, doc=""" Contours optionally accept a value dimension, corresponding to the supplied values.""") group = param.String(default='Contours', constant=True) _level_vdim = Dimension('Level') # For backward compatibility def __init__(self, data, kdims=None, vdims=None, **params): data = [] if data is None else data if params.get('level') is not None: self.param.warning( "The level parameter on %s elements is deprecated, " "supply the value dimension(s) as columns in the data.", type(self).__name__) vdims = vdims or [self._level_vdim] params['vdims'] = [] else: params['vdims'] = vdims super(Contours, self).__init__(data, kdims=kdims, **params) if params.get('level') is not None: with disable_constant(self): self.vdims = [asdim(d) for d in vdims] def dimension_values(self, dim, expanded=True, flat=True): dimension = self.get_dimension(dim, strict=True) if dimension in self.vdims and self.level is not None: if expanded: return np.full(len(self), self.level) return np.array([self.level]) return super(Contours, self).dimension_values(dim, expanded, flat)
[docs]class Polygons(Contours): """ The Polygons element represents one or more polygon geometries with associated scalar values. Each polygon geometry may be split into sub-geometries on NaN-values and may be associated with scalar values. In analogy to GEOS geometry types a Polygons element is a collection of Polygon and MultiPolygon geometries. Polygon geometries are defined as a set of coordinates describing the exterior bounding ring and any number of interior holes. Like all other elements a Polygons element may be defined through an extensible list of interfaces. Natively HoloViews provides the MultiInterface which allows representing paths as lists of regular columnar data objects including arrays, dataframes and dictionaries of column arrays and scalars. The canonical representation is a list of dictionaries storing the x- and y-coordinates, a list-of-lists of arrays representing the holes, along with any other values: [{'x': 1d-array, 'y': 1d-array, 'holes': list-of-lists-of-arrays, 'value': scalar}, ...] Alternatively Polygons also supports a single columnar data-structure to specify an individual polygon: {'x': 1d-array, 'y': 1d-array, 'holes': list-of-lists-of-arrays, 'value': scalar} The list-of-lists format of the holes corresponds to the potential for each coordinate array to be split into a multi-geometry through NaN-separators. Each sub-geometry separated by the NaNs therefore has an unambiguous mapping to a list of holes. If a (multi-)polygon has no holes, the 'holes' key may be ommitted. Any value dimensions stored on a Polygons geometry must be scalar, just like the Contours element. Since not all formats allow storing scalar values as actual scalars arrays which are the same length as the coordinates but have only one unique value are also considered scalar. The easiest way of accessing the individual geometries is using the `Polygons.split` method, which returns each path geometry as a separate entity, while the other methods assume a flattened representation where all paths are separated by NaN values. """ group = param.String(default="Polygons", constant=True) vdims = param.List(default=[], doc=""" Polygons optionally accept a value dimension, corresponding to the supplied value.""") _level_vdim = Dimension('Value') # Defines which key the DictInterface uses to look for holes _hole_key = 'holes' @property def has_holes(self): """ Detects whether any polygon in the Polygons element defines holes. Useful to avoid expanding Polygons unless necessary. """ return self.interface.has_holes(self) def holes(self): """ Returns a list-of-lists-of-lists of hole arrays. The three levels of nesting reflects the structure of the polygons: 1. The first level of nesting corresponds to the list of geometries 2. The second level corresponds to each Polygon in a MultiPolygon 3. The third level of nesting allows for multiple holes per Polygon """ return self.interface.holes(self)
[docs]class BaseShape(Path): """ A BaseShape is a Path that can be succinctly expressed by a small number of parameters instead of a full path specification. For instance, a circle may be expressed by the center position and radius instead of an explicit list of path coordinates. """ __abstract = True def __new__(cls, *args, **kwargs): return super(Dataset, cls).__new__(cls) def __init__(self, **params): super(BaseShape, self).__init__([], **params) self.interface = MultiInterface
[docs] def clone(self, *args, **overrides): """ Returns a clone of the object with matching parameter values containing the specified args and kwargs. """ link = overrides.pop('link', True) settings = dict(self.param.get_param_values(), **overrides) if 'id' not in settings: settings['id'] = self.id if not args and link: settings['plot_id'] = self._plot_id pos_args = getattr(self, '_' + type(self).__name__ + '__pos_params', []) return self.__class__(*(settings[n] for n in pos_args), **{k:v for k,v in settings.items() if k not in pos_args})
[docs]class Box(BaseShape): """ Draw a centered box of a given width at the given position with the specified aspect ratio (if any). """ x = param.Number(default=0, doc="The x-position of the box center.") y = param.Number(default=0, doc="The y-position of the box center.") width = param.Number(default=1, doc="The width of the box.") height = param.Number(default=1, doc="The height of the box.") orientation = param.Number(default=0, doc=""" Orientation in the Cartesian coordinate system, the counterclockwise angle in radians between the first axis and the horizontal.""") aspect= param.Number(default=1.0, doc=""" Optional multiplier applied to the box size to compute the width in cases where only the length value is set.""") group = param.String(default='Box', constant=True, doc="The assigned group name.") __pos_params = ['x','y', 'height'] def __init__(self, x, y, spec, **params): if isinstance(spec, tuple): if 'aspect' in params: raise ValueError('Aspect parameter not supported when supplying ' '(width, height) specification.') (width, height ) = spec else: width, height = params.get('width', spec), spec params['width']=params.get('width',width) super(Box, self).__init__(x=x, y=y, height=height, **params) half_width = (self.width * self.aspect)/ 2.0 half_height = self.height / 2.0 (l,b,r,t) = (-half_width, -half_height, half_width, half_height) box = np.array([(l, b), (l, t), (r, t), (r, b),(l, b)]) rot = np.array([[np.cos(self.orientation), -np.sin(self.orientation)], [np.sin(self.orientation), np.cos(self.orientation)]]) xs, ys = np.tensordot(rot, box.T, axes=[1,0]) self.data = [np.column_stack([xs+x, ys+y])]
[docs]class Ellipse(BaseShape): """ Draw an axis-aligned ellipse at the specified x,y position with the given orientation. The simplest (default) Ellipse is a circle, specified using: Ellipse(x,y, diameter) A circle is a degenerate ellipse where the width and height are equal. To specify these explicitly, you can use: Ellipse(x,y, (width, height)) There is also an aspect parameter allowing you to generate an ellipse by specifying a multiplicating factor that will be applied to the height only. Note that as a subclass of Path, internally an Ellipse is a sequence of (x,y) sample positions. Ellipse could also be implemented as an annotation that uses a dedicated ellipse artist. """ x = param.Number(default=0, doc="The x-position of the ellipse center.") y = param.Number(default=0, doc="The y-position of the ellipse center.") width = param.Number(default=1, doc="The width of the ellipse.") height = param.Number(default=1, doc="The height of the ellipse.") orientation = param.Number(default=0, doc=""" Orientation in the Cartesian coordinate system, the counterclockwise angle in radians between the first axis and the horizontal.""") aspect= param.Number(default=1.0, doc=""" Optional multiplier applied to the diameter to compute the width in cases where only the diameter value is set.""") samples = param.Number(default=100, doc="The sample count used to draw the ellipse.") group = param.String(default='Ellipse', constant=True, doc="The assigned group name.") __pos_params = ['x','y', 'height'] def __init__(self, x, y, spec, **params): if isinstance(spec, tuple): if 'aspect' in params: raise ValueError('Aspect parameter not supported when supplying ' '(width, height) specification.') (width, height) = spec else: width, height = params.get('width', spec), spec params['width']=params.get('width',width) super(Ellipse, self).__init__(x=x, y=y, height=height, **params) angles = np.linspace(0, 2*np.pi, self.samples) half_width = (self.width * self.aspect)/ 2.0 half_height = self.height / 2.0 #create points ellipse = np.array( list(zip(half_width*np.sin(angles), half_height*np.cos(angles)))) #rotate ellipse and add offset rot = np.array([[np.cos(self.orientation), -np.sin(self.orientation)], [np.sin(self.orientation), np.cos(self.orientation)]]) self.data = [np.tensordot(rot, ellipse.T, axes=[1,0]).T+np.array([x,y])]
[docs]class Bounds(BaseShape): """ An arbitrary axis-aligned bounding rectangle defined by the (left, bottom, right, top) coordinate positions. If supplied a single real number as input, this value will be treated as the radius of a square, zero-center box which will be used to compute the corresponding lbrt tuple. """ lbrt = param.Tuple(default=(-0.5, -0.5, 0.5, 0.5), doc=""" The (left, bottom, right, top) coordinates of the bounding box.""") group = param.String(default='Bounds', constant=True, doc="The assigned group name.") __pos_params = ['lbrt'] def __init__(self, lbrt, **params): if not isinstance(lbrt, tuple): lbrt = (-lbrt, -lbrt, lbrt, lbrt) super(Bounds, self).__init__(lbrt=lbrt, **params) (l,b,r,t) = self.lbrt xdim, ydim = self.kdims self.data = [OrderedDict([(xdim.name, np.array([l, l, r, r, l])), (ydim.name, np.array([b, t, t, b, b]))])]