Source code for fastplotlib.graphics.line

from typing import *
from warnings import warn

import numpy as np

import pygfx

from .selectors import (
    LinearRegionSelector,
    LinearSelector,
    RectangleSelector,
    PolygonSelector,
)
from .features import (
    Thickness,
    DashPattern,
    parse_dash_pattern,
)
from ..utils import quick_min_max, global_config
from ._positions_base import PositionsGraphic
from ..utils.types import ColorLike, MultiColorLike, ColormapLike


[docs] @global_config.register class LineGraphic(PositionsGraphic): _features = { "thickness": Thickness, "dash_pattern": DashPattern, } @global_config.declare( "thickness", "colors", "cmap", "size_space", "dash_pattern", "thin" ) def __init__( self, data: Any, thickness: float = 2.0, colors: ColorLike | MultiColorLike = "w", cmap: ColormapLike | None = None, cmap_transform: np.ndarray | Iterable[int | float] | None = None, cmap_range: tuple[float, float] | None = None, size_space: Literal["screen", "world", "model"] = "screen", dash_pattern: str | tuple | list = (), thin: bool = False, **kwargs, ): """ Create a line Graphic, 2d or 3d Parameters ---------- data: array-like Line data to plot. Can provide 1D, 2D, or a 3D data. | If passing a 1D array, it is used to set the y-values and the x-values are generated as an integer range from [0, data.size] | 2D data must be of shape [n_points, 2]. 3D data must be of shape [n_points, 3] thickness: float, optional, default 2.0 thickness of the line colors: ColorLike or MultiColorLike, default "w" specify colors as a single human-readable string, a single RGBA array, or a Sequence (array, tuple, or list) of strings or RGBA arrays cmap: ColormapLike, optional Apply a colormap to the line instead of assigning colors manually, this overrides any argument passed to "colors". For supported colormaps see the ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ cmap_transform: np.ndarray, optional 1D array-like of numerical values, if provided, these values are used to map the colors from the cmap cmap_range: (float, float), optional the (min, max) of the cmap_transform mapped onto the colormap, defaults to the transform's own range size_space: str, default "screen" coordinate space in which the thickness is expressed ("screen", "world", "model") dash_pattern: str, tuple, or list, default () The dash pattern. May be a matplotlib-style string, one of ``"-", "--", "-.", ":"`` or ``"solid", "dashed", "dashdot", "dotted"``, or a sequence of floats describing the length of strokes and gaps. Ignored when ``thin`` is True. thin: bool, default False Use the more performant thin line material, which is always one physical pixel wide. Thickness, dashing, and anti-aliasing are ignored when True. **kwargs passed to :class:`.Graphic` """ super().__init__( data=data, colors=colors, cmap=cmap, cmap_transform=cmap_transform, cmap_range=cmap_range, size_space=size_space, **kwargs, ) self._thickness = Thickness(thickness) self._dash_pattern = DashPattern(dash_pattern) self._thin = bool(thin) if self._thin and parse_dash_pattern(dash_pattern): warn( "`dash_pattern` is ignored when `thin=True`; the thin line material does not " "support dashing" ) world_object = pygfx.Line( geometry=self._make_geo(), material=self._make_material(), ) self._set_world_object(world_object) def _get_material_kwargs(self) -> dict: # pygfx line material kwargs assembled from the current feature state kwargs = super()._get_material_kwargs() kwargs["thickness"] = self.thickness kwargs["thickness_space"] = self.size_space kwargs["dash_pattern"] = parse_dash_pattern(self._dash_pattern.value) return kwargs def _make_material(self) -> pygfx.LineMaterial: # create the pygfx material, subclasses override to use a different line material material_cls = pygfx.LineThinMaterial if self._thin else pygfx.LineMaterial return material_cls(**self._get_material_kwargs()) @property def thickness(self) -> float: """Get or set the line thickness""" return self._thickness.value @thickness.setter def thickness(self, value: float): self._thickness.set_value(self, value) @property def dash_pattern(self) -> str | tuple | list: """ Get or set the dash pattern. May be a matplotlib-style string, one of ``"-", "--", "-.", ":"`` or ``"solid", "dashed", "dashdot", "dotted"``, or a sequence of floats describing the length of strokes and gaps. Ignored when ``thin`` is True. """ return self._dash_pattern.value @dash_pattern.setter def dash_pattern(self, value: str | tuple | list): if self._thin and parse_dash_pattern(value): warn( "`dash_pattern` is ignored when `thin=True`; the thin line material does not " "support dashing" ) self._dash_pattern.set_value(self, value) @property def thin(self) -> bool: """ Get or set whether the line uses the more performant thin line material, which is always one physical pixel wide. Thickness, dashing, and anti-aliasing are ignored when True. """ return self._thin @thin.setter def thin(self, value: bool): value = bool(value) if value == self._thin: return if value and parse_dash_pattern(self._dash_pattern.value): warn( "`dash_pattern` is ignored when `thin=True`; the thin line material does not " "support dashing" ) self._thin = value # thin vs. non-thin is a different pygfx material, so rebuild and swap it in place, # keeping the same geometry material = self._make_material() material.opacity = self.alpha material.alpha_mode = self.alpha_mode self.world_object.material = material
[docs] def create_legend_item( self, label: str = None, dash_pattern_labels: dict[str | tuple, str] = None, cmap_transform_labels: dict[int, str] = None, ): """ Create the :class:`.LineLegendItem` of this line, add it to a legend with ``Legend.add()``. The item follows the line: when its colors, colormap, thickness or dash pattern change the item changes with them. Per-vertex colors cannot be represented in a legend. Parameters ---------- label: str, optional label of the line in the legend, its ``name`` is used if not provided dash_pattern_labels: dict, optional {dash_pattern: label}, the label to use for the line's dash pattern cmap_transform_labels: dict, optional {cmap_transform value: label}, the label of each value of a qualitative colormap. A quantitative colormap is shown as a colorbar instead and needs no labels. Returns ------- LineLegendItem """ self._check_legend_item() from ..ui._legend import LineLegendItem self._legend_item = LineLegendItem( self, label=label, dash_pattern_labels=dash_pattern_labels, cmap_transform_labels=cmap_transform_labels, ) return self._legend_item
[docs] def add_linear_selector( self, selection: float = None, axis: str = "x", **kwargs ) -> LinearSelector: """ Adds a :class:`.LinearSelector`. 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, optional selected point on the linear selector, by default the first datapoint on the line. axis: str, default "x" axis that the selector resides on kwargs passed to :class:`.LinearSelector` Returns ------- LinearSelector """ bounds_init, limits, size, center = self._get_linear_selector_init_args( axis, padding=0 ) if selection is None: selection = bounds_init[0] selector = LinearSelector( selection=selection, limits=limits, axis=axis, parent=self, **kwargs, ) self._plot_area.add_graphic(selector, center=False) 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) # 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 """ # computes args to create selectors n_datapoints = self.data.value.shape[0] value_25p = int(n_datapoints / 4) # remove any nans data = self.data.value[~np.any(np.isnan(self.data.value), axis=1)] x_axis_vals = data[:, 0] y_axis_vals = data[:, 1] ymin = np.floor(y_axis_vals.min()).astype(int) ymax = np.ceil(y_axis_vals.max()).astype(int) # default selection is 25% of the image if selection is None: selection = (x_axis_vals[0], x_axis_vals[value_25p], ymin, ymax) # min/max limits limits = (x_axis_vals[0], x_axis_vals[-1], ymin * 1.5, ymax * 1.5) selector = RectangleSelector( selection=selection, limits=limits, parent=self, **kwargs, ) self._plot_area.add_graphic(selector, center=False) return selector
[docs] def add_polygon_selector( self, selection: List[tuple[float, float]] = None, **kwargs, ) -> PolygonSelector: """ Add a :class:`.PolygonSelector`. Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a plot area just like any other ``Graphic``. Parameters ---------- selection: list[tuple[float, float]], optional Initial points for the polygon. If not given or None, you'll start drawing the selection (clicking adds points to the polygon). """ # remove any nans data = self.data.value[~np.any(np.isnan(self.data.value), axis=1)] x_axis_vals = data[:, 0] y_axis_vals = data[:, 1] ymin = np.floor(y_axis_vals.min()).astype(int) ymax = np.ceil(y_axis_vals.max()).astype(int) # min/max limits limits = (x_axis_vals[0], x_axis_vals[-1], ymin * 1.5, ymax * 1.5) selector = PolygonSelector( selection, limits, parent=self, **kwargs, ) self._plot_area.add_graphic(selector, center=False) return selector
# TODO: this method is a bit of a mess, can refactor later def _get_linear_selector_init_args( self, axis: str, padding ) -> tuple[tuple[float, float], tuple[float, float], float, float]: # computes args to create selectors n_datapoints = self.data.value.shape[0] value_25p = int(n_datapoints / 4) # remove any nans data = self.data.value[~np.any(np.isnan(self.data.value), axis=1)] if axis == "x": # xvals axis_vals = data[:, 0] # yvals to get size and center magn_vals = data[:, 1] elif axis == "y": axis_vals = data[:, 1] magn_vals = data[:, 0] bounds_init = axis_vals[0], axis_vals[value_25p] limits = axis_vals[0], axis_vals[-1] # width or height of selector size = int(np.ptp(magn_vals) * 1.5 + padding) # center of selector along the other axis center = sum(quick_min_max(magn_vals)) / 2 return bounds_init, limits, size, center