Source code for holoviews.core.layout

"""
Supplies Pane, Layout, NdLayout and AdjointLayout. Pane extends View
to allow multiple Views to be presented side-by-side in a NdLayout. An
AdjointLayout allows one or two Views to be adjoined to a primary View
to act as supplementary elements.
"""

import numpy as np
import param

from . import traversal
from .dimension import Dimension, Dimensioned, ViewableElement, ViewableTree
from .ndmapping import NdMapping, UniformNdMapping


class Layoutable:
    """
    Layoutable provides a mix-in class to support the
    add operation for creating a layout from the operands.
    """
    def __add__(x, y):
        "Compose objects into a Layout"
        if any(isinstance(arg, int) for arg in (x, y)):
            raise TypeError(f"unsupported operand type(s) for +: {x.__class__.__name__} and {y.__class__.__name__}. "
                            "If you are trying to use a reduction like `sum(elements)` "
                            "to combine a list of elements, we recommend you use "
                            "`Layout(elements)` (and similarly `Overlay(elements)` for "
                            "making an overlay from a list) instead.")
        try:
            return Layout([x, y])
        except NotImplementedError:
            return NotImplemented

    def __radd__(self, other):
        return self.__class__.__add__(other, self)


class Composable(Layoutable):
    """
    Composable is a mix-in class to allow Dimensioned objects to be
    embedded within Layouts and GridSpaces.
    """

    def __lshift__(self, other):
        "Compose objects into an AdjointLayout"
        if isinstance(other, (ViewableElement, NdMapping, Empty)):
            return AdjointLayout([self, other])
        elif isinstance(other, AdjointLayout):
            return AdjointLayout(other.data.values()+[self])
        else:
            raise TypeError(f'Cannot append {type(other).__name__} to a AdjointLayout')



[docs]class Empty(Dimensioned, Composable): """ Empty may be used to define an empty placeholder in a Layout. It can be placed in a Layout just like any regular Element and container type via the + operator or by passing it to the Layout constructor as a part of a list. """ group = param.String(default='Empty') def __init__(self, **params): super().__init__(None, **params)
[docs]class AdjointLayout(Layoutable, Dimensioned): """ An AdjointLayout provides a convenient container to lay out some marginal plots next to a primary plot. This is often useful to display the marginal distributions of a plot next to the primary plot. AdjointLayout accepts a list of up to three elements, which are laid out as follows with the names 'main', 'top' and 'right': _______________ | 3 | | |___________|___| | | | 1: main | | | 2: right | 1 | 2 | 3: top | | | |___________|___| """ kdims = param.List(default=[Dimension('AdjointLayout')], constant=True) layout_order = ['main', 'right', 'top'] _deep_indexable = True _auxiliary_component = False def __init__(self, data, **params): self.main_layer = 0 # The index of the main layer if .main is an overlay if data and len(data) > 3: raise Exception('AdjointLayout accepts no more than three elements.') if data is not None and all(isinstance(v, tuple) for v in data): data = dict(data) if isinstance(data, dict): wrong_pos = [k for k in data if k not in self.layout_order] if wrong_pos: raise Exception('Wrong AdjointLayout positions provided.') elif isinstance(data, list): data = dict(zip(self.layout_order, data)) else: data = {} super().__init__(data, **params) def __mul__(self, other, reverse=False): layer1 = other if reverse else self layer2 = self if reverse else other adjoined_items = [] if isinstance(layer1, AdjointLayout) and isinstance(layer2, AdjointLayout): adjoined_items = [] adjoined_items.append(layer1.main*layer2.main) if layer1.right is not None and layer2.right is not None: if layer1.right.dimensions() == layer2.right.dimensions(): adjoined_items.append(layer1.right*layer2.right) else: adjoined_items += [layer1.right, layer2.right] elif layer1.right is not None: adjoined_items.append(layer1.right) elif layer2.right is not None: adjoined_items.append(layer2.right) if layer1.top is not None and layer2.top is not None: if layer1.top.dimensions() == layer2.top.dimensions(): adjoined_items.append(layer1.top*layer2.top) else: adjoined_items += [layer1.top, layer2.top] elif layer1.top is not None: adjoined_items.append(layer1.top) elif layer2.top is not None: adjoined_items.append(layer2.top) if len(adjoined_items) > 3: raise ValueError("AdjointLayouts could not be overlaid, " "the dimensions of the adjoined plots " "do not match and the AdjointLayout can " "hold no more than two adjoined plots.") elif isinstance(layer1, AdjointLayout): adjoined_items = [layer1.data[o] for o in self.layout_order if o in layer1.data] adjoined_items[0] = layer1.main * layer2 elif isinstance(layer2, AdjointLayout): adjoined_items = [layer2.data[o] for o in self.layout_order if o in layer2.data] adjoined_items[0] = layer1 * layer2.main if adjoined_items: return self.clone(adjoined_items) else: return NotImplemented def __rmul__(self, other): return self.__mul__(other, reverse=True) @property def group(self): "Group inherited from main element" if self.main and self.main.group != type(self.main).__name__: return self.main.group else: return 'AdjointLayout' @property def label(self): "Label inherited from main element" return self.main.label if self.main else '' # Both group and label need empty setters due to param inheritance @group.setter def group(self, group): pass @label.setter def label(self, label): pass
[docs] def relabel(self, label=None, group=None, depth=1): """Clone object and apply new group and/or label. Applies relabeling to child up to the supplied depth. Args: label (str, optional): New label to apply to returned object group (str, optional): New group to apply to returned object depth (int, optional): Depth to which relabel will be applied If applied to container allows applying relabeling to contained objects up to the specified depth Returns: Returns relabelled object """ return super().relabel(label=label, group=group, depth=depth)
[docs] def get(self, key, default=None): """ Returns the viewable corresponding to the supplied string or integer based key. Args: key: Numeric or string index: 0) 'main' 1) 'right' 2) 'top' default: Value returned if key not found Returns: Indexed value or supplied default """ return self.data[key] if key in self.data else default
[docs] def dimension_values(self, dimension, expanded=True, flat=True): """Return the values along the requested dimension. Applies to the main object in the AdjointLayout. Args: dimension: The dimension to return values for expanded (bool, optional): Whether to expand values Whether to return the expanded values, behavior depends on the type of data: * Columnar: If false returns unique values * Geometry: If false returns scalar values per geometry * Gridded: If false returns 1D coordinates flat (bool, optional): Whether to flatten array Returns: NumPy array of values along the requested dimension """ dimension = self.get_dimension(dimension, strict=True).name return self.main.dimension_values(dimension, expanded, flat)
def __getitem__(self, key): "Index into the AdjointLayout by index or label" if key == (): return self data_slice = None if isinstance(key, tuple): data_slice = key[1:] key = key[0] if isinstance(key, int) and key <= len(self): if key == 0: data = self.main if key == 1: data = self.right if key == 2: data = self.top if data_slice: data = data[data_slice] return data elif isinstance(key, str) and key in self.data: if data_slice is None: return self.data[key] else: self.data[key][data_slice] elif isinstance(key, slice) and key.start is None and key.stop is None: return self if data_slice is None else self.clone([el[data_slice] for el in self]) else: raise KeyError(f"Key {key} not found in AdjointLayout.") def __setitem__(self, key, value): if key in ['main', 'right', 'top']: if isinstance(value, (ViewableElement, UniformNdMapping, Empty)): self.data[key] = value else: raise ValueError('AdjointLayout only accepts Element types.') else: raise Exception(f'Position {key} not valid in AdjointLayout.') def __lshift__(self, other): "Add another plot to the AdjointLayout" views = [self.data.get(k, None) for k in self.layout_order] return AdjointLayout([v for v in views if v is not None] + [other]) @property def ddims(self): return self.main.dimensions() @property def main(self): "Returns the main element in the AdjointLayout" return self.data.get('main', None) @property def right(self): "Returns the right marginal element in the AdjointLayout" return self.data.get('right', None) @property def top(self): "Returns the top marginal element in the AdjointLayout" return self.data.get('top', None) @property def last(self): items = [(k, v.last) if isinstance(v, NdMapping) else (k, v) for k, v in self.data.items()] return self.__class__(dict(items)) def keys(self): return list(self.data.keys()) def items(self): return list(self.data.items()) def __iter__(self): i = 0 while i < len(self): yield self[i] i += 1 def __len__(self): "Number of items in the AdjointLayout" return len(self.data)
[docs]class NdLayout(Layoutable, UniformNdMapping): """ NdLayout is a UniformNdMapping providing an n-dimensional data structure to display the contained Elements and containers in a layout. Using the cols method the NdLayout can be rearranged with the desired number of columns. """ data_type = (ViewableElement, AdjointLayout, UniformNdMapping) def __init__(self, initial_items=None, kdims=None, **params): self._max_cols = 4 self._style = None super().__init__(initial_items=initial_items, kdims=kdims, **params) @property def uniform(self): return traversal.uniform(self) @property def shape(self): "Tuple indicating the number of rows and columns in the NdLayout." num = len(self.keys()) if num <= self._max_cols: return (1, num) nrows = num // self._max_cols last_row_cols = num % self._max_cols return nrows+(1 if last_row_cols else 0), min(num, self._max_cols)
[docs] def grid_items(self): """ Compute a dict of {(row,column): (key, value)} elements from the current set of items and specified number of columns. """ if list(self.keys()) == []: return {} cols = self._max_cols return {(idx // cols, idx % cols): (key, item) for idx, (key, item) in enumerate(self.data.items())}
[docs] def cols(self, ncols): """Sets the maximum number of columns in the NdLayout. Any items beyond the set number of cols will flow onto a new row. The number of columns control the indexing and display semantics of the NdLayout. Args: ncols (int): Number of columns to set on the NdLayout """ self._max_cols = ncols return self
@property def last(self): """ Returns another NdLayout constituted of the last views of the individual elements (if they are maps). """ last_items = [] for (k, v) in self.items(): if isinstance(v, NdMapping): item = (k, v.clone((v.last_key, v.last))) elif isinstance(v, AdjointLayout): item = (k, v.last) else: item = (k, v) last_items.append(item) return self.clone(last_items)
[docs] def clone(self, *args, **overrides): """Clones the NdLayout, overriding data and parameters. Args: data: New data replacing the existing data shared_data (bool, optional): Whether to use existing data new_type (optional): Type to cast object to *args: Additional arguments to pass to constructor **overrides: New keyword arguments to pass to constructor Returns: Cloned NdLayout object """ clone = super().clone(*args, **overrides) clone._max_cols = self._max_cols clone.id = self.id return clone
[docs]class Layout(Layoutable, ViewableTree): """ A Layout is an ViewableTree with ViewableElement objects as leaf values. Unlike ViewableTree, a Layout supports a rich display, displaying leaf items in a grid style layout. In addition to the usual ViewableTree indexing, Layout supports indexing of items by their row and column index in the layout. The maximum number of columns in such a layout may be controlled with the cols method. """ group = param.String(default='Layout', constant=True) _deep_indexable = True def __init__(self, items=None, identifier=None, parent=None, **kwargs): self.__dict__['_max_cols'] = 4 super().__init__(items, identifier, parent, **kwargs)
[docs] def decollate(self): """Packs Layout of DynamicMaps into a single DynamicMap that returns a Layout Decollation allows packing a Layout of DynamicMaps into a single DynamicMap that returns a Layout of simple (non-dynamic) elements. All nested streams are lifted to the resulting DynamicMap, and are available in the `streams` property. The `callback` property of the resulting DynamicMap is a pure, stateless function of the stream values. To avoid stream parameter name conflicts, the resulting DynamicMap is configured with positional_stream_args=True, and the callback function accepts stream values as positional dict arguments. Returns: DynamicMap that returns a Layout """ from .decollate import decollate return decollate(self)
@property def shape(self): "Tuple indicating the number of rows and columns in the Layout." num = len(self) if num <= self._max_cols: return (1, num) nrows = num // self._max_cols last_row_cols = num % self._max_cols return nrows+(1 if last_row_cols else 0), min(num, self._max_cols) def __getitem__(self, key): "Allows indexing Layout by row and column or path" if isinstance(key, int): if key < len(self): return list(self.data.values())[key] raise KeyError("Element out of range.") elif isinstance(key, slice): raise KeyError("A Layout may not be sliced, ensure that you " "are slicing on a leaf (i.e. not a branch) of the Layout.") if len(key) == 2 and not any([isinstance(k, str) for k in key]): if key == (slice(None), slice(None)): return self row, col = key idx = row * self._max_cols + col keys = list(self.data.keys()) if idx >= len(keys) or col >= self._max_cols: raise KeyError(f'Index {key} is outside available item range') key = keys[idx] return super().__getitem__(key)
[docs] def clone(self, *args, **overrides): """Clones the Layout, overriding data and parameters. Args: data: New data replacing the existing data shared_data (bool, optional): Whether to use existing data new_type (optional): Type to cast object to *args: Additional arguments to pass to constructor **overrides: New keyword arguments to pass to constructor Returns: Cloned Layout object """ clone = super().clone(*args, **overrides) clone._max_cols = self._max_cols return clone
[docs] def cols(self, ncols): """Sets the maximum number of columns in the NdLayout. Any items beyond the set number of cols will flow onto a new row. The number of columns control the indexing and display semantics of the NdLayout. Args: ncols (int): Number of columns to set on the NdLayout """ self._max_cols = ncols return self
[docs] def relabel(self, label=None, group=None, depth=1): """Clone object and apply new group and/or label. Applies relabeling to children up to the supplied depth. Args: label (str, optional): New label to apply to returned object group (str, optional): New group to apply to returned object depth (int, optional): Depth to which relabel will be applied If applied to container allows applying relabeling to contained objects up to the specified depth Returns: Returns relabelled object """ return super().relabel(label, group, depth)
def grid_items(self): return {tuple(np.unravel_index(idx, self.shape)): (path, item) for idx, (path, item) in enumerate(self.items())} def __mul__(self, other, reverse=False): from .spaces import HoloMap if not isinstance(other, (ViewableElement, HoloMap)): return NotImplemented layout = Layout([other*v if reverse else v*other for v in self]) layout._max_cols = self._max_cols return layout def __rmul__(self, other): return self.__mul__(other, reverse=True)
__all__ = list({_k for _k, _v in locals().items() if isinstance(_v, type) and (issubclass(_v, Dimensioned) or issubclass(_v, Layout))})