Source code for fastplotlib.graphics.selectors._rectangle

import warnings
from numbers import Real
from typing import *
import numpy as np

import pygfx
from .._collection_base import GraphicCollection

from .._base import Graphic
from .._features import RectangleSelectionFeature
from ._base_selector import BaseSelector


[docs] class RectangleSelector(BaseSelector): @property def parent(self) -> Graphic | None: """Graphic that selector is associated with.""" return self._parent @property def selection(self) -> np.ndarray[float]: """ (xmin, xmax, ymin, ymax) of the rectangle selection """ return self._selection.value @selection.setter def selection(self, selection: Sequence[float]): # set (xmin, xmax, ymin, ymax) of the selector in data space graphic = self._parent if isinstance(graphic, GraphicCollection): pass self._selection.set_value(self, selection) @property def limits(self) -> Tuple[float, float, float, float]: """Return the limits of the selector.""" return self._limits @limits.setter def limits(self, values: Tuple[float, float, float, float]): if len(values) != 4 or not all(map(lambda v: isinstance(v, Real), values)): raise TypeError("limits must be an iterable of two numeric values") self._limits = tuple( map(round, values) ) # if values are close to zero things get weird so round them self._selection._limits = self._limits def __init__( self, selection: Sequence[float], limits: Sequence[float], parent: Graphic = None, resizable: bool = True, fill_color=(0, 0, 0.35), edge_color=(0.8, 0.6, 0), edge_thickness: float = 8, vertex_color=(0.7, 0.4, 0), vertex_thickness: float = 8, arrow_keys_modifier: str = "Shift", name: str = None, ): """ Create a RectangleSelector graphic which can be used to select a rectangular region of data. Allows sub-selecting data from a ``Graphic`` or from multiple Graphics. Parameters ---------- selection: (float, float, float, float) the initial selection of the rectangle, ``(x_min, x_max, y_min, y_max)`` limits: (float, float, float, float) limits of the selector, ``(x_min, x_max, y_min, y_max)`` parent: Graphic, default ``None`` associate this selector with a parent Graphic resizable: bool, default ``True`` if ``True``, the edges can be dragged to resize the selection fill_color: str, array, or tuple fill color for the selector, passed to pygfx.Color edge_color: str, array, or tuple edge color for the selector, passed to pygfx.Color edge_thickness: float, default 8 edge thickness arrow_keys_modifier: str modifier key that must be pressed to initiate movement using arrow keys, must be one of: "Control", "Shift", "Alt" or ``None`` name: str name for this selector graphic """ if not len(selection) == 4 or not len(limits) == 4: raise ValueError() # lots of very close to zero values etc. so round them selection = tuple(map(round, selection)) limits = tuple(map(round, limits)) self._parent = parent self._limits = np.asarray(limits) self._resizable = resizable selection = np.asarray(selection) # world object for this will be a group # basic mesh for the fill area of the selector # line for each edge of the selector group = pygfx.Group() xmin, xmax, ymin, ymax = selection self._fill_color = pygfx.Color(fill_color) self._edge_color = pygfx.Color(edge_color) self._vertex_color = pygfx.Color(vertex_color) width = xmax - xmin height = ymax - ymin if width < 0 or height < 0: raise ValueError() self.fill = pygfx.Mesh( pygfx.box_geometry(width, height, 1), pygfx.MeshBasicMaterial( color=pygfx.Color(self.fill_color), pick_write=True ), ) self.fill.world.position = (0, 0, -2) group.add(self.fill) # position data for the left edge line left_line_data = np.array( [ [xmin, ymin, 0], [xmin, ymax, 0], ] ).astype(np.float32) left_line = pygfx.Line( pygfx.Geometry(positions=left_line_data.copy()), pygfx.LineMaterial(thickness=edge_thickness, color=self.edge_color), ) # position data for the right edge line right_line_data = np.array( [ [xmax, ymin, 0], [xmax, ymax, 0], ] ).astype(np.float32) right_line = pygfx.Line( pygfx.Geometry(positions=right_line_data.copy()), pygfx.LineMaterial(thickness=edge_thickness, color=self.edge_color), ) # position data for the left edge line bottom_line_data = np.array( [ [xmin, ymax, 0], [xmax, ymax, 0], ] ).astype(np.float32) bottom_line = pygfx.Line( pygfx.Geometry(positions=bottom_line_data.copy()), pygfx.LineMaterial(thickness=edge_thickness, color=self.edge_color), ) # position data for the right edge line top_line_data = np.array( [ [xmin, ymin, 0], [xmax, ymin, 0], ] ).astype(np.float32) top_line = pygfx.Line( pygfx.Geometry(positions=top_line_data.copy()), pygfx.LineMaterial(thickness=edge_thickness, color=self.edge_color), ) self.edges: Tuple[pygfx.Line, pygfx.Line, pygfx.Line, pygfx.Line] = ( left_line, right_line, bottom_line, top_line, ) # left line, right line, bottom line, top line # add the edge lines for edge in self.edges: edge.world.z = -0.5 group.add(edge) # vertices top_left_vertex_data = (xmin, ymax, 1) top_right_vertex_data = (xmax, ymax, 1) bottom_left_vertex_data = (xmin, ymin, 1) bottom_right_vertex_data = (xmax, ymin, 1) top_left_vertex = pygfx.Points( pygfx.Geometry(positions=[top_left_vertex_data], sizes=[vertex_thickness]), pygfx.PointsMarkerMaterial( marker="square", size=vertex_thickness, color=self.vertex_color, size_mode="vertex", edge_color=self.vertex_color, ), ) top_right_vertex = pygfx.Points( pygfx.Geometry(positions=[top_right_vertex_data], sizes=[vertex_thickness]), pygfx.PointsMarkerMaterial( marker="square", size=vertex_thickness, color=self.vertex_color, size_mode="vertex", edge_color=self.vertex_color, ), ) bottom_left_vertex = pygfx.Points( pygfx.Geometry( positions=[bottom_left_vertex_data], sizes=[vertex_thickness] ), pygfx.PointsMarkerMaterial( marker="square", size=vertex_thickness, color=self.vertex_color, size_mode="vertex", edge_color=self.vertex_color, ), ) bottom_right_vertex = pygfx.Points( pygfx.Geometry( positions=[bottom_right_vertex_data], sizes=[vertex_thickness] ), pygfx.PointsMarkerMaterial( marker="square", size=vertex_thickness, color=self.vertex_color, size_mode="vertex", edge_color=self.vertex_color, ), ) self.vertices: Tuple[pygfx.Points, pygfx.Points, pygfx.Points, pygfx.Points] = ( bottom_left_vertex, bottom_right_vertex, top_left_vertex, top_right_vertex, ) for vertex in self.vertices: vertex.world.z = -0.25 group.add(vertex) self._selection = RectangleSelectionFeature(selection, limits=self._limits) # include parent offset if parent is not None: offset = (parent.offset[0], parent.offset[1], 0) else: offset = (0, 0, 0) BaseSelector.__init__( self, edges=self.edges, fill=(self.fill,), vertices=self.vertices, hover_responsive=(*self.edges, *self.vertices), arrow_keys_modifier=arrow_keys_modifier, parent=parent, name=name, offset=offset, ) self._set_world_object(group) self.selection = selection
[docs] def get_selected_data( self, graphic: Graphic = None, mode: str = "full" ) -> Union[np.ndarray, List[np.ndarray]]: """ Get the ``Graphic`` data bounded by the current selection. Returns a view of the data array. If the ``Graphic`` is a collection, such as a ``LineStack``, it returns a list of views of the full array. Can be performed on the ``parent`` Graphic or on another graphic by passing to the ``graphic`` arg. Parameters ---------- graphic: Graphic, optional, default ``None`` if provided, returns the data selection from this graphic instead of the graphic set as ``parent`` mode: str, default 'full' One of 'full', 'partial', or 'ignore'. Indicates how selected data should be returned based on the selectors position over the graphic. Only used for ``LineGraphic``, ``LineCollection``, and ``LineStack`` | If 'full', will return all data bounded by the x and y limits of the selector even if partial indices along one axis are not fully covered by the selector. | If 'partial' will return only the data that is bounded by the selector, missing indices not bounded by the selector will be set to NaNs | If 'ignore', will only return data for graphics that have indices completely bounded by the selector Returns ------- np.ndarray or List[np.ndarray] view or list of views of the full array, returns empty array if selection is empty """ source = self._get_source(graphic) ixs = self.get_selected_indices(source) # do not need to check for mode for images, because the selector is bounded by the image shape # will always be `full` if "Image" in source.__class__.__name__: row_ixs, col_ixs = ixs row_slice = slice(row_ixs[0], row_ixs[-1] + 1) col_slice = slice(col_ixs[0], col_ixs[-1] + 1) return source.data[row_slice, col_slice] if mode not in ["full", "partial", "ignore"]: raise ValueError( f"`mode` must be one of 'full', 'partial', or 'ignore', you have passed {mode}" ) if "Line" in source.__class__.__name__: if isinstance(source, GraphicCollection): data_selections: List[np.ndarray] = list() for i, g in enumerate(source.graphics): # want to keep same length as the original line collection if ixs[i].size == 0: data_selections.append( np.array([], dtype=np.float32).reshape(0, 3) ) else: # s gives entire slice of data along the x s = slice( ixs[i][0], ixs[i][-1] + 1 ) # add 1 because these are direct indices # slices n_datapoints dim # calculate missing ixs using set difference # then calculate shift missing_ixs = ( np.setdiff1d(np.arange(ixs[i][0], ixs[i][-1] + 1), ixs[i]) - ixs[i][0] ) match mode: # take all ixs, ignore missing case "full": data_selections.append(g.data[s]) # set missing ixs data to NaNs case "partial": if len(missing_ixs) > 0: data = g.data[s].copy() data[missing_ixs] = np.nan data_selections.append(data) else: data_selections.append(g.data[s]) # ignore lines that do not have full ixs to start case "ignore": if len(missing_ixs) > 0: data_selections.append( np.array([], dtype=np.float32).reshape(0, 3) ) else: data_selections.append(g.data[s]) return data_selections else: # for lines if ixs.size == 0: # empty selection return np.array([], dtype=np.float32).reshape(0, 3) s = slice( ixs[0], ixs[-1] + 1 ) # add 1 to end because these are direct indices # slices n_datapoints dim # slice with min, max is faster than using all the indices # get missing ixs missing_ixs = np.setdiff1d(np.arange(ixs[0], ixs[-1] + 1), ixs) - ixs[0] match mode: # return all, do not care about missing case "full": return source.data[s] # set missing to NaNs case "partial": if len(missing_ixs) > 0: data = source.data[s].copy() data[missing_ixs] = np.nan return data else: return source.data[s] # missing means nothing will be returned even if selector is partially over data # warn the user and return empty case "ignore": if len(missing_ixs) > 0: warnings.warn( "You have selected 'ignore' mode. Selected graphic has incomplete indices. " "Move the selector or change the mode to one of `partial` or `full`." ) return np.array([], dtype=np.float32) else: return source.data[s]
[docs] def get_selected_indices( self, graphic: Graphic = None ) -> np.ndarray | tuple[np.ndarray]: """ Returns the indices of the ``Graphic`` data bounded by the current selection. These are the data indices which correspond to the data under the selector. Parameters ---------- graphic: Graphic, default ``None`` If provided, returns the selection indices from this graphic instrad of the graphic set as ``parent`` Returns ------- Union[np.ndarray, List[np.ndarray]] data indicies of the selection | tuple of [row_indices, col_indices] if the graphic is an image | list of indices along the x-dimension for each line if graphic is a line collection | array of indices along the x-dimension if graphic is a line """ # get indices from source source = self._get_source(graphic) # selector (xmin, xmax, ymin, ymax) values xmin, xmax, ymin, ymax = self.selection # image data does not need to check for mode because the selector is always bounded # to the image if "Image" in source.__class__.__name__: col_ixs = np.arange(xmin, xmax, dtype=int) row_ixs = np.arange(ymin, ymax, dtype=int) return row_ixs, col_ixs if "Line" in source.__class__.__name__: if isinstance(source, GraphicCollection): ixs = list() for g in source.graphics: data = g.data.value g_ixs = np.where( (data[:, 0] >= xmin - g.offset[0]) & (data[:, 0] <= xmax - g.offset[0]) & (data[:, 1] >= ymin - g.offset[1]) & (data[:, 1] <= ymax - g.offset[1]) )[0] ixs.append(g_ixs) else: # map only this graphic data = source.data.value ixs = np.where( (data[:, 0] >= xmin) & (data[:, 0] <= xmax) & (data[:, 1] >= ymin) & (data[:, 1] <= ymax) )[0] return ixs
def _move_graphic(self, delta: np.ndarray): # new selection positions xmin_new = self.selection[0] + delta[0] xmax_new = self.selection[1] + delta[0] ymin_new = self.selection[2] + delta[1] ymax_new = self.selection[3] + delta[1] # move entire selector if source is fill if self._move_info.source == self.fill: if self.selection[0] == self.limits[0] and xmin_new < self.limits[0]: return if self.selection[1] == self.limits[1] and xmax_new > self.limits[1]: return if self.selection[2] == self.limits[2] and ymin_new < self.limits[2]: return if self.selection[3] == self.limits[3] and ymax_new > self.limits[3]: return # set thew new bounds self._selection.set_value(self, (xmin_new, xmax_new, ymin_new, ymax_new)) return # if selector not resizable return if not self._resizable: return xmin, xmax, ymin, ymax = self.selection if self._move_info.source == self.vertices[0]: # bottom left self._selection.set_value(self, (xmin_new, xmax, ymin_new, ymax)) if self._move_info.source == self.vertices[1]: # bottom right self._selection.set_value(self, (xmin, xmax_new, ymin_new, ymax)) if self._move_info.source == self.vertices[2]: # top left self._selection.set_value(self, (xmin_new, xmax, ymin, ymax_new)) if self._move_info.source == self.vertices[3]: # top right self._selection.set_value(self, (xmin, xmax_new, ymin, ymax_new)) # if event source was an edge and selector is resizable, move the edge that caused the event if self._move_info.source == self.edges[0]: self._selection.set_value(self, (xmin_new, xmax, ymin, ymax)) if self._move_info.source == self.edges[1]: self._selection.set_value(self, (xmin, xmax_new, ymin, ymax)) if self._move_info.source == self.edges[2]: self._selection.set_value(self, (xmin, xmax, ymin_new, ymax)) if self._move_info.source == self.edges[3]: self._selection.set_value(self, (xmin, xmax, ymin, ymax_new)) def _move_to_pointer(self, ev): pass