Source code for fastplotlib.graphics.line_collection

from typing import *

import numpy as np

import pygfx

from ..utils import parse_cmap_values
from ._collection_base import CollectionIndexer, GraphicCollection, CollectionFeature
from .line import LineGraphic
from .selectors import LinearRegionSelector, LinearSelector, RectangleSelector


class _LineCollectionProperties:
    """Mix-in class for LineCollection properties"""

    @property
    def colors(self) -> CollectionFeature:
        """get or set colors of lines in the collection"""
        return CollectionFeature(self.graphics, "colors")

    @colors.setter
    def colors(self, values: str | np.ndarray | tuple[float] | list[float] | list[str]):
        if isinstance(values, str):
            # set colors of all lines to one str color
            for g in self:
                g.colors = values
            return

        elif all(isinstance(v, str) for v in values):
            # individual str colors for each line
            if not len(values) == len(self):
                raise IndexError

            for g, v in zip(self.graphics, values):
                g.colors = v

            return

        if isinstance(values, np.ndarray):
            if values.ndim == 2:
                # assume individual colors for each
                for g, v in zip(self, values):
                    g.colors = v
                return

        elif len(values) == 4:
            # assume RGBA
            self.colors[:] = values

        else:
            # assume individual colors for each
            for g, v in zip(self, values):
                g.colors = v

    @property
    def data(self) -> CollectionFeature:
        """get or set data of lines in the collection"""
        return CollectionFeature(self.graphics, "data")

    @data.setter
    def data(self, values):
        for g, v in zip(self, values):
            g.data = v

    @property
    def cmap(self) -> CollectionFeature:
        """
        Get or set a cmap along the line collection.

        Optionally set using a tuple ("cmap", <transform>, <alpha>) to set the transform and/or alpha.
        Example:

        line_collection.cmap = ("jet", sine_transform_vals, 0.7)

        """
        return CollectionFeature(self.graphics, "cmap")

    @cmap.setter
    def cmap(self, args):
        if isinstance(args, str):
            name = args
            transform, alpha = None, 1.0
        elif len(args) == 1:
            name = args[0]
            transform, alpha = None, None

        elif len(args) == 2:
            name, transform = args
            alpha = None

        elif len(args) == 3:
            name, transform, alpha = args

        colors = parse_cmap_values(
            n_colors=len(self), cmap_name=name, transform=transform
        )
        colors[:, -1] = alpha
        self.colors = colors

    @property
    def thickness(self) -> np.ndarray:
        """get or set the thickness of the lines"""
        return np.asarray([g.thickness for g in self])

    @thickness.setter
    def thickness(self, values: np.ndarray | list[float]):
        if not len(values) == len(self):
            raise IndexError

        for g, v in zip(self, values):
            g.thickness = v


class LineCollectionIndexer(CollectionIndexer, _LineCollectionProperties):
    """Indexer for line collections"""

    pass


[docs] class LineCollection(GraphicCollection, _LineCollectionProperties): _child_type = LineGraphic _indexer = LineCollectionIndexer def __init__( self, data: np.ndarray | List[np.ndarray], thickness: float | Sequence[float] = 2.0, colors: str | Sequence[str] | np.ndarray | Sequence[np.ndarray] = "w", uniform_colors: bool = False, alpha: float = 1.0, cmap: Sequence[str] | str = None, cmap_transform: np.ndarray | List = None, name: str = None, names: list[str] = None, metadata: Any = None, metadatas: Sequence[Any] | np.ndarray = None, isolated_buffer: bool = True, kwargs_lines: list[dict] = None, **kwargs, ): """ Create a collection of :class:`.LineGraphic` Parameters ---------- data: list of array-like List or array-like of multiple line data to plot | if ``list`` each item in the list must be a 1D, 2D, or 3D numpy array | if array-like, must be of shape [n_lines, n_points_line, y | xy | xyz] thickness: float or Iterable of float, default 2.0 | if ``float``, single thickness will be used for all lines | if ``list`` of ``float``, each value will apply to the individual lines colors: str, RGBA array, Iterable of RGBA array, or Iterable of str, default "w" | if single ``str`` such as "w", "r", "b", etc, represents a single color for all lines | if single ``RGBA array`` (tuple or list of size 4), represents a single color for all lines | if ``list`` of ``str``, represents color for each individual line, example ["w", "b", "r",...] | if ``RGBA array`` of shape [data_size, 4], represents a single RGBA array for each line alpha: float, optional alpha value for colors, if colors is a ``str`` cmap: Iterable of str or str, optional | if ``str``, single cmap will be used for all lines | if ``list`` of ``str``, each cmap will apply to the individual lines .. note:: ``cmap`` overrides any arguments passed to ``colors`` cmap_transform: 1D array-like of numerical values, optional if provided, these values are used to map the colors from the cmap name: str, optional name of the line collection as a whole names: list[str], optional names of the individual lines in the collection, ``len(names)`` must equal ``len(data)`` metadata: Any meatadata associated with the collection as a whole metadatas: Iterable or array metadata for each individual line associated with this collection, this is for the user to manage. ``len(metadata)`` must be same as ``len(data)`` kwargs_lines: list[dict], optional list of kwargs passed to the individual lines, ``len(kwargs_lines)`` must equal ``len(data)`` kwargs_collection kwargs for the collection, passed to GraphicCollection """ super().__init__(name=name, metadata=metadata, **kwargs) if not isinstance(thickness, (float, int)): if len(thickness) != len(data): raise ValueError( f"len(thickness) != len(data)\n" f"{len(thickness)} != {len(data)}" ) if names is not None: if len(names) != len(data): raise ValueError( f"len(names) != len(data)\n" f"{len(names)} != {len(data)}" ) if metadatas is not None: if len(metadatas) != len(data): raise ValueError( f"len(metadata) != len(data)\n" f"{len(metadatas)} != {len(data)}" ) if kwargs_lines is not None: if len(kwargs_lines) != len(data): raise ValueError( f"len(kwargs_lines) != len(data)\n" f"{len(kwargs_lines)} != {len(data)}" ) self._cmap_transform = cmap_transform self._cmap_str = cmap # cmap takes priority over colors if cmap is not None: # cmap across lines if isinstance(cmap, str): colors = parse_cmap_values( n_colors=len(data), cmap_name=cmap, transform=cmap_transform ) single_color = False cmap = None elif isinstance(cmap, (tuple, list)): if len(cmap) != len(data): raise ValueError( "cmap argument must be a single cmap or a list of cmaps " "with the same length as the data" ) single_color = False else: raise ValueError( "cmap argument must be a single cmap or a list of cmaps " "with the same length as the data" ) else: if isinstance(colors, np.ndarray): # single color for all lines in the collection as RGBA if colors.shape == (4,): single_color = True # colors specified for each line as array of shape [n_lines, RGBA] elif colors.shape == (len(data), 4): single_color = False else: raise ValueError( f"numpy array colors argument must be of shape (4,) or (n_lines, 4)." f"You have pass the following shape: {colors.shape}" ) elif isinstance(colors, str): if colors == "random": colors = np.random.rand(len(data), 4) colors[:, -1] = alpha single_color = False else: # parse string color single_color = True colors = pygfx.Color(colors) elif isinstance(colors, (tuple, list)): if len(colors) == 4: # single color specified as (R, G, B, A) tuple or list if all([isinstance(c, (float, int)) for c in colors]): single_color = True elif len(colors) == len(data): # colors passed as list/tuple of colors, such as list of string single_color = False else: raise ValueError( "tuple or list colors argument must be a single color represented as [R, G, B, A], " "or must be a tuple/list of colors represented by a string with the same length as the data" ) if kwargs_lines is None: kwargs_lines = dict() self._set_world_object(pygfx.Group()) for i, d in enumerate(data): if isinstance(thickness, list): _s = thickness[i] else: _s = thickness if cmap is None: _cmap = None if single_color: _c = colors else: _c = colors[i] else: _cmap = cmap[i] _c = None if metadatas is not None: _m = metadatas[i] else: _m = None if names is not None: _name = names[i] else: _name = None lg = LineGraphic( data=d, thickness=_s, colors=_c, uniform_color=uniform_colors, cmap=_cmap, name=_name, metadata=_m, isolated_buffer=isolated_buffer, **kwargs_lines, ) self.add_graphic(lg) def __getitem__(self, item) -> LineCollectionIndexer: return super().__getitem__(item)
[docs] def add_linear_selector( self, selection: float = None, padding: float = 0.0, axis: str = "x", **kwargs ) -> LinearSelector: """ Adds a linear selector. Parameters ---------- Parameters ---------- selection: float, optional selected point on the linear selector, computed from data if not provided axis: str, default "x" axis that the selector resides on padding: float, default 0.0 Extra padding to extend the linear selector along the orthogonal axis to make it easier to interact with. kwargs passed to :class:`.LinearSelector` Returns ------- LinearSelector """ bounds_init, limits, size, center = self._get_linear_selector_init_args( axis, padding ) if selection is None: selection = bounds_init[0] selector = LinearSelector( selection=selection, limits=limits, size=size, center=center, axis=axis, parent=self, **kwargs, ) self._plot_area.add_graphic(selector, center=False) # place selector above this graphic selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) return selector
[docs] def add_linear_region_selector( self, selection: tuple[float, float] = None, padding: float = 0.0, axis: str = "x", **kwargs, ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a plot area just like any other ``Graphic``. Parameters ---------- selection: (float, float), optional the starting bounds of the linear region selector, computed from data if not provided axis: str, default "x" axis that the selector resides on padding: float, default 0.0 Extra padding to extend the linear region selector along the orthogonal axis to make it easier to interact with. kwargs passed to ``LinearRegionSelector`` Returns ------- LinearRegionSelector linear selection graphic """ bounds_init, limits, size, center = self._get_linear_selector_init_args( axis, padding ) if selection is None: selection = bounds_init # create selector selector = LinearRegionSelector( selection=selection, limits=limits, size=size, center=center, axis=axis, parent=self, **kwargs, ) self._plot_area.add_graphic(selector, center=False) # place selector below this graphic selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] - 1) # PlotArea manages this for garbage collection etc. just like all other Graphics # so we should only work with a proxy on the user-end return selector
[docs] def add_rectangle_selector( self, selection: tuple[float, float, float, float] = None, **kwargs, ) -> RectangleSelector: """ Add a :class:`.RectangleSelector`. Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a plot area just like any other ``Graphic``. Parameters ---------- selection: (float, float, float, float), optional initial (xmin, xmax, ymin, ymax) of the selection """ bbox = self.world_object.get_world_bounding_box() xdata = np.array(self.data[:, 0]) xmin, xmax = (np.nanmin(xdata), np.nanmax(xdata)) value_25px = (xmax - xmin) / 4 ydata = np.array(self.data[:, 1]) ymin = np.floor(ydata.min()).astype(int) ymax = np.ptp(bbox[:, 1]) if selection is None: selection = (xmin, value_25px, ymin, ymax) limits = (xmin, xmax, ymin - (ymax * 1.5 - ymax), ymax * 1.5) selector = RectangleSelector( selection=selection, limits=limits, parent=self, **kwargs, ) self._plot_area.add_graphic(selector, center=False) return selector
def _get_linear_selector_init_args(self, axis, padding): # use bbox to get size and center bbox = self.world_object.get_world_bounding_box() if axis == "x": xdata = np.array(self.data[:, 0]) xmin, xmax = (np.nanmin(xdata), np.nanmax(xdata)) value_25p = (xmax - xmin) / 4 bounds = (xmin, value_25p) limits = (xmin, xmax) # size from orthogonal axis size = np.ptp(bbox[:, 1]) * 1.5 # center on orthogonal axis center = bbox[:, 1].mean() elif axis == "y": ydata = np.array(self.data[:, 1]) xmin, xmax = (np.nanmin(ydata), np.nanmax(ydata)) value_25p = (xmax - xmin) / 4 bounds = (xmin, value_25p) limits = (xmin, xmax) size = np.ptp(bbox[:, 0]) * 1.5 # center on orthogonal axis center = bbox[:, 0].mean() return bounds, limits, size, center
axes = {"x": 0, "y": 1, "z": 2}
[docs] class LineStack(LineCollection): def __init__( self, data: List[np.ndarray], thickness: float | Iterable[float] = 2.0, colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", alpha: float = 1.0, cmap: Iterable[str] | str = None, cmap_transform: np.ndarray | List = None, name: str = None, names: list[str] = None, metadata: Any = None, metadatas: Sequence[Any] | np.ndarray = None, isolated_buffer: bool = True, separation: float = 10.0, separation_axis: str = "y", kwargs_lines: list[dict] = None, **kwargs, ): """ Create a stack of :class:`.LineGraphic` that are separated along the "x" or "y" axis. Parameters ---------- data: list of array-like List or array-like of multiple line data to plot | if ``list`` each item in the list must be a 1D, 2D, or 3D numpy array | if array-like, must be of shape [n_lines, n_points_line, y | xy | xyz] thickness: float or Iterable of float, default 2.0 | if ``float``, single thickness will be used for all lines | if ``list`` of ``float``, each value will apply to the individual lines colors: str, RGBA array, Iterable of RGBA array, or Iterable of str, default "w" | if single ``str`` such as "w", "r", "b", etc, represents a single color for all lines | if single ``RGBA array`` (tuple or list of size 4), represents a single color for all lines | if ``list`` of ``str``, represents color for each individual line, example ["w", "b", "r",...] | if ``RGBA array`` of shape [data_size, 4], represents a single RGBA array for each line alpha: float, optional alpha value for colors, if colors is a ``str`` cmap: Iterable of str or str, optional | if ``str``, single cmap will be used for all lines | if ``list`` of ``str``, each cmap will apply to the individual lines .. note:: ``cmap`` overrides any arguments passed to ``colors`` cmap_transform: 1D array-like of numerical values, optional if provided, these values are used to map the colors from the cmap name: str, optional name of the line collection as a whole names: list[str], optional names of the individual lines in the collection, ``len(names)`` must equal ``len(data)`` metadata: Any metadata associated with the collection as a whole metadatas: Iterable or array metadata for each individual line associated with this collection, this is for the user to manage. ``len(metadata)`` must be same as ``len(data)`` separation: float, default 10 space in between each line graphic in the stack separation_axis: str, default "y" axis in which the line graphics in the stack should be separated kwargs_lines: list[dict], optional list of kwargs passed to the individual lines, ``len(kwargs_lines)`` must equal ``len(data)`` kwargs_collection kwargs for the collection, passed to GraphicCollection """ super().__init__( data=data, thickness=thickness, colors=colors, alpha=alpha, cmap=cmap, cmap_transform=cmap_transform, name=name, names=names, metadata=metadata, metadatas=metadatas, isolated_buffer=isolated_buffer, kwargs_lines=kwargs_lines, **kwargs, ) axis_zero = 0 for i, line in enumerate(self.graphics): if separation_axis == "x": line.offset = (axis_zero, *line.offset[1:]) elif separation_axis == "y": line.offset = (line.offset[0], axis_zero, line.offset[2]) axis_zero = ( axis_zero + line.data.value[:, axes[separation_axis]].max() + separation ) self.separation = separation