from typing import *
import numpy as np
import pygfx
from ._positions_base import PositionsGraphic
from .selectors import LinearRegionSelector, LinearSelector, RectangleSelector
from ._features import Thickness
[docs]
class LineGraphic(PositionsGraphic):
_features = {"data", "colors", "cmap", "thickness"}
def __init__(
self,
data: Any,
thickness: float = 2.0,
colors: str | np.ndarray | Iterable = "w",
uniform_color: bool = False,
alpha: float = 1.0,
cmap: str = None,
cmap_transform: np.ndarray | Iterable = None,
isolated_buffer: bool = True,
**kwargs,
):
"""
Create a line Graphic, 2d or 3d
Parameters
----------
data: array-like
Line data to plot, 2D must be of shape [n_points, 2], 3D must be of shape [n_points, 3]
thickness: float, optional, default 2.0
thickness of the line
colors: str, array, or iterable, default "w"
specify colors as a single human-readable string, a single RGBA array,
or an iterable of strings or RGBA arrays
uniform_color: bool, default ``False``
if True, uses a uniform buffer for the line color,
basically saves GPU VRAM when the entire line has a single color
alpha: float, optional, default 1.0
alpha value for the colors
cmap: str, optional
apply a colormap to the line instead of assigning colors manually, this
overrides any argument 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
**kwargs
passed to Graphic
"""
super().__init__(
data=data,
colors=colors,
uniform_color=uniform_color,
alpha=alpha,
cmap=cmap,
cmap_transform=cmap_transform,
isolated_buffer=isolated_buffer,
**kwargs,
)
self._thickness = Thickness(thickness)
if thickness < 1.1:
MaterialCls = pygfx.LineThinMaterial
else:
MaterialCls = pygfx.LineMaterial
if uniform_color:
geometry = pygfx.Geometry(positions=self._data.buffer)
material = MaterialCls(
thickness=self.thickness,
color_mode="uniform",
color=self.colors,
pick_write=True,
)
else:
material = MaterialCls(
thickness=self.thickness, color_mode="vertex", pick_write=True
)
geometry = pygfx.Geometry(
positions=self._data.buffer, colors=self._colors.buffer
)
world_object: pygfx.Line = pygfx.Line(geometry=geometry, material=material)
self._set_world_object(world_object)
@property
def thickness(self) -> float:
"""line thickness"""
return self._thickness.value
@thickness.setter
def thickness(self, value: float):
self._thickness.set_value(self, value)
[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
"""
# 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
# 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 = np.nanmean(magn_vals)
return bounds_init, limits, size, center