diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index fdf120268..4442c851e 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,4 +1,4 @@ -from typing import * +from typing import Any, Literal import weakref from warnings import warn from abc import ABC, abstractmethod @@ -13,7 +13,7 @@ # dict that holds all world objects for a given python kernel/session # Graphic objects only use proxies to WorldObjects -WORLD_OBJECTS: Dict[str, WorldObject] = dict() #: {hex id str: WorldObject} +WORLD_OBJECTS: dict[str, WorldObject] = dict() #: {hex id str: WorldObject} PYGFX_EVENTS = [ @@ -87,7 +87,7 @@ def __init__( self._plot_area = None @property - def name(self) -> Union[str, None]: + def name(self) -> str | None: """str name reference for this item""" return self._name @@ -162,7 +162,7 @@ def visible(self, v: bool): self.world_object.visible = v @property - def children(self) -> List[WorldObject]: + def children(self) -> list[WorldObject]: """Return the children of the WorldObject.""" return self.world_object.children @@ -432,7 +432,7 @@ class PreviouslyModifiedData: indices: Any -COLLECTION_GRAPHICS: Dict[str, Graphic] = dict() +COLLECTION_GRAPHICS: dict[str, Graphic] = dict() class GraphicCollection(Graphic): @@ -440,7 +440,7 @@ class GraphicCollection(Graphic): def __init__(self, name: str = None): super().__init__(name) - self._graphics: List[str] = list() + self._graphics: list[str] = list() self._graphics_changed: bool = True self._graphics_array: np.ndarray[Graphic] = None @@ -548,7 +548,7 @@ class CollectionIndexer: def __init__( self, parent: GraphicCollection, - selection: List[Graphic], + selection: list[Graphic], ): """ @@ -605,7 +605,7 @@ def __repr__(self): class CollectionFeature: """Collection Feature""" - def __init__(self, selection: List[Graphic], feature: str): + def __init__(self, selection: list[Graphic], feature: str): """ selection: list of Graphics a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` @@ -618,7 +618,7 @@ def __init__(self, selection: List[Graphic], feature: str): self._selection = selection self._feature = feature - self._feature_instances: List[GraphicFeature] = list() + self._feature_instances: list[GraphicFeature] = list() if len(self._selection) > 0: for graphic in self._selection: diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index d76c8e704..f44347a58 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -18,10 +18,10 @@ def __init__( self, data: Any, thickness: float = 2.0, - colors: Union[str, np.ndarray, Iterable] = "w", + colors: str | np.ndarray | Iterable = "w", alpha: float = 1.0, cmap: str = None, - cmap_values: Union[np.ndarray, List] = None, + cmap_values: np.ndarray | Iterable = None, z_position: float = None, collection_index: int = None, *args, @@ -46,7 +46,7 @@ def __init__( apply a colormap to the line instead of assigning colors manually, this overrides any argument passed to "colors" - cmap_values: 1D array-like or list of numerical values, optional + cmap_values: 1D array-like or Iterable of numerical values, optional if provided, these values are used to map the colors from the cmap alpha: float, optional, default 1.0 diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index bb7bb2444..8488ec15e 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -20,14 +20,14 @@ class LineCollection(GraphicCollection, Interaction): def __init__( self, data: List[np.ndarray], - z_position: Union[List[float], float] = None, - thickness: Union[float, List[float]] = 2.0, - colors: Union[List[np.ndarray], np.ndarray] = "w", + z_position: Iterable[float] | float = None, + thickness: float | Iterable[float] = 2.0, + colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", alpha: float = 1.0, - cmap: Union[List[str], str] = None, - cmap_values: Union[np.ndarray, List] = None, + cmap: Iterable[str] | str = None, + cmap_values: np.ndarray | List = None, name: str = None, - metadata: Union[list, tuple, np.ndarray] = None, + metadata: Iterable[Any] | np.ndarray = None, *args, **kwargs, ): @@ -36,39 +36,41 @@ def __init__( Parameters ---------- - data: list of array-like or array List of line data to plot, each element must be a 1D, 2D, or 3D numpy array if elements are 2D, interpreted as [y_vals, n_lines] - z_position: list of float or float, optional + z_position: Iterable of float or float, optional | if ``float``, single position will be used for all lines | if ``list`` of ``float``, each value will apply to the individual lines - thickness: float or list of float, default 2.0 + 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, list of RGBA array, or list of str, default "w" + 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 - cmap: list of str or str, optional + 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_values: 1D array-like or list of numerical values, optional + cmap_values: 1D array-like or Iterable 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 - metadata: list, tuple, or array + metadata: Iterable or array metadata associated with this collection, this is for the user to manage. ``len(metadata)`` must be same as ``len(data)`` @@ -235,7 +237,7 @@ def cmap_values(self) -> np.ndarray: return self._cmap_values @cmap_values.setter - def cmap_values(self, values: Union[np.ndarray, list]): + def cmap_values(self, values: np.ndarray | Iterable): colors = parse_cmap_values( n_colors=len(self), cmap_name=self.cmap, cmap_values=values ) @@ -477,13 +479,16 @@ class LineStack(LineCollection): def __init__( self, data: List[np.ndarray], - z_position: Union[List[float], float] = None, - thickness: Union[float, List[float]] = 2.0, - colors: Union[List[np.ndarray], np.ndarray] = "w", - cmap: Union[List[str], str] = None, - separation: float = 10, - separation_axis: str = "y", + z_position: Iterable[float] | float = None, + 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_values: np.ndarray | List = None, name: str = None, + metadata: Iterable[Any] | np.ndarray = None, + separation: float = 10.0, + separation_axis: str = "y", *args, **kwargs, ): @@ -492,33 +497,37 @@ def __init__( Parameters ---------- - data: list of array-like + data: list of array-like or array List of line data to plot, each element must be a 1D, 2D, or 3D numpy array if elements are 2D, interpreted as [y_vals, n_lines] - z_position: list of float or float, optional + z_position: Iterable of float or float, optional | if ``float``, single position will be used for all lines - | if ``list`` of ``float``, each value will apply to individual lines + | if ``list`` of ``float``, each value will apply to the individual lines - thickness: float or list of float, default 2.0 + 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, list of RGBA array, or list of str, default "w" + 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 - | is ``list`` of ``str``, represents color for each individual line, example ["w", "b", "r",...] - | if ``list`` of ``RGBA array`` of shape [data_size, 4], represents a single RGBA array for each line + | 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 - cmap: list of str or str, optional + 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`` - name: str, optional - name of the line stack + cmap_values: 1D array-like or Iterable of numerical values, optional + if provided, these values are used to map the colors from the cmap + + metadata: Iterable or array + metadata 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 @@ -529,13 +538,9 @@ def __init__( name: str, optional name of the line stack - args - passed to LineCollection - kwargs passed to LineCollection - Features -------- @@ -549,8 +554,12 @@ def __init__( z_position=z_position, thickness=thickness, colors=colors, + alpha=alpha, cmap=cmap, + cmap_values=cmap_values, + metadata=metadata, name=name, + *args, **kwargs, ) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 3f04f644e..2557cd637 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -14,11 +14,11 @@ class ScatterGraphic(Graphic): def __init__( self, data: np.ndarray, - sizes: Union[int, float, np.ndarray, list] = 1, - colors: np.ndarray = "w", + sizes: float | np.ndarray | Iterable[float] = 1, + colors: str | np.ndarray | Iterable[str] = "w", alpha: float = 1.0, cmap: str = None, - cmap_values: Union[np.ndarray, List] = None, + cmap_values: np.ndarray | List = None, z_position: float = 0.0, *args, **kwargs, diff --git a/fastplotlib/layouts/_defaults.py b/fastplotlib/layouts/_defaults.py deleted file mode 100644 index 8b1378917..000000000 --- a/fastplotlib/layouts/_defaults.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fastplotlib/layouts/_frame/_ipywidget_toolbar.py b/fastplotlib/layouts/_frame/_ipywidget_toolbar.py index 72976a445..5b42c8eab 100644 --- a/fastplotlib/layouts/_frame/_ipywidget_toolbar.py +++ b/fastplotlib/layouts/_frame/_ipywidget_toolbar.py @@ -4,8 +4,6 @@ from math import copysign from functools import partial from pathlib import Path -from typing import * - from ipywidgets.widgets import ( IntSlider, @@ -238,7 +236,7 @@ def __init__(self, iw): tooltip="reset vmin/vmax and reset histogram using current frame", ) - self.sliders: Dict[str, IntSlider] = dict() + self.sliders: dict[str, IntSlider] = dict() # only for xy data, no time point slider needed if self.iw.ndim == 2: diff --git a/fastplotlib/layouts/_frame/_jupyter_output.py b/fastplotlib/layouts/_frame/_jupyter_output.py index 786041bcf..9ebf0941d 100644 --- a/fastplotlib/layouts/_frame/_jupyter_output.py +++ b/fastplotlib/layouts/_frame/_jupyter_output.py @@ -1,5 +1,3 @@ -from typing import * - from ipywidgets import VBox, Widget from sidecar import Sidecar from IPython.display import display @@ -20,7 +18,7 @@ def __init__( make_toolbar: bool, use_sidecar: bool, sidecar_kwargs: dict, - add_widgets: List[Widget], + add_widgets: list[Widget], ): """ diff --git a/fastplotlib/layouts/graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py similarity index 87% rename from fastplotlib/layouts/graphic_methods_mixin.py rename to fastplotlib/layouts/_graphic_methods_mixin.py index 0376fd777..a7acb5eec 100644 --- a/fastplotlib/layouts/graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -178,14 +178,14 @@ def add_image( def add_line_collection( self, data: List[numpy.ndarray], - z_position: Union[List[float], float] = None, - thickness: Union[float, List[float]] = 2.0, - colors: Union[List[numpy.ndarray], numpy.ndarray] = "w", + z_position: Union[Iterable[float], float] = None, + thickness: Union[float, Iterable[float]] = 2.0, + colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", alpha: float = 1.0, - cmap: Union[List[str], str] = None, + cmap: Union[Iterable[str], str] = None, cmap_values: Union[numpy.ndarray, List] = None, name: str = None, - metadata: Union[list, tuple, numpy.ndarray] = None, + metadata: Union[Iterable[Any], numpy.ndarray] = None, *args, **kwargs ) -> LineCollection: @@ -195,39 +195,41 @@ def add_line_collection( Parameters ---------- - data: list of array-like or array List of line data to plot, each element must be a 1D, 2D, or 3D numpy array if elements are 2D, interpreted as [y_vals, n_lines] - z_position: list of float or float, optional + z_position: Iterable of float or float, optional | if ``float``, single position will be used for all lines | if ``list`` of ``float``, each value will apply to the individual lines - thickness: float or list of float, default 2.0 + 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, list of RGBA array, or list of str, default "w" + 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 - cmap: list of str or str, optional + 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_values: 1D array-like or list of numerical values, optional + cmap_values: 1D array-like or Iterable 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 - metadata: list, tuple, or array + metadata: Iterable or array metadata associated with this collection, this is for the user to manage. ``len(metadata)`` must be same as ``len(data)`` @@ -268,7 +270,7 @@ def add_line( colors: Union[str, numpy.ndarray, Iterable] = "w", alpha: float = 1.0, cmap: str = None, - cmap_values: Union[numpy.ndarray, List] = None, + cmap_values: Union[numpy.ndarray, Iterable] = None, z_position: float = None, collection_index: int = None, *args, @@ -294,7 +296,7 @@ def add_line( apply a colormap to the line instead of assigning colors manually, this overrides any argument passed to "colors" - cmap_values: 1D array-like or list of numerical values, optional + cmap_values: 1D array-like or Iterable of numerical values, optional if provided, these values are used to map the colors from the cmap alpha: float, optional, default 1.0 @@ -346,13 +348,16 @@ def add_line( def add_line_stack( self, data: List[numpy.ndarray], - z_position: Union[List[float], float] = None, - thickness: Union[float, List[float]] = 2.0, - colors: Union[List[numpy.ndarray], numpy.ndarray] = "w", - cmap: Union[List[str], str] = None, - separation: float = 10, - separation_axis: str = "y", + z_position: Union[Iterable[float], float] = None, + thickness: Union[float, Iterable[float]] = 2.0, + colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", + alpha: float = 1.0, + cmap: Union[Iterable[str], str] = None, + cmap_values: Union[numpy.ndarray, List] = None, name: str = None, + metadata: Union[Iterable[Any], numpy.ndarray] = None, + separation: float = 10.0, + separation_axis: str = "y", *args, **kwargs ) -> LineStack: @@ -362,33 +367,37 @@ def add_line_stack( Parameters ---------- - data: list of array-like + data: list of array-like or array List of line data to plot, each element must be a 1D, 2D, or 3D numpy array if elements are 2D, interpreted as [y_vals, n_lines] - z_position: list of float or float, optional + z_position: Iterable of float or float, optional | if ``float``, single position will be used for all lines - | if ``list`` of ``float``, each value will apply to individual lines + | if ``list`` of ``float``, each value will apply to the individual lines - thickness: float or list of float, default 2.0 + 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, list of RGBA array, or list of str, default "w" + 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 - | is ``list`` of ``str``, represents color for each individual line, example ["w", "b", "r",...] - | if ``list`` of ``RGBA array`` of shape [data_size, 4], represents a single RGBA array for each line + | 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 - cmap: list of str or str, optional + 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`` - name: str, optional - name of the line stack + cmap_values: 1D array-like or Iterable of numerical values, optional + if provided, these values are used to map the colors from the cmap + + metadata: Iterable or array + metadata 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 @@ -399,13 +408,9 @@ def add_line_stack( name: str, optional name of the line stack - args - passed to LineCollection - kwargs passed to LineCollection - Features -------- @@ -421,10 +426,13 @@ def add_line_stack( z_position, thickness, colors, + alpha, cmap, + cmap_values, + name, + metadata, separation, separation_axis, - name, *args, **kwargs ) @@ -432,8 +440,8 @@ def add_line_stack( def add_scatter( self, data: numpy.ndarray, - sizes: Union[int, float, numpy.ndarray, list] = 1, - colors: numpy.ndarray = "w", + sizes: Union[float, numpy.ndarray, Iterable[float]] = 1, + colors: Union[str, numpy.ndarray, Iterable[str]] = "w", alpha: float = 1.0, cmap: str = None, cmap_values: Union[numpy.ndarray, List] = None, diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index fa987b661..5f7f3086d 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -1,6 +1,6 @@ from itertools import product, chain import numpy as np -from typing import * +from typing import Literal from inspect import getfullargspec from warnings import warn @@ -18,14 +18,23 @@ class GridPlot(Frame, RecordMixin): def __init__( self, - shape: Tuple[int, int], - cameras: Union[str, list, np.ndarray] = "2d", - controller_types: Union[str, list, np.ndarray] = None, - controller_ids: Union[str, list, np.ndarray] = None, - canvas: Union[str, WgpuCanvasBase, pygfx.Texture] = None, + shape: tuple[int, int], + cameras: ( + Literal["2d", "3d"] + | list[Literal["2d", "3d"]] + | list[pygfx.PerspectiveCamera] + | np.ndarray + ) = "2d", + controller_types: ( + Literal["panzoom", "fly", "trackball", "orbit"] + | list[Literal["panzoom", "fly", "trackball", "orbit"]] + | np.ndarray + ) = None, + controller_ids: str | list[int] | np.ndarray | list[list[str]] = None, + canvas: str | WgpuCanvasBase | pygfx.Texture = None, renderer: pygfx.WgpuRenderer = None, - size: Tuple[int, int] = (500, 300), - names: Union[list, np.ndarray] = None, + size: tuple[int, int] = (500, 300), + names: list | np.ndarray = None, ): """ A grid of subplots. @@ -35,27 +44,31 @@ def __init__( shape: (int, int) (n_rows, n_cols) - cameras: str, list, or np.ndarray, optional - | One of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots + cameras: "2d", "3", list of "2d" | "3d", list of camera instances, or np.ndarray of "2d" | "3d", optional + | if str, one of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots | list/array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot | list/array of pygfx.PerspectiveCamera instances controller_types: str, list or np.ndarray, optional list or array that specifies the controller type for each subplot, or list/array of - pygfx.Controller instances + pygfx.Controller instances. Valid controller types: "panzoom", "fly", "trackball", "orbit". + If not specified a default controller is chosen based on the camera type. + Orthographic projections, i.e. "2d" cameras, use a "panzoom" controller by default. + Perspective projections with a FOV > 0, i.e. "3d" cameras, use a "fly" controller by default. - controller_ids: str, list or np.ndarray of int or str ids, optional + + controller_ids: str, list of int, np.ndarray of int, or list with sublists of subplot str names, optional | If `None` a unique controller is created for each subplot | If "sync" all the subplots use the same controller - | If ``numpy.array``, its shape must be the same as ``grid_shape``. + | If array/list it must be reshapeable to ``grid_shape``. This allows custom assignment of controllers | Example with integers: | sync first 2 plots, and sync last 2 plots: [[0, 0, 1], [2, 3, 3]] | Example with str subplot names: - | list of lists of subplot names, each sublist is synced: [[subplot_a, subplot_b], [subplot_f, subplot_c]] - | this syncs subplot_a and subplot_b together; syncs subplot_f and subplot_c together + | list of lists of subplot names, each sublist is synced: [[subplot_a, subplot_b, subplot_e], [subplot_c, subplot_d]] + | this syncs subplot_a, subplot_b and subplot_e together; syncs subplot_c and subplot_d together canvas: WgpuCanvas, optional Canvas for drawing @@ -249,8 +262,8 @@ def __init__( name=name, ) - self._animate_funcs_pre: List[callable] = list() - self._animate_funcs_post: List[callable] = list() + self._animate_funcs_pre: list[callable] = list() + self._animate_funcs_post: list[callable] = list() self._current_iter = None @@ -269,12 +282,12 @@ def renderer(self) -> pygfx.WgpuRenderer: """The renderer associated to this GridPlot""" return self._renderer - def __getitem__(self, index: Union[Tuple[int, int], str]) -> Subplot: + def __getitem__(self, index: tuple[int, int] | str) -> Subplot: if isinstance(index, str): for subplot in self._subplots.ravel(): if subplot.name == index: return subplot - raise IndexError("no subplot with given name") + raise IndexError(f"no subplot with given name: {index}") else: return self._subplots[index[0], index[1]] @@ -291,7 +304,7 @@ def render(self): # call post-render animate functions self._call_animate_functions(self._animate_funcs_post) - def _call_animate_functions(self, funcs: Iterable[callable]): + def _call_animate_functions(self, funcs: list[callable]): for fn in funcs: try: if len(getfullargspec(fn).args) > 0: @@ -307,7 +320,7 @@ def _call_animate_functions(self, funcs: Iterable[callable]): def add_animations( self, - *funcs: Iterable[callable], + *funcs: callable, pre_render: bool = True, post_render: bool = False, ): @@ -317,7 +330,7 @@ def add_animations( Parameters ---------- - *funcs: callable or iterable of callable + *funcs: callable(s) function(s) that are called on each render cycle pre_render: bool, default ``True``, optional keyword-only argument diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 2c93d7e9e..299bc6e5d 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -1,5 +1,5 @@ from inspect import getfullargspec -from typing import * +from typing import TypeAlias, Literal, Union import weakref from warnings import warn @@ -9,7 +9,7 @@ from pylinalg import vec_transform, vec_unproject from wgpu.gui import WgpuCanvasBase -from ._utils import create_camera, create_controller +from ._utils import create_controller from ..graphics._base import Graphic from ..graphics.selectors._base_selector import BaseSelector from ..legends import Legend @@ -17,17 +17,18 @@ # dict to store Graphic instances # this is the only place where the real references to Graphics are stored in a Python session # {hex id str: Graphic} -GRAPHICS: Dict[str, Graphic] = dict() -SELECTORS: Dict[str, BaseSelector] = dict() +HexStr: TypeAlias = str +GRAPHICS: dict[HexStr, Graphic] = dict() +SELECTORS: dict[HexStr, BaseSelector] = dict() class PlotArea: def __init__( self, - parent, - position: Any, - camera: Union[pygfx.PerspectiveCamera], - controller: Union[pygfx.Controller], + parent: Union["PlotArea", "GridPlot"], + position: tuple[int, int] | str, + camera: pygfx.PerspectiveCamera, + controller: pygfx.Controller, scene: pygfx.Scene, canvas: WgpuCanvasBase, renderer: pygfx.WgpuRenderer, @@ -39,18 +40,18 @@ def __init__( Parameters ---------- - parent: PlotArea - parent class of subclasses will be a ``PlotArea`` instance + parent: PlotArea or GridPlot + parent object position: Any - typical use will be for ``subplots`` in a ``gridplot``, position would correspond to the ``[row, column]`` - location of the ``subplot`` in its ``gridplot`` + position of the plot area. In a ``subplot`` position would correspond to the ``[row, column]`` + index of the ``subplot``. In docks this would correspond to a str name, "top", "right", "bottom" or "left" camera: pygfx.PerspectiveCamera - Use perspective camera for both perspective and orthographic views. Set fov = 0 for orthographic mode. + Use perspective camera for both perspective and orthographic views. Set fov = 0 for orthographic projection controller: pygfx.Controller - One of the pygfx controllers, panzoom, fly, orbit, or trackball + One of the pygfx controllers: "panzoom", "fly", "trackball", "orbit" scene: pygfx.Scene represents the root of a scene graph, will be viewed by the given ``camera`` @@ -62,20 +63,17 @@ def __init__( renders the scene onto the canvas name: str, optional - name this ``subplot`` or ``plot`` + name this plot area """ - self._parent: PlotArea = parent + self._parent = parent self._position = position self._scene = scene self._canvas = canvas self._renderer = renderer - if parent is None: - self._viewport: pygfx.Viewport = pygfx.Viewport(renderer) - else: - self._viewport = pygfx.Viewport(parent.renderer) + self._viewport: pygfx.Viewport = pygfx.Viewport(renderer) self._camera = camera self._controller = controller @@ -85,18 +83,18 @@ def __init__( self.viewport, ) - self._animate_funcs_pre = list() - self._animate_funcs_post = list() + self._animate_funcs_pre: list[callable] = list() + self._animate_funcs_post: list[callable] = list() self.renderer.add_event_handler(self.set_viewport_rect, "resize") # list of hex id strings for all graphics managed by this PlotArea # the real Graphic instances are stored in the ``GRAPHICS`` dict - self._graphics: List[str] = list() + self._graphics: list[str] = list() # selectors are in their own list so they can be excluded from scene bbox calculations # managed similar to GRAPHICS for garbage collection etc. - self._selectors: List[str] = list() + self._selectors: list[str] = list() self._name = name @@ -108,11 +106,11 @@ def __init__( # several read-only properties @property def parent(self): - """A parent if relevant, used by individual Subplots in GridPlot""" + """A parent if relevant""" return self._parent @property - def position(self) -> Union[Tuple[int, int], Any]: + def position(self) -> tuple[int, int] | str: """Position of this plot area within a larger layout (such as GridPlot) if relevant""" return self._position @@ -142,7 +140,7 @@ def camera(self) -> pygfx.PerspectiveCamera: return self._camera @camera.setter - def camera(self, new_camera: Union[str, pygfx.PerspectiveCamera]): + def camera(self, new_camera: str | pygfx.PerspectiveCamera): # user wants to set completely new camera, remove current camera from controller if isinstance(new_camera, pygfx.PerspectiveCamera): self.controller.remove_camera(self._camera) @@ -178,7 +176,7 @@ def controller(self) -> pygfx.Controller: return self._controller @controller.setter - def controller(self, new_controller: Union[str, pygfx.Controller]): + def controller(self, new_controller: str | pygfx.Controller): new_controller = create_controller(new_controller, self._camera) cameras_list = list() @@ -206,7 +204,7 @@ def controller(self, new_controller: Union[str, pygfx.Controller]): self._controller = new_controller @property - def graphics(self) -> Tuple[Graphic, ...]: + def graphics(self) -> tuple[Graphic, ...]: """Graphics in the plot area. Always returns a proxy to the Graphic instances.""" proxies = list() for loc in self._graphics: @@ -218,7 +216,7 @@ def graphics(self) -> Tuple[Graphic, ...]: return tuple(proxies) @property - def selectors(self) -> Tuple[BaseSelector, ...]: + def selectors(self) -> tuple[BaseSelector, ...]: """Selectors in the plot area. Always returns a proxy to the Graphic instances.""" proxies = list() for loc in self._selectors: @@ -228,7 +226,7 @@ def selectors(self) -> Tuple[BaseSelector, ...]: return tuple(proxies) @property - def legends(self) -> Tuple[Legend, ...]: + def legends(self) -> tuple[Legend, ...]: """Legends in the plot area.""" proxies = list() for loc in self._graphics: @@ -253,7 +251,7 @@ def name(self, name: str): raise TypeError("PlotArea `name` must be of type ") self._name = name - def get_rect(self) -> Tuple[float, float, float, float]: + def get_rect(self) -> tuple[float, float, float, float]: """ Returns the viewport rect to define the rectangle occupied by the viewport w.r.t. the Canvas. @@ -267,7 +265,7 @@ def get_rect(self) -> Tuple[float, float, float, float]: raise NotImplementedError("Must be implemented in subclass") def map_screen_to_world( - self, pos: Union[Tuple[float, float], pygfx.PointerEvent] + self, pos: tuple[float, float] | pygfx.PointerEvent ) -> np.ndarray: """ Map screen position to world position @@ -316,7 +314,7 @@ def render(self): self._call_animate_functions(self._animate_funcs_post) - def _call_animate_functions(self, funcs: Iterable[callable]): + def _call_animate_functions(self, funcs: list[callable]): for fn in funcs: try: args = getfullargspec(fn).args @@ -337,7 +335,7 @@ def _call_animate_functions(self, funcs: Iterable[callable]): def add_animations( self, - *funcs: Iterable[callable], + *funcs: callable, pre_render: bool = True, post_render: bool = False, ): @@ -347,7 +345,7 @@ def add_animations( Parameters ---------- - *funcs: callable or iterable of callable + *funcs: callable(s) function(s) that are called on each render cycle pre_render: bool, default ``True``, optional keyword-only argument @@ -460,7 +458,7 @@ def _add_or_insert_graphic( self, graphic: Graphic, center: bool = True, - action: str = Union["insert", "add"], + action: str = Literal["insert", "add"], index: int = 0, ): """Private method to handle inserting or adding a graphic to a PlotArea.""" @@ -570,7 +568,7 @@ def center_scene(self, *, zoom: float = 1.35): def auto_scale( self, *, # since this is often used as an event handler, don't want to coerce maintain_aspect = True - maintain_aspect: Union[None, bool] = None, + maintain_aspect: None | bool = None, zoom: float = 0.8, ): """ @@ -650,6 +648,7 @@ def delete_graphic(self, graphic: Graphic): """ # TODO: proper gc of selectors, RAM is freed for regular graphics but not selectors # TODO: references to selectors must be lingering somewhere + # TODO: update March 2024, I think selectors are gc properly, should check # get location loc = graphic.loc @@ -718,7 +717,7 @@ def __getitem__(self, name: str): f"The current selectors are:\n {selector_names}" ) - def __contains__(self, item: Union[str, Graphic]): + def __contains__(self, item: str | Graphic): to_check = [*self.graphics, *self.selectors, *self.legends] if isinstance(item, Graphic): diff --git a/fastplotlib/layouts/_record_mixin.py b/fastplotlib/layouts/_record_mixin.py index e3bfdeba5..59a8e92e4 100644 --- a/fastplotlib/layouts/_record_mixin.py +++ b/fastplotlib/layouts/_record_mixin.py @@ -1,4 +1,3 @@ -from typing import * from pathlib import Path from multiprocessing import Queue, Process from time import time @@ -21,7 +20,7 @@ class VideoWriterAV(Process): def __init__( self, - path: Union[Path, str], + path: Path | str, queue: Queue, fps: int, width: int, @@ -115,7 +114,7 @@ def _record(self): def record_start( self, - path: Union[str, Path], + path: str | Path, fps: int = 25, codec: str = "mpeg4", pixel_format: str = "yuv420p", diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 509840fa7..4b1e92c51 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -1,4 +1,4 @@ -from typing import * +from typing import Literal, Union import numpy as np @@ -9,18 +9,22 @@ from ..graphics import TextGraphic from ._utils import make_canvas_and_renderer, create_camera, create_controller from ._plot_area import PlotArea -from .graphic_methods_mixin import GraphicMethodsMixin +from ._graphic_methods_mixin import GraphicMethodsMixin class Subplot(PlotArea, GraphicMethodsMixin): def __init__( self, - parent: Any = None, - position: Tuple[int, int] = None, - parent_dims: Tuple[int, int] = None, - camera: Union[str, pygfx.PerspectiveCamera] = "2d", - controller: Union[str, pygfx.Controller] = None, - canvas: Union[str, WgpuCanvasBase, pygfx.Texture] = None, + parent: Union["GridPlot", None] = None, + position: tuple[int, int] = None, + parent_dims: tuple[int, int] = None, + camera: Literal["2d", "3d"] | pygfx.PerspectiveCamera = "2d", + controller: ( + Literal["panzoom", "fly", "trackball", "orbit"] | pygfx.Controller + ) = None, + canvas: ( + Literal["glfw", "jupyter", "qt", "wx"] | WgpuCanvasBase | pygfx.Texture + ) = None, renderer: pygfx.WgpuRenderer = None, name: str = None, ): @@ -33,7 +37,7 @@ def __init__( Parameters ---------- - parent: Any + parent: 'GridPlot' | None parent GridPlot instance position: (int, int), optional @@ -51,7 +55,7 @@ def __init__( | if ``str``, must be one of: `"panzoom", "fly", "trackball", or "orbit"`. | also accepts a pygfx.Controller instance - canvas: one of "jupyter", "glfw", "qt", WgpuCanvas, or pygfx.Texture, optional + canvas: one of "jupyter", "glfw", "qt", "ex, a WgpuCanvas, or a pygfx.Texture, optional Provides surface on which a scene will be rendered. Can optionally provide a WgpuCanvas instance or a str to force the PlotArea to use a specific canvas from one of the following options: "jupyter", "glfw", "qt". Can also provide a pygfx Texture to render to. @@ -113,11 +117,11 @@ def __init__( self.set_title(self.name) @property - def name(self) -> Any: + def name(self) -> str: return self._name @name.setter - def name(self, name: Any): + def name(self, name: str): self._name = name self.set_title(name) @@ -136,7 +140,7 @@ def docks(self) -> dict: """ return self._docks - def set_title(self, text: Any): + def set_title(self, text: str): """Sets the plot title, stored as a ``TextGraphic`` in the "top" dock area""" if text is None: return diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 5ee930b67..6994838d5 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -1,15 +1,14 @@ -from typing import * import importlib import pygfx -from pygfx import WgpuRenderer, Texture +from pygfx import WgpuRenderer, Texture, Renderer from wgpu.gui import WgpuCanvasBase from ..utils import gui def make_canvas_and_renderer( - canvas: Union[str, WgpuCanvasBase, Texture, None], renderer: [WgpuRenderer, None] + canvas: str | WgpuCanvasBase | Texture | None, renderer: Renderer | None ): """ Parses arguments and returns the appropriate canvas and renderer instances @@ -22,19 +21,23 @@ def make_canvas_and_renderer( m = importlib.import_module("wgpu.gui." + canvas) canvas = m.WgpuCanvas(max_fps=60) elif not isinstance(canvas, (WgpuCanvasBase, Texture)): - raise ValueError( + raise TypeError( f"canvas option must either be a valid WgpuCanvas implementation, a pygfx Texture" f" or a str with the wgpu gui backend name." ) if renderer is None: renderer = WgpuRenderer(canvas, pixel_ratio=2) + elif not isinstance(renderer, Renderer): + raise TypeError( + f"renderer option must be a pygfx.Renderer instance such as pygfx.WgpuRenderer" + ) return canvas, renderer def create_camera( - camera_type: Union[pygfx.PerspectiveCamera, str], + camera_type: pygfx.PerspectiveCamera | str, ) -> pygfx.PerspectiveCamera: if isinstance(camera_type, pygfx.PerspectiveCamera): return camera_type @@ -61,7 +64,7 @@ def create_camera( def create_controller( - controller_type: Union[pygfx.Controller, None, str], + controller_type: pygfx.Controller | None | str, camera: pygfx.PerspectiveCamera, ) -> pygfx.Controller: """ diff --git a/fastplotlib/legends/legend.py b/fastplotlib/legends/legend.py index 291c25ff3..be90004aa 100644 --- a/fastplotlib/legends/legend.py +++ b/fastplotlib/legends/legend.py @@ -1,6 +1,6 @@ from functools import partial from collections import OrderedDict -from typing import * +from typing import Iterable import numpy as np import pygfx @@ -31,7 +31,7 @@ def __init__( class LineLegendItem(LegendItem): def __init__( - self, parent, graphic: LineGraphic, label: str, position: Tuple[int, int] + self, parent, graphic: LineGraphic, label: str, position: tuple[int, int] ): """ @@ -142,7 +142,7 @@ class Legend(Graphic): def __init__( self, plot_area, - highlight_color: Union[str, tuple, np.ndarray] = "w", + highlight_color: str | tuple | np.ndarray = "w", max_rows: int = 5, *args, **kwargs, @@ -161,7 +161,7 @@ def __init__( maximum number of rows allowed in the legend """ - self._graphics: List[Graphic] = list() + self._graphics: list[Graphic] = list() # hex id of Graphic, i.e. graphic.loc are the keys self._items: OrderedDict[str:LegendItem] = OrderedDict() @@ -204,7 +204,7 @@ def __init__( self._row_counter = 0 self._col_counter = 0 - def graphics(self) -> Tuple[Graphic, ...]: + def graphics(self) -> tuple[Graphic, ...]: return tuple(self._graphics) def _check_label_unique(self, label): @@ -235,7 +235,7 @@ def add_graphic(self, graphic: Graphic, label: str = None): # get x position offset for this new column of LegendItems # start by getting the LegendItems in the previous column - prev_column_items: List[LegendItem] = list(self._items.values())[ + prev_column_items: list[LegendItem] = list(self._items.values())[ -self._max_rows : ] # x position of LegendItems in previous column diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index 8d1e8694f..da781b521 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -1,5 +1,4 @@ from collections import OrderedDict -from typing import * from pathlib import Path import numpy as np @@ -165,7 +164,7 @@ def make_colors_dict(labels: iter, cmap: str, **kwargs) -> OrderedDict: return OrderedDict(zip(labels, colors)) -def quick_min_max(data: np.ndarray) -> Tuple[float, float]: +def quick_min_max(data: np.ndarray) -> tuple[float, float]: """ Adapted from pyqtgraph.ImageView. Estimate the min/max values of *data* by subsampling. @@ -220,7 +219,7 @@ def make_pygfx_colors(colors, n_colors): return colors_array -def calculate_gridshape(n_subplots: int) -> Tuple[int, int]: +def calculate_gridshape(n_subplots: int) -> tuple[int, int]: """ Returns ``(n_rows, n_cols)`` from given number of subplots ``n_subplots`` """ @@ -240,7 +239,7 @@ def normalize_min_max(a): def parse_cmap_values( n_colors: int, cmap_name: str, - cmap_values: Union[np.ndarray, List[Union[int, float]]] = None, + cmap_values: np.ndarray | list[int | float] = None, ) -> np.ndarray: """ diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 31f6ab8e9..43f2b48b3 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -1,4 +1,3 @@ -from typing import * import weakref import numpy as np @@ -112,7 +111,7 @@ def __init__( self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) - def _get_vmin_vmax_str(self) -> Tuple[str, str]: + def _get_vmin_vmax_str(self) -> tuple[str, str]: if self.vmin < 0.001 or self.vmin > 99_999: vmin_str = f"{self.vmin:.2e}" else: diff --git a/fastplotlib/utils/generate_add_methods.py b/scripts/generate_add_graphic_methods.py similarity index 97% rename from fastplotlib/utils/generate_add_methods.py rename to scripts/generate_add_graphic_methods.py index 100ad7757..2a480d884 100644 --- a/fastplotlib/utils/generate_add_methods.py +++ b/scripts/generate_add_graphic_methods.py @@ -4,7 +4,7 @@ import black root = pathlib.Path(__file__).parent.parent.resolve() -filename = root.joinpath("layouts/graphic_methods_mixin.py") +filename = root.joinpath("fastplotlib", "layouts", "_graphic_methods_mixin.py") # if there is an existing mixin class, replace it with an empty class # so that fastplotlib will import diff --git a/setup.py b/setup.py index e8f2613d9..b50a6a9bf 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ license="Apache 2.0", author="Kushal Kolar, Caitlin Lewis", author_email="", - python_requires=">=3.9", + python_requires=">=3.10", install_requires=install_requires, extras_require=extras_require, include_package_data=True,