From 60e7f60d4f3aba38c7249a97f799cc74812372b9 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 3 Apr 2024 03:03:19 -0400 Subject: [PATCH 001/196] just a start --- fastplotlib/graphics/_features/_base.py | 160 ++++++++++++++++++------ 1 file changed, 120 insertions(+), 40 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 99ebbf436..235da1410 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -1,10 +1,10 @@ -from abc import ABC, abstractmethod +from abc import abstractmethod from inspect import getfullargspec from warnings import warn -from typing import * -import weakref +from typing import Any, Literal import numpy as np +from numpy.typing import NDArray import pygfx @@ -76,29 +76,14 @@ def __repr__(self): ) -class GraphicFeature(ABC): - def __init__(self, parent, data: Any, collection_index: int = None): - # not shown as a docstring so it doesn't show up in the docs - # - # Parameters - # ---------- - # parent - # - # data: Any - # - # collection_index: int - # if part of a collection, index of this graphic within the collection - - self._parent = weakref.proxy(parent) - - self._data = to_gpu_supported_dtype(data) - - self._collection_index = collection_index +class GraphicFeature: + def __init__(self, **kwargs): self._event_handlers = list() self._block_events = False - def __call__(self, *args, **kwargs): - return self._data + @property + def data(self) -> Any: + raise NotImplemented def block_events(self, val: bool): """ @@ -112,21 +97,12 @@ def block_events(self, val: bool): """ self._block_events = val - @abstractmethod - def _set(self, value): - pass - - def _parse_set_value(self, value): - if isinstance(value, GraphicFeature): - return value() - - return value - def add_event_handler(self, handler: callable): """ Add an event handler. All added event handlers are called when this feature changes. + The ``handler`` can optionally accept a :class:`.FeatureEvent` as the first and only argument. - The ``FeatureEvent`` only has two attributes, ``type`` which denotes the type of event + The ``FeatureEvent`` only has 2 attributes, ``type`` which denotes the type of event as a ``str`` in the form of "", such as "color". And ``pick_info`` which contains information about the event and Graphic that triggered it. @@ -166,7 +142,7 @@ def clear_event_handlers(self): # TODO: maybe this can be implemented right here in the base class @abstractmethod - def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): + def _feature_changed(self,new_data: Any, key: int | slice | tuple[slice] | None = None): """Called whenever a feature changes, and it calls all funcs in self._event_handlers""" pass @@ -191,12 +167,116 @@ def _call_event_handlers(self, event_data: FeatureEvent): ) func() - @abstractmethod def __repr__(self) -> str: - pass + raise NotImplementedError + + +class BufferManager(GraphicFeature): + """Smaller wrapper for pygfx.Buffer""" + + def __init__( + self, + data: NDArray, + buffer_type: Literal["buffer", "texture"] = "buffer", + isolated_buffer: bool = True, + texture_dim: int = 2, + **kwargs + ): + super().__init__() + if isolated_buffer: + # useful if data is read-only, example: memmaps + bdata = np.zeros(data.shape) + bdata[:] = data[:] + else: + # user's input array is used as the buffer + bdata = data + + if buffer_type == "buffer": + self._buffer = pygfx.Buffer(bdata) + elif buffer_type == "texture": + self._buffer = pygfx.Texture(bdata, dim=texture_dim) + else: + raise ValueError("`buffer_type` must be one of: 'buffer' or 'texture'") + + self._event_handlers: list[callable] = list() + + @property + def data(self) -> NDArray: + return self.buffer.data + + @property + def buffer(self) -> pygfx.Buffer | pygfx.Texture: + return self._buffer + + def __getitem__(self, item): + return self.buffer.data[item] + + def __setitem__(self, key, value): + raise NotImplementedError + + def _update_range(self, offset, size): + self.buffer.update_range(offset=offset, size=size) + + def __repr__(self): + return f"{self.__class__.__name__} buffer data:\n" \ + f"{self.data.__repr__()}" + + +def parse_colors(value, n): + """parse colors using pygfx and return RGBA array for each vertex""" + if isinstance(value, str): + return np.array([pygfx.Color(value)] * n) + + return value + + +def parse_colors(key, value, n_colors, max_n_colors): + """ + + Parameters + ---------- + key: slice + + value + + n_colors + + max_n_colors: basically data.shape[0] + + Returns + ------- + + """ + pass + + +class ColorFeature(BufferManager): + """Manage color buffer for positions type objects""" + + def __init__(self, data: str | np.ndarray, n_colors: int, isolated_buffer: bool): + if not isinstance(data, np.ndarray): + # isolated buffer is only useful when data is a numpy array + isolated_buffer = False + + colors = parse_colors(data, n_colors) + + super().__init__(colors, isolated_buffer) + + def __setitem__(self, key, value): + if isinstance(value, BufferManager): + # trying to set feature from another feature instance + value = value.data + + key = self.cleanup_slice(key) + + colors = parse_colors(value, len(key)) + + self.buffer.data[key] = colors + + self._update_range(key.start, key.stop - key.start) -def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]: +def cleanup_slice(key: int | slice, upper_bound) -> slice | int: """ If the key in an `int`, it just returns it. Otherwise, @@ -257,7 +337,7 @@ def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]: return slice(start, stop, step) -def cleanup_array_slice(key: np.ndarray, upper_bound) -> Union[np.ndarray, None]: +def cleanup_array_slice(key: np.ndarray, upper_bound) -> np.darray | None: """ Cleanup numpy array used for fancy indexing, make sure key[-1] <= upper_bound. @@ -321,7 +401,7 @@ def _update_range(self, key): @property @abstractmethod - def buffer(self) -> Union[pygfx.Buffer, pygfx.Texture]: + def buffer(self) -> pygfx.Buffer | pygfx.Texture: """Underlying buffer for this feature""" pass From 6eec7d5440ffedd41a170ffee2447485bb08c9ce Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 16 May 2024 22:54:58 -0400 Subject: [PATCH 002/196] pushing to continue on my desktop --- fastplotlib/graphics/_features/_base.py | 27 ++++++++++++++++++----- fastplotlib/graphics/_features/_colors.py | 15 +++---------- fastplotlib/graphics/_features/utils.py | 7 ++++++ 3 files changed, 32 insertions(+), 17 deletions(-) create mode 100644 fastplotlib/graphics/_features/utils.py diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 235da1410..1280829ed 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -80,9 +80,10 @@ class GraphicFeature: def __init__(self, **kwargs): self._event_handlers = list() self._block_events = False + self.collection_index: int = None @property - def data(self) -> Any: + def value(self) -> Any: raise NotImplemented def block_events(self, val: bool): @@ -99,7 +100,7 @@ def block_events(self, val: bool): def add_event_handler(self, handler: callable): """ - Add an event handler. All added event handlers are called when this feature changes. + Add an event handler. All added event handlers are calledcollection_ind when this feature changes. The ``handler`` can optionally accept a :class:`.FeatureEvent` as the first and only argument. The ``FeatureEvent`` only has 2 attributes, ``type`` which denotes the type of event @@ -201,7 +202,7 @@ def __init__( self._event_handlers: list[callable] = list() @property - def data(self) -> NDArray: + def value(self) -> NDArray: return self.buffer.data @property @@ -219,7 +220,23 @@ def _update_range(self, offset, size): def __repr__(self): return f"{self.__class__.__name__} buffer data:\n" \ - f"{self.data.__repr__()}" + f"{self.value.__repr__()}" + + +class GraphicProperty: + def __init__(self, name, collection_index: int = None): + self.name = name + + def _get_feature(self, instance): + feature: GraphicFeature = getattr(instance, f"_{self.name}") + return feature + + def __get__(self, instance, owner): + return self._get_feature(instance) + + def __set__(self, obj, value): + feature = self._get_feature(obj) + feature[:] = value def parse_colors(value, n): @@ -265,7 +282,7 @@ def __init__(self, data: str | np.ndarray, n_colors: int, isolated_buffer: bool) def __setitem__(self, key, value): if isinstance(value, BufferManager): # trying to set feature from another feature instance - value = value.data + value = value.value key = self.cleanup_slice(key) diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index 48405e74c..8a17225b7 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -10,14 +10,14 @@ ) from ._base import ( GraphicFeature, - GraphicFeatureIndexable, + BufferManager, cleanup_slice, FeatureEvent, cleanup_array_slice, ) -class ColorFeature(GraphicFeatureIndexable): +class ColorFeature(BufferManager): """ Manages the color buffer for :class:`LineGraphic` or :class:`ScatterGraphic` @@ -34,20 +34,11 @@ class ColorFeature(GraphicFeatureIndexable): """ - @property - def buffer(self) -> pygfx.Buffer: - return self._parent.world_object.geometry.colors - - def __getitem__(self, item): - return self.buffer.data[item] - def __init__( self, - parent, colors, n_colors: int, - alpha: float = 1.0, - collection_index: int = None, + alpha: float = None, ): """ ColorFeature diff --git a/fastplotlib/graphics/_features/utils.py b/fastplotlib/graphics/_features/utils.py new file mode 100644 index 000000000..0ffa08c13 --- /dev/null +++ b/fastplotlib/graphics/_features/utils.py @@ -0,0 +1,7 @@ +import pygfx +import numpy as np +from typing import Iterable + + +def parse_colors(colors: str | np.ndarray | Iterable[str]): + pass From 02491258306cf27a36d5e337ae7c289d3c1fd593 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 17 May 2024 00:51:24 -0400 Subject: [PATCH 003/196] progress on buffer manager cleanup_key --- fastplotlib/graphics/_features/_base.py | 269 +++++++--------------- fastplotlib/graphics/_features/_colors.py | 33 +-- fastplotlib/graphics/_features/utils.py | 21 +- 3 files changed, 107 insertions(+), 216 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 1280829ed..3c434ec79 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -209,13 +209,88 @@ def value(self) -> NDArray: def buffer(self) -> pygfx.Buffer | pygfx.Texture: return self._buffer + def cleanup_key(self, key: int | np.ndarray[int, bool] | slice | tuple[slice, ...]) -> int | np.ndarray | range: + """ + Cleanup slice indices for setitem, returns positive indices. Converts negative indices to positive if necessary. + + Returns a cleaned up key corresponding to only the first dimension. + """ + upper_bound = self.value.shape[0] + + if isinstance(key, int): + if abs(key) > upper_bound: # absolute value in case negative index + raise IndexError(f"key value: {key} out of range for dimension with size: {upper_bound}") + return [key] + + elif isinstance(key, np.ndarray): + if key.ndim > 1: + raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing") + + # if boolean array convert to integer array of indices + if key.dtype == bool: + key = np.nonzero(key)[0] + + if key.size < 1: + return None + + # make sure indices within bounds of feature buffer range + if key[-1] > upper_bound: + raise IndexError( + f"Index: `{key[-1]}` out of bounds for feature array of size: `{upper_bound}`" + ) + + # make sure indices are integers + if np.issubdtype(key.dtype, np.integer): + return key + + raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing graphic features") + + elif isinstance(key, tuple): + if isinstance(key[0], slice): + key = key[0] + else: + raise TypeError + + if not isinstance(key, (slice, range)): + raise TypeError("Must pass slice or int object") + + start = key.start if key.start is not None else 0 + stop = key.stop if key.stop is not None else self.value.shape[0] + # absolute value of the step in case it's negative + # since we convert start and stop to be positive below it is fine for step to be converted to positive + step = abs(key.step) if key.step is not None else 1 + + # modulus in case of negative indices + start %= upper_bound + stop %= upper_bound + + if start > stop: + raise ValueError("start index greater than stop index") + + return range(start, stop, step) + def __getitem__(self, item): return self.buffer.data[item] def __setitem__(self, key, value): raise NotImplementedError - def _update_range(self, offset, size): + def _update_range(self, key): + # assumes key is already cleaned up + if isinstance(key, range): + offset = key.start + size = key.stop - key.start + + elif isinstance(key, np.ndarray): + offset = key.min() + size = key.max() - offset + + elif isinstance(key, int): + offset = key + size = 1 + else: + raise TypeError + self.buffer.update_range(offset=offset, size=size) def __repr__(self): @@ -224,7 +299,7 @@ def __repr__(self): class GraphicProperty: - def __init__(self, name, collection_index: int = None): + def __init__(self, name): self.name = name def _get_feature(self, instance): @@ -239,33 +314,6 @@ def __set__(self, obj, value): feature[:] = value -def parse_colors(value, n): - """parse colors using pygfx and return RGBA array for each vertex""" - if isinstance(value, str): - return np.array([pygfx.Color(value)] * n) - - return value - - -def parse_colors(key, value, n_colors, max_n_colors): - """ - - Parameters - ---------- - key: slice - - value - - n_colors - - max_n_colors: basically data.shape[0] - - Returns - ------- - - """ - pass - class ColorFeature(BufferManager): """Manage color buffer for positions type objects""" @@ -291,166 +339,3 @@ def __setitem__(self, key, value): self.buffer.data[key] = colors self._update_range(key.start, key.stop - key.start) - - -def cleanup_slice(key: int | slice, upper_bound) -> slice | int: - """ - - If the key in an `int`, it just returns it. Otherwise, - it parses it and removes the `None` vals and replaces - them with corresponding values that can be used to - create a `range`, get `len` etc. - - Parameters - ---------- - key - upper_bound - - Returns - ------- - - """ - if isinstance(key, int): - return key - - if isinstance(key, np.ndarray): - return cleanup_array_slice(key, upper_bound) - - if isinstance(key, tuple): - # if tuple of slice we only need the first obj - # since the first obj is the datapoint indices - if isinstance(key[0], slice): - key = key[0] - else: - raise TypeError("Tuple slicing must have slice object in first position") - - if not isinstance(key, slice): - raise TypeError("Must pass slice or int object") - - start = key.start - stop = key.stop - step = key.step - for attr in [start, stop, step]: - if attr is None: - continue - if attr < 0: - raise IndexError("Negative indexing not supported.") - - if start is None: - start = 0 - - if stop is None: - stop = upper_bound - - elif stop > upper_bound: - raise IndexError( - f"Index: `{stop}` out of bounds for feature array of size: `{upper_bound}`" - ) - - step = key.step - if step is None: - step = 1 - - return slice(start, stop, step) - - -def cleanup_array_slice(key: np.ndarray, upper_bound) -> np.darray | None: - """ - Cleanup numpy array used for fancy indexing, make sure key[-1] <= upper_bound. - - Returns None if nothing to change. - - Parameters - ---------- - key: np.ndarray - integer or boolean array - - upper_bound - - Returns - ------- - np.ndarray - integer indexing array - - """ - - if key.ndim > 1: - raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing") - - # if boolean array convert to integer array of indices - if key.dtype == bool: - key = np.nonzero(key)[0] - - if key.size < 1: - return None - - # make sure indices within bounds of feature buffer range - if key[-1] > upper_bound: - raise IndexError( - f"Index: `{key[-1]}` out of bounds for feature array of size: `{upper_bound}`" - ) - - # make sure indices are integers - if np.issubdtype(key.dtype, np.integer): - return key - - raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing") - - -class GraphicFeatureIndexable(GraphicFeature): - """An indexable Graphic Feature, colors, data, sizes etc.""" - - def _set(self, value): - value = self._parse_set_value(value) - self[:] = value - - @abstractmethod - def __getitem__(self, item): - pass - - @abstractmethod - def __setitem__(self, key, value): - pass - - @abstractmethod - def _update_range(self, key): - pass - - @property - @abstractmethod - def buffer(self) -> pygfx.Buffer | pygfx.Texture: - """Underlying buffer for this feature""" - pass - - @property - def _upper_bound(self) -> int: - return self._data.shape[0] - - def _update_range_indices(self, key): - """Currently used by colors and positions data""" - if not isinstance(key, np.ndarray): - key = cleanup_slice(key, self._upper_bound) - - if isinstance(key, int): - self.buffer.update_range(key, size=1) - return - - # else if it's a slice obj - if isinstance(key, slice): - if key.step == 1: # we cleaned up the slice obj so step of None becomes 1 - # update range according to size using the offset - self.buffer.update_range(offset=key.start, size=key.stop - key.start) - - else: - step = key.step - # convert slice to indices - ixs = range(key.start, key.stop, step) - for ix in ixs: - self.buffer.update_range(ix, size=1) - - # TODO: See how efficient this is with large indexing - elif isinstance(key, np.ndarray): - self.buffer.update_range() - - else: - raise TypeError("must pass int or slice to update range") diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index 8a17225b7..aefd36a94 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -39,6 +39,7 @@ def __init__( colors, n_colors: int, alpha: float = None, + isolated_buffer: bool = True, ): """ ColorFeature @@ -118,17 +119,11 @@ def __init__( super().__init__(parent, data, collection_index=collection_index) def __setitem__(self, key, value): - # parse numerical slice indices - if isinstance(key, slice): - _key = cleanup_slice(key, self._upper_bound) - indices = range(_key.start, _key.stop, _key.step) - - # or single numerical index - elif isinstance(key, (int, np.integer)): - key = cleanup_slice(key, self._upper_bound) - indices = [key] + if isinstance(key, (int, np.ndarray, tuple, slice, range)): + key = self.cleanup_key(key) elif isinstance(key, tuple): + # directly setting RGBA values on every datapoint if not isinstance(value, (float, int, np.ndarray)): raise ValueError( "If using multiple-fancy indexing for color, you can only set numerical" @@ -144,26 +139,20 @@ def __setitem__(self, key, value): self.buffer.data[key] = value # update range - # first slice obj is going to be the indexing so use key[0] + # first slice obj is going to be the datapoints to modify so use key[0] # key[1] is going to be RGBA so get rid of it to pass to _update_range - # _key = cleanup_slice(key[0], self._upper_bound) + key = self.cleanup_key(key[0]) self._update_range(key) + self._feature_changed(key, value) return - elif isinstance(key, np.ndarray): - key = cleanup_array_slice(key, self._upper_bound) - if key is None: - return - - indices = key - else: raise TypeError( "Graphic features only support integer and numerical fancy indexing" ) - new_data_size = len(indices) + new_data_size = len(key) if not isinstance(value, np.ndarray): color = np.array(pygfx.Color(value)) # pygfx color parser @@ -199,14 +188,14 @@ def __setitem__(self, key, value): "numpy array passed to color must be of shape (4,) or (n_colors_modify, 4)" ) + else: + raise TypeError + self.buffer.data[key] = new_colors self._update_range(key) self._feature_changed(key, new_colors) - def _update_range(self, key): - self._update_range_indices(key) - def _feature_changed(self, key, new_data): key = cleanup_slice(key, self._upper_bound) if isinstance(key, int): diff --git a/fastplotlib/graphics/_features/utils.py b/fastplotlib/graphics/_features/utils.py index 0ffa08c13..5b0ccdb54 100644 --- a/fastplotlib/graphics/_features/utils.py +++ b/fastplotlib/graphics/_features/utils.py @@ -3,5 +3,22 @@ from typing import Iterable -def parse_colors(colors: str | np.ndarray | Iterable[str]): - pass +def parse_colors( + colors: str | np.ndarray | Iterable[str], + n_colors: int | None, + alpha: float | None = None, + key: int | tuple | slice | None = None, +): + """ + + Parameters + ---------- + colors + n_colors + alpha + key + + Returns + ------- + + """ From 9d891ee75da19670bdfe943b25c0fd7c1a7ed483 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 17 May 2024 01:33:07 -0400 Subject: [PATCH 004/196] more progress, still lots to do --- fastplotlib/graphics/_features/_colors.py | 67 ++--------------------- fastplotlib/graphics/_features/_data.py | 29 +++------- fastplotlib/graphics/_features/utils.py | 64 ++++++++++++++++++++++ 3 files changed, 76 insertions(+), 84 deletions(-) diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index aefd36a94..81adeebbd 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -4,17 +4,15 @@ from ...utils import ( make_colors, get_cmap_texture, - make_pygfx_colors, parse_cmap_values, quick_min_max, ) from ._base import ( GraphicFeature, BufferManager, - cleanup_slice, FeatureEvent, - cleanup_array_slice, ) +from .utils import parse_colors class ColorFeature(BufferManager): @@ -59,64 +57,9 @@ def __init__( alpha value for the colors """ - # if provided as a numpy array of str - if isinstance(colors, np.ndarray): - if colors.dtype.kind in ["U", "S"]: - colors = colors.tolist() - # if the color is provided as a numpy array - if isinstance(colors, np.ndarray): - if colors.shape == (4,): # single RGBA array - data = np.repeat(np.array([colors]), n_colors, axis=0) - # else assume it's already a stack of RGBA arrays, keep this directly as the data - elif colors.ndim == 2: - if colors.shape[1] != 4 and colors.shape[0] != n_colors: - raise ValueError( - "Valid array color arguments must be a single RGBA array or a stack of " - "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" - ) - data = colors - else: - raise ValueError( - "Valid array color arguments must be a single RGBA array or a stack of " - "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" - ) - - # if the color is provided as an iterable - elif isinstance(colors, (list, tuple, np.ndarray)): - # if iterable of str - if all([isinstance(val, str) for val in colors]): - if not len(colors) == n_colors: - raise ValueError( - f"Valid iterable color arguments must be a `tuple` or `list` of `str` " - f"where the length of the iterable is the same as the number of datapoints." - ) - - data = np.vstack([np.array(pygfx.Color(c)) for c in colors]) - - # if it's a single RGBA array as a tuple/list - elif len(colors) == 4: - c = pygfx.Color(colors) - data = np.repeat(np.array([c]), n_colors, axis=0) - - else: - raise ValueError( - f"Valid iterable color arguments must be a `tuple` or `list` representing RGBA values or " - f"an iterable of `str` with the same length as the number of datapoints." - ) - elif isinstance(colors, str): - if colors == "random": - data = np.random.rand(n_colors, 4) - data[:, -1] = alpha - else: - data = make_pygfx_colors(colors, n_colors) - else: - # assume it's a single color, use pygfx.Color to parse it - data = make_pygfx_colors(colors, n_colors) - - if alpha != 1.0: - data[:, -1] = alpha - - super().__init__(parent, data, collection_index=collection_index) + data = parse_colors(colors, n_colors, alpha) + + super().__init__(data=data, isolated_buffer=isolated_buffer) def __setitem__(self, key, value): if isinstance(key, (int, np.ndarray, tuple, slice, range)): @@ -194,7 +137,7 @@ def __setitem__(self, key, value): self.buffer.data[key] = new_colors self._update_range(key) - self._feature_changed(key, new_colors) + # self._feature_changed(key, new_colors) def _feature_changed(self, key, new_data): key = cleanup_slice(key, self._upper_bound) diff --git a/fastplotlib/graphics/_features/_data.py b/fastplotlib/graphics/_features/_data.py index bcfe9446a..680218014 100644 --- a/fastplotlib/graphics/_features/_data.py +++ b/fastplotlib/graphics/_features/_data.py @@ -5,30 +5,20 @@ import pygfx from ._base import ( - GraphicFeatureIndexable, - cleanup_slice, + BufferManager, FeatureEvent, to_gpu_supported_dtype, - cleanup_array_slice, ) -class PointsDataFeature(GraphicFeatureIndexable): +class PointsDataFeature(BufferManager): """ Access to the vertex buffer data shown in the graphic. Supports fancy indexing if the data array also supports it. """ - def __init__(self, parent, data: Any, collection_index: int = None): - data = self._fix_data(data, parent) - super().__init__(parent, data, collection_index=collection_index) - - @property - def buffer(self) -> pygfx.Buffer: - return self._parent.world_object.geometry.positions - - def __getitem__(self, item): - return self.buffer.data[item] + def __init__(self, data: Any, isolated_buffer: bool = True): + super().__init__(data, isolated_buffer=isolated_buffer) def _fix_data(self, data, parent): graphic_type = parent.__class__.__name__ @@ -56,9 +46,7 @@ def _fix_data(self, data, parent): return data def __setitem__(self, key, value): - if isinstance(key, np.ndarray): - # make sure 1D array of int or boolean - key = cleanup_array_slice(key, self._upper_bound) + key = self.cleanup_key(key) # put data into right shape if they're only indexing datapoints if isinstance(key, (slice, int, np.ndarray, np.integer)): @@ -69,11 +57,8 @@ def __setitem__(self, key, value): self.buffer.data[key] = value self._update_range(key) # avoid creating dicts constantly if there are no events to handle - if len(self._event_handlers) > 0: - self._feature_changed(key, value) - - def _update_range(self, key): - self._update_range_indices(key) + # if len(self._event_handlers) > 0: + # self._feature_changed(key, value) def _feature_changed(self, key, new_data): if key is not None: diff --git a/fastplotlib/graphics/_features/utils.py b/fastplotlib/graphics/_features/utils.py index 5b0ccdb54..1b1bb56d9 100644 --- a/fastplotlib/graphics/_features/utils.py +++ b/fastplotlib/graphics/_features/utils.py @@ -2,6 +2,8 @@ import numpy as np from typing import Iterable +from ...utils import make_pygfx_colors + def parse_colors( colors: str | np.ndarray | Iterable[str], @@ -22,3 +24,65 @@ def parse_colors( ------- """ + + # if provided as a numpy array of str + if isinstance(colors, np.ndarray): + if colors.dtype.kind in ["U", "S"]: + colors = colors.tolist() + # if the color is provided as a numpy array + if isinstance(colors, np.ndarray): + if colors.shape == (4,): # single RGBA array + data = np.repeat(np.array([colors]), n_colors, axis=0) + # else assume it's already a stack of RGBA arrays, keep this directly as the data + elif colors.ndim == 2: + if colors.shape[1] != 4 and colors.shape[0] != n_colors: + raise ValueError( + "Valid array color arguments must be a single RGBA array or a stack of " + "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" + ) + data = colors + else: + raise ValueError( + "Valid array color arguments must be a single RGBA array or a stack of " + "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" + ) + + # if the color is provided as an iterable + elif isinstance(colors, (list, tuple, np.ndarray)): + # if iterable of str + if all([isinstance(val, str) for val in colors]): + if not len(colors) == n_colors: + raise ValueError( + f"Valid iterable color arguments must be a `tuple` or `list` of `str` " + f"where the length of the iterable is the same as the number of datapoints." + ) + + data = np.vstack([np.array(pygfx.Color(c)) for c in colors]) + + # if it's a single RGBA array as a tuple/list + elif len(colors) == 4: + c = pygfx.Color(colors) + data = np.repeat(np.array([c]), n_colors, axis=0) + + else: + raise ValueError( + f"Valid iterable color arguments must be a `tuple` or `list` representing RGBA values or " + f"an iterable of `str` with the same length as the number of datapoints." + ) + elif isinstance(colors, str): + if colors == "random": + data = np.random.rand(n_colors, 4) + data[:, -1] = alpha + else: + data = make_pygfx_colors(colors, n_colors) + else: + # assume it's a single color, use pygfx.Color to parse it + data = make_pygfx_colors(colors, n_colors) + + if alpha is not None: + if isinstance(alpha, float): + data[:, -1] = alpha + else: + raise TypeError("if alpha is provided it must be of type `float`") + + return data From 9635bc010ab3d517ea24c1ad26aa91b376967718 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 17 May 2024 23:28:59 -0400 Subject: [PATCH 005/196] slicing working with PointsDataFeature, negative slices too, still major WIP --- fastplotlib/graphics/_features/__init__.py | 77 +++-- fastplotlib/graphics/_features/_base.py | 68 ++-- fastplotlib/graphics/_features/_colors.py | 378 ++++++++++----------- fastplotlib/graphics/_features/_data.py | 261 +++++++------- fastplotlib/graphics/_features/utils.py | 3 +- 5 files changed, 401 insertions(+), 386 deletions(-) diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index fb25db287..417472f72 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -1,33 +1,60 @@ -from ._colors import ColorFeature, CmapFeature, ImageCmapFeature, HeatmapCmapFeature -from ._data import PointsDataFeature, ImageDataFeature, HeatmapDataFeature -from ._sizes import PointsSizesFeature -from ._present import PresentFeature -from ._thickness import ThicknessFeature +from ._colors import ColorFeature#, CmapFeature, ImageCmapFeature, HeatmapCmapFeature +from ._data import PointsDataFeature#, ImageDataFeature, HeatmapDataFeature +# from ._sizes import PointsSizesFeature +# from ._present import PresentFeature +# from ._thickness import ThicknessFeature from ._base import ( GraphicFeature, - GraphicFeatureIndexable, + BufferManager, + GraphicFeatureDescriptor, FeatureEvent, to_gpu_supported_dtype, ) from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature from ._deleted import Deleted +# +# __all__ = [ +# "ColorFeature", +# "CmapFeature", +# "ImageCmapFeature", +# "HeatmapCmapFeature", +# "PointsDataFeature", +# "PointsSizesFeature", +# "ImageDataFeature", +# "HeatmapDataFeature", +# "PresentFeature", +# "ThicknessFeature", +# "GraphicFeature", +# "FeatureEvent", +# "to_gpu_supported_dtype", +# "LinearSelectionFeature", +# "LinearRegionSelectionFeature", +# "Deleted", +# ] -__all__ = [ - "ColorFeature", - "CmapFeature", - "ImageCmapFeature", - "HeatmapCmapFeature", - "PointsDataFeature", - "PointsSizesFeature", - "ImageDataFeature", - "HeatmapDataFeature", - "PresentFeature", - "ThicknessFeature", - "GraphicFeature", - "GraphicFeatureIndexable", - "FeatureEvent", - "to_gpu_supported_dtype", - "LinearSelectionFeature", - "LinearRegionSelectionFeature", - "Deleted", -] +class PresentFeature: + pass + +class Deleted: + pass + +class CmapFeature: + pass + +class PointsSizesFeature: + pass + +class ThicknessFeature: + pass + +class ImageCmapFeature: + pass + +class ImageDataFeature: + pass + +class HeatmapDataFeature: + pass + +class HeatmapCmapFeature: + pass \ No newline at end of file diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 3c434ec79..0a9e29e95 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -186,7 +186,7 @@ def __init__( super().__init__() if isolated_buffer: # useful if data is read-only, example: memmaps - bdata = np.zeros(data.shape) + bdata = np.zeros(data.shape, dtype=data.dtype) bdata[:] = data[:] else: # user's input array is used as the buffer @@ -209,7 +209,7 @@ def value(self) -> NDArray: def buffer(self) -> pygfx.Buffer | pygfx.Texture: return self._buffer - def cleanup_key(self, key: int | np.ndarray[int, bool] | slice | tuple[slice, ...]) -> int | np.ndarray | range: + def cleanup_key(self, key: int | np.ndarray[int, bool] | slice | tuple[slice, ...]) -> int | np.ndarray | range | tuple[range, ...]: """ Cleanup slice indices for setitem, returns positive indices. Converts negative indices to positive if necessary. @@ -220,7 +220,7 @@ def cleanup_key(self, key: int | np.ndarray[int, bool] | slice | tuple[slice, .. if isinstance(key, int): if abs(key) > upper_bound: # absolute value in case negative index raise IndexError(f"key value: {key} out of range for dimension with size: {upper_bound}") - return [key] + return key elif isinstance(key, np.ndarray): if key.ndim > 1: @@ -246,16 +246,22 @@ def cleanup_key(self, key: int | np.ndarray[int, bool] | slice | tuple[slice, .. raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing graphic features") elif isinstance(key, tuple): - if isinstance(key[0], slice): - key = key[0] - else: - raise TypeError + # multiple dimension slicing + if not all([isinstance(k, (int, slice, range, np.ndarray)) for k in key]): + raise TypeError(key) + + cleaned_tuple = list() + # cleanup the key for each dim + for k in key: + cleaned_tuple.append(self.cleanup_key(k)) + + return key if not isinstance(key, (slice, range)): raise TypeError("Must pass slice or int object") start = key.start if key.start is not None else 0 - stop = key.stop if key.stop is not None else self.value.shape[0] + stop = key.stop if key.stop is not None else self.value.shape[0] - 1 # absolute value of the step in case it's negative # since we convert start and stop to be positive below it is fine for step to be converted to positive step = abs(key.step) if key.step is not None else 1 @@ -265,7 +271,7 @@ def cleanup_key(self, key: int | np.ndarray[int, bool] | slice | tuple[slice, .. stop %= upper_bound if start > stop: - raise ValueError("start index greater than stop index") + raise ValueError(f"start index: {start} greater than stop index: {stop}") return range(start, stop, step) @@ -288,8 +294,21 @@ def _update_range(self, key): elif isinstance(key, int): offset = key size = 1 + elif isinstance(key, tuple): + key: range | slice = key[0] + upper_bound = self.value.shape[0] + + offset = key.start if key.start is not None else 0 + # size is number of points so do not subtract 1 from upper bound like in cleanup_key for indexing + stop = key.stop if key.stop is not None else upper_bound + + offset %= upper_bound + stop %= upper_bound + 1 + + size = stop - offset + else: - raise TypeError + raise TypeError(key) self.buffer.update_range(offset=offset, size=size) @@ -298,7 +317,7 @@ def __repr__(self): f"{self.value.__repr__()}" -class GraphicProperty: +class GraphicFeatureDescriptor: def __init__(self, name): self.name = name @@ -312,30 +331,3 @@ def __get__(self, instance, owner): def __set__(self, obj, value): feature = self._get_feature(obj) feature[:] = value - - - -class ColorFeature(BufferManager): - """Manage color buffer for positions type objects""" - - def __init__(self, data: str | np.ndarray, n_colors: int, isolated_buffer: bool): - if not isinstance(data, np.ndarray): - # isolated buffer is only useful when data is a numpy array - isolated_buffer = False - - colors = parse_colors(data, n_colors) - - super().__init__(colors, isolated_buffer) - - def __setitem__(self, key, value): - if isinstance(value, BufferManager): - # trying to set feature from another feature instance - value = value.value - - key = self.cleanup_slice(key) - - colors = parse_colors(value, len(key)) - - self.buffer.data[key] = colors - - self._update_range(key.start, key.stop - key.start) diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index 81adeebbd..bb98eb3b3 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -166,192 +166,192 @@ def __repr__(self) -> str: return s -class CmapFeature(ColorFeature): - """ - Indexable colormap feature, mostly wraps colors and just provides a way to set colormaps. - - Same event pick info as :class:`ColorFeature` - """ - - def __init__(self, parent, colors, cmap_name: str, cmap_values: np.ndarray): - # Skip the ColorFeature's __init__ - super(ColorFeature, self).__init__(parent, colors) - - self._cmap_name = cmap_name - self._cmap_values = cmap_values - - def __setitem__(self, key, cmap_name): - key = cleanup_slice(key, self._upper_bound) - if not isinstance(key, (slice, np.ndarray)): - raise TypeError( - "Cannot set cmap on single indices, must pass a slice object, " - "numpy.ndarray or set it on the entire data." - ) - - if isinstance(key, slice): - n_colors = len(range(key.start, key.stop, key.step)) - - else: - # numpy array - n_colors = key.size - - colors = parse_cmap_values( - n_colors=n_colors, cmap_name=cmap_name, cmap_values=self._cmap_values - ) - - self._cmap_name = cmap_name - super().__setitem__(key, colors) - - @property - def name(self) -> str: - return self._cmap_name - - @property - def values(self) -> np.ndarray: - return self._cmap_values - - @values.setter - def values(self, values: np.ndarray): - if not isinstance(values, np.ndarray): - values = np.array(values) - - colors = parse_cmap_values( - n_colors=self().shape[0], cmap_name=self._cmap_name, cmap_values=values - ) - - self._cmap_values = values - - super().__setitem__(slice(None), colors) - - def __repr__(self) -> str: - s = f"CmapFeature for {self._parent}, to get name or values: `.cmap.name`, `.cmap.values`" - return s - - -class ImageCmapFeature(GraphicFeature): - """ - Colormap for :class:`ImageGraphic`. - - .cmap() returns the Texture buffer for the cmap. - - .cmap.name returns the cmap name as a str. - - **event pick info:** - - ================ =================== =============== - key type description - ================ =================== =============== - "index" ``None`` not used - "name" ``str`` colormap name - "world_object" pygfx.WorldObject world object - "vmin" ``float`` minimum value - "vmax" ``float`` maximum value - ================ =================== =============== - - """ - - def __init__(self, parent, cmap: str): - cmap_texture_view = get_cmap_texture(cmap) - super().__init__(parent, cmap_texture_view) - self._name = cmap - - def _set(self, cmap_name: str): - if self._parent.data().ndim > 2: - return - - self._parent.world_object.material.map.data[:] = make_colors(256, cmap_name) - self._parent.world_object.material.map.update_range((0, 0, 0), size=(256, 1, 1)) - self._name = cmap_name - - self._feature_changed(key=None, new_data=self._name) - - @property - def name(self) -> str: - return self._name - - @property - def vmin(self) -> float: - """Minimum contrast limit.""" - return self._parent.world_object.material.clim[0] - - @vmin.setter - def vmin(self, value: float): - """Minimum contrast limit.""" - self._parent.world_object.material.clim = ( - value, - self._parent.world_object.material.clim[1], - ) - self._feature_changed(key=None, new_data=None) - - @property - def vmax(self) -> float: - """Maximum contrast limit.""" - return self._parent.world_object.material.clim[1] - - @vmax.setter - def vmax(self, value: float): - """Maximum contrast limit.""" - self._parent.world_object.material.clim = ( - self._parent.world_object.material.clim[0], - value, - ) - self._feature_changed(key=None, new_data=None) - - def reset_vmin_vmax(self): - """Reset vmin vmax values based on current data""" - self.vmin, self.vmax = quick_min_max(self._parent.data()) - - def _feature_changed(self, key, new_data): - # this is a non-indexable feature so key=None - - pick_info = { - "index": None, - "world_object": self._parent.world_object, - "name": self._name, - "vmin": self.vmin, - "vmax": self.vmax, - } - - event_data = FeatureEvent(type="cmap", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"ImageCmapFeature for {self._parent}. Use `.cmap.name` to get str name of cmap." - return s - - -class HeatmapCmapFeature(ImageCmapFeature): - """ - Colormap for :class:`HeatmapGraphic` - - Same event pick info as :class:`ImageCmapFeature` - """ - - def _set(self, cmap_name: str): - # in heatmap we use one material for all ImageTiles - self._parent._material.map.data[:] = make_colors(256, cmap_name) - self._parent._material.map.update_range((0, 0, 0), size=(256, 1, 1)) - self._name = cmap_name - - self._feature_changed(key=None, new_data=self.name) - - @property - def vmin(self) -> float: - """Minimum contrast limit.""" - return self._parent._material.clim[0] - - @vmin.setter - def vmin(self, value: float): - """Minimum contrast limit.""" - self._parent._material.clim = (value, self._parent._material.clim[1]) - - @property - def vmax(self) -> float: - """Maximum contrast limit.""" - return self._parent._material.clim[1] - - @vmax.setter - def vmax(self, value: float): - """Maximum contrast limit.""" - self._parent._material.clim = (self._parent._material.clim[0], value) +# class CmapFeature(ColorFeature): +# """ +# Indexable colormap feature, mostly wraps colors and just provides a way to set colormaps. +# +# Same event pick info as :class:`ColorFeature` +# """ +# +# def __init__(self, parent, colors, cmap_name: str, cmap_values: np.ndarray): +# # Skip the ColorFeature's __init__ +# super(ColorFeature, self).__init__(parent, colors) +# +# self._cmap_name = cmap_name +# self._cmap_values = cmap_values +# +# def __setitem__(self, key, cmap_name): +# key = cleanup_slice(key, self._upper_bound) +# if not isinstance(key, (slice, np.ndarray)): +# raise TypeError( +# "Cannot set cmap on single indices, must pass a slice object, " +# "numpy.ndarray or set it on the entire data." +# ) +# +# if isinstance(key, slice): +# n_colors = len(range(key.start, key.stop, key.step)) +# +# else: +# # numpy array +# n_colors = key.size +# +# colors = parse_cmap_values( +# n_colors=n_colors, cmap_name=cmap_name, cmap_values=self._cmap_values +# ) +# +# self._cmap_name = cmap_name +# super().__setitem__(key, colors) +# +# @property +# def name(self) -> str: +# return self._cmap_name +# +# @property +# def values(self) -> np.ndarray: +# return self._cmap_values +# +# @values.setter +# def values(self, values: np.ndarray): +# if not isinstance(values, np.ndarray): +# values = np.array(values) +# +# colors = parse_cmap_values( +# n_colors=self().shape[0], cmap_name=self._cmap_name, cmap_values=values +# ) +# +# self._cmap_values = values +# +# super().__setitem__(slice(None), colors) +# +# def __repr__(self) -> str: +# s = f"CmapFeature for {self._parent}, to get name or values: `.cmap.name`, `.cmap.values`" +# return s +# +# +# class ImageCmapFeature(GraphicFeature): +# """ +# Colormap for :class:`ImageGraphic`. +# +# .cmap() returns the Texture buffer for the cmap. +# +# .cmap.name returns the cmap name as a str. +# +# **event pick info:** +# +# ================ =================== =============== +# key type description +# ================ =================== =============== +# "index" ``None`` not used +# "name" ``str`` colormap name +# "world_object" pygfx.WorldObject world object +# "vmin" ``float`` minimum value +# "vmax" ``float`` maximum value +# ================ =================== =============== +# +# """ +# +# def __init__(self, parent, cmap: str): +# cmap_texture_view = get_cmap_texture(cmap) +# super().__init__(parent, cmap_texture_view) +# self._name = cmap +# +# def _set(self, cmap_name: str): +# if self._parent.data().ndim > 2: +# return +# +# self._parent.world_object.material.map.data[:] = make_colors(256, cmap_name) +# self._parent.world_object.material.map.update_range((0, 0, 0), size=(256, 1, 1)) +# self._name = cmap_name +# +# self._feature_changed(key=None, new_data=self._name) +# +# @property +# def name(self) -> str: +# return self._name +# +# @property +# def vmin(self) -> float: +# """Minimum contrast limit.""" +# return self._parent.world_object.material.clim[0] +# +# @vmin.setter +# def vmin(self, value: float): +# """Minimum contrast limit.""" +# self._parent.world_object.material.clim = ( +# value, +# self._parent.world_object.material.clim[1], +# ) +# self._feature_changed(key=None, new_data=None) +# +# @property +# def vmax(self) -> float: +# """Maximum contrast limit.""" +# return self._parent.world_object.material.clim[1] +# +# @vmax.setter +# def vmax(self, value: float): +# """Maximum contrast limit.""" +# self._parent.world_object.material.clim = ( +# self._parent.world_object.material.clim[0], +# value, +# ) +# self._feature_changed(key=None, new_data=None) +# +# def reset_vmin_vmax(self): +# """Reset vmin vmax values based on current data""" +# self.vmin, self.vmax = quick_min_max(self._parent.data()) +# +# def _feature_changed(self, key, new_data): +# # this is a non-indexable feature so key=None +# +# pick_info = { +# "index": None, +# "world_object": self._parent.world_object, +# "name": self._name, +# "vmin": self.vmin, +# "vmax": self.vmax, +# } +# +# event_data = FeatureEvent(type="cmap", pick_info=pick_info) +# +# self._call_event_handlers(event_data) +# +# def __repr__(self) -> str: +# s = f"ImageCmapFeature for {self._parent}. Use `.cmap.name` to get str name of cmap." +# return s +# +# +# class HeatmapCmapFeature(ImageCmapFeature): +# """ +# Colormap for :class:`HeatmapGraphic` +# +# Same event pick info as :class:`ImageCmapFeature` +# """ +# +# def _set(self, cmap_name: str): +# # in heatmap we use one material for all ImageTiles +# self._parent._material.map.data[:] = make_colors(256, cmap_name) +# self._parent._material.map.update_range((0, 0, 0), size=(256, 1, 1)) +# self._name = cmap_name +# +# self._feature_changed(key=None, new_data=self.name) +# +# @property +# def vmin(self) -> float: +# """Minimum contrast limit.""" +# return self._parent._material.clim[0] +# +# @vmin.setter +# def vmin(self, value: float): +# """Minimum contrast limit.""" +# self._parent._material.clim = (value, self._parent._material.clim[1]) +# +# @property +# def vmax(self) -> float: +# """Maximum contrast limit.""" +# return self._parent._material.clim[1] +# +# @vmax.setter +# def vmax(self, value: float): +# """Maximum contrast limit.""" +# self._parent._material.clim = (self._parent._material.clim[0], value) diff --git a/fastplotlib/graphics/_features/_data.py b/fastplotlib/graphics/_features/_data.py index 680218014..b912b2bb1 100644 --- a/fastplotlib/graphics/_features/_data.py +++ b/fastplotlib/graphics/_features/_data.py @@ -18,32 +18,27 @@ class PointsDataFeature(BufferManager): """ def __init__(self, data: Any, isolated_buffer: bool = True): + data = self._fix_data(data) super().__init__(data, isolated_buffer=isolated_buffer) - def _fix_data(self, data, parent): - graphic_type = parent.__class__.__name__ - - data = to_gpu_supported_dtype(data) + def _fix_data(self, data): + # data = to_gpu_supported_dtype(data) if data.ndim == 1: - # for scatter if we receive just 3 points in a 1d array, treat it as just a single datapoint - # this is different from fix_data for LineGraphic since there we assume that a 1d array - # is just y-values - if graphic_type == "ScatterGraphic": - data = np.array([data]) - elif graphic_type == "LineGraphic": - data = np.dstack([np.arange(data.size, dtype=data.dtype), data])[0] + # if user provides a 1D array, assume these are y-values + data = np.column_stack([np.arange(data.size, dtype=data.dtype), data]) if data.shape[1] != 3: if data.shape[1] != 2: - raise ValueError(f"Must pass 1D, 2D or 3D data to {graphic_type}") + raise ValueError(f"Must pass 1D, 2D or 3D data") # zeros for z zs = np.zeros(data.shape[0], dtype=data.dtype) - data = np.dstack([data[:, 0], data[:, 1], zs])[0] + # column stack [x, y, z] to make data of shape [n_points, 3] + data = np.column_stack([data[:, 0], data[:, 1], zs]) - return data + return to_gpu_supported_dtype(data) def __setitem__(self, key, value): key = self.cleanup_key(key) @@ -83,122 +78,122 @@ def _feature_changed(self, key, new_data): self._call_event_handlers(event_data) - def __repr__(self) -> str: - s = f"PointsDataFeature for {self._parent}, call `.data()` to get values" - return s - - -class ImageDataFeature(GraphicFeatureIndexable): - """ - Access to the Texture buffer shown in an ImageGraphic. - """ - - def __init__(self, parent, data: Any): - if data.ndim not in (2, 3): - raise ValueError( - "`data.ndim` must be 2 or 3, ImageGraphic data shape must be " - "``[x_dim, y_dim]`` or ``[x_dim, y_dim, rgb]``" - ) - - super().__init__(parent, data) - - @property - def buffer(self) -> pygfx.Texture: - """Texture buffer for the image data""" - return self._parent.world_object.geometry.grid - - def update_gpu(self): - """Update the GPU with the buffer""" - self._update_range(None) - - def __call__(self, *args, **kwargs): - return self.buffer.data - - def __getitem__(self, item): - return self.buffer.data[item] - - def __setitem__(self, key, value): - # make sure float32 - value = to_gpu_supported_dtype(value) - - self.buffer.data[key] = value - self._update_range(key) - - # avoid creating dicts constantly if there are no events to handle - if len(self._event_handlers) > 0: - self._feature_changed(key, value) - - def _update_range(self, key): - self.buffer.update_range((0, 0, 0), size=self.buffer.size) - - def _feature_changed(self, key, new_data): - if key is not None: - key = cleanup_slice(key, self._upper_bound) - if isinstance(key, int): - indices = [key] - elif isinstance(key, slice): - indices = range(key.start, key.stop, key.step) - elif key is None: - indices = None - - pick_info = { - "index": indices, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="data", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"ImageDataFeature for {self._parent}, call `.data()` to get values" - return s - - -class HeatmapDataFeature(ImageDataFeature): - @property - def buffer(self) -> List[pygfx.Texture]: - """list of Texture buffer for the image data""" - return [img.geometry.grid for img in self._parent.world_object.children] - - def __getitem__(self, item): - return self._data[item] - - def __call__(self, *args, **kwargs): - return self._data - - def __setitem__(self, key, value): - # make sure supported type, not float64 etc. - value = to_gpu_supported_dtype(value) - - self._data[key] = value - self._update_range(key) - - # avoid creating dicts constantly if there are no events to handle - if len(self._event_handlers) > 0: - self._feature_changed(key, value) - - def _update_range(self, key): - for buffer in self.buffer: - buffer.update_range((0, 0, 0), size=buffer.size) - - def _feature_changed(self, key, new_data): - if key is not None: - key = cleanup_slice(key, self._upper_bound) - if isinstance(key, int): - indices = [key] - elif isinstance(key, slice): - indices = range(key.start, key.stop, key.step) - elif key is None: - indices = None - - pick_info = { - "index": indices, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="data", pick_info=pick_info) - - self._call_event_handlers(event_data) + # def __repr__(self) -> str: + # s = f"PointsDataFeature for {self._parent}, call `.data()` to get values" + # return s + +# +# class ImageDataFeature(GraphicFeatureIndexable): +# """ +# Access to the Texture buffer shown in an ImageGraphic. +# """ +# +# def __init__(self, parent, data: Any): +# if data.ndim not in (2, 3): +# raise ValueError( +# "`data.ndim` must be 2 or 3, ImageGraphic data shape must be " +# "``[x_dim, y_dim]`` or ``[x_dim, y_dim, rgb]``" +# ) +# +# super().__init__(parent, data) +# +# @property +# def buffer(self) -> pygfx.Texture: +# """Texture buffer for the image data""" +# return self._parent.world_object.geometry.grid +# +# def update_gpu(self): +# """Update the GPU with the buffer""" +# self._update_range(None) +# +# def __call__(self, *args, **kwargs): +# return self.buffer.data +# +# def __getitem__(self, item): +# return self.buffer.data[item] +# +# def __setitem__(self, key, value): +# # make sure float32 +# value = to_gpu_supported_dtype(value) +# +# self.buffer.data[key] = value +# self._update_range(key) +# +# # avoid creating dicts constantly if there are no events to handle +# if len(self._event_handlers) > 0: +# self._feature_changed(key, value) +# +# def _update_range(self, key): +# self.buffer.update_range((0, 0, 0), size=self.buffer.size) +# +# def _feature_changed(self, key, new_data): +# if key is not None: +# key = cleanup_slice(key, self._upper_bound) +# if isinstance(key, int): +# indices = [key] +# elif isinstance(key, slice): +# indices = range(key.start, key.stop, key.step) +# elif key is None: +# indices = None +# +# pick_info = { +# "index": indices, +# "world_object": self._parent.world_object, +# "new_data": new_data, +# } +# +# event_data = FeatureEvent(type="data", pick_info=pick_info) +# +# self._call_event_handlers(event_data) +# +# def __repr__(self) -> str: +# s = f"ImageDataFeature for {self._parent}, call `.data()` to get values" +# return s +# +# +# class HeatmapDataFeature(ImageDataFeature): +# @property +# def buffer(self) -> List[pygfx.Texture]: +# """list of Texture buffer for the image data""" +# return [img.geometry.grid for img in self._parent.world_object.children] +# +# def __getitem__(self, item): +# return self._data[item] +# +# def __call__(self, *args, **kwargs): +# return self._data +# +# def __setitem__(self, key, value): +# # make sure supported type, not float64 etc. +# value = to_gpu_supported_dtype(value) +# +# self._data[key] = value +# self._update_range(key) +# +# # avoid creating dicts constantly if there are no events to handle +# if len(self._event_handlers) > 0: +# self._feature_changed(key, value) +# +# def _update_range(self, key): +# for buffer in self.buffer: +# buffer.update_range((0, 0, 0), size=buffer.size) +# +# def _feature_changed(self, key, new_data): +# if key is not None: +# key = cleanup_slice(key, self._upper_bound) +# if isinstance(key, int): +# indices = [key] +# elif isinstance(key, slice): +# indices = range(key.start, key.stop, key.step) +# elif key is None: +# indices = None +# +# pick_info = { +# "index": indices, +# "world_object": self._parent.world_object, +# "new_data": new_data, +# } +# +# event_data = FeatureEvent(type="data", pick_info=pick_info) +# +# self._call_event_handlers(event_data) diff --git a/fastplotlib/graphics/_features/utils.py b/fastplotlib/graphics/_features/utils.py index 1b1bb56d9..316014881 100644 --- a/fastplotlib/graphics/_features/utils.py +++ b/fastplotlib/graphics/_features/utils.py @@ -2,6 +2,7 @@ import numpy as np from typing import Iterable +from ._base import to_gpu_supported_dtype from ...utils import make_pygfx_colors @@ -85,4 +86,4 @@ def parse_colors( else: raise TypeError("if alpha is provided it must be of type `float`") - return data + return to_gpu_supported_dtype(data) From 9be017cbbe82dfd97d0df98be26e5db15a9f1de1 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 17 May 2024 23:33:40 -0400 Subject: [PATCH 006/196] comitting stuff --- fastplotlib/graphics/_base.py | 13 ++++++++----- fastplotlib/graphics/line.py | 30 ++++++++++++++---------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 3a5b043f5..be1d932cd 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -9,7 +9,7 @@ from pygfx import WorldObject -from ._features import GraphicFeature, PresentFeature, GraphicFeatureIndexable, Deleted +from ._features import GraphicFeature, PresentFeature, BufferManager, GraphicFeatureDescriptor, Deleted HexStr: TypeAlias = str @@ -49,12 +49,15 @@ def __init_subclass__(cls, **kwargs): class Graphic(BaseGraphic): - feature_events = {} + features = {} def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) # all graphics give off a feature event when deleted - cls.feature_events = {*cls.feature_events, "deleted"} + cls.features = {*cls.features}#, "deleted"} + + for f in cls.features: + setattr(cls, f, GraphicFeatureDescriptor(f)) def __init__( self, @@ -80,12 +83,12 @@ def __init__( self.metadata = metadata self.collection_index = collection_index self.registered_callbacks = dict() - self.present = PresentFeature(parent=self) + # self.present = PresentFeature(parent=self) # store hex id str of Graphic instance mem location self._fpl_address: HexStr = hex(id(self)) - self.deleted = Deleted(self, False) + # self.deleted = Deleted(self, False) self._plot_area = None diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 0371fe59b..b4589f413 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -7,12 +7,12 @@ from ..utils import parse_cmap_values from ._base import Graphic, Interaction, PreviouslyModifiedData -from ._features import PointsDataFeature, ColorFeature, CmapFeature, ThicknessFeature +from ._features import GraphicFeatureDescriptor, PointsDataFeature, ColorFeature#, CmapFeature, ThicknessFeature from .selectors import LinearRegionSelector, LinearSelector class LineGraphic(Graphic, Interaction): - feature_events = {"data", "colors", "cmap", "thickness", "present"} + features = {"data", "colors"}#, "cmap", "thickness", "present"} def __init__( self, @@ -24,6 +24,7 @@ def __init__( cmap_values: np.ndarray | Iterable = None, z_position: float = None, collection_index: int = None, + isolated_buffer: bool = True, *args, **kwargs, ): @@ -64,6 +65,7 @@ def __init__( Features -------- + **data**: :class:`.ImageDataFeature` Manages the line [x, y, z] positions data buffer, allows regular and fancy indexing. @@ -81,26 +83,24 @@ def __init__( """ - self.data = PointsDataFeature(self, data, collection_index=collection_index) + self._data = PointsDataFeature(data, isolated_buffer=isolated_buffer) if cmap is not None: - n_datapoints = self.data().shape[0] + n_datapoints = self._data.value.shape[0] colors = parse_cmap_values( n_colors=n_datapoints, cmap_name=cmap, cmap_values=cmap_values ) - self.colors = ColorFeature( - self, + self._colors = ColorFeature( colors, - n_colors=self.data().shape[0], + n_colors=self._data.value.shape[0], alpha=alpha, - collection_index=collection_index, ) - self.cmap = CmapFeature( - self, self.colors(), cmap_name=cmap, cmap_values=cmap_values - ) + # self.cmap = CmapFeature( + # self, self.colors(), cmap_name=cmap, cmap_values=cmap_values + # ) super().__init__(*args, **kwargs) @@ -109,14 +109,12 @@ def __init__( else: material = pygfx.LineMaterial - self.thickness = ThicknessFeature(self, thickness) + # self.thickness = ThicknessFeature(self, thickness) world_object: pygfx.Line = pygfx.Line( # self.data.feature_data because data is a Buffer - geometry=pygfx.Geometry(positions=self.data(), colors=self.colors()), - material=material( - thickness=self.thickness(), color_mode="vertex", pick_write=True - ), + geometry=pygfx.Geometry(positions=self._data.buffer, colors=self._colors.buffer), + material=material(thickness=thickness, color_mode="vertex"), ) self._set_world_object(world_object) From 0491f8ca8e3bcbf54152f05a76f3f6fa195c5026 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 18 May 2024 00:50:20 -0400 Subject: [PATCH 007/196] _update_range() is pretty good now --- fastplotlib/graphics/_features/_base.py | 55 ++++++++++++++++++------- fastplotlib/graphics/_features/_data.py | 12 ++---- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 0a9e29e95..8bebadba8 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -231,7 +231,7 @@ def cleanup_key(self, key: int | np.ndarray[int, bool] | slice | tuple[slice, .. key = np.nonzero(key)[0] if key.size < 1: - return None + return np.array([], dtype=np.int64) # make sure indices within bounds of feature buffer range if key[-1] > upper_bound: @@ -281,32 +281,55 @@ def __getitem__(self, item): def __setitem__(self, key, value): raise NotImplementedError - def _update_range(self, key): - # assumes key is already cleaned up - if isinstance(key, range): - offset = key.start - size = key.stop - key.start + def _update_range(self, key: int | slice | range | np.ndarray[int | bool] | tuple[slice, ...] | tuple[range, ...]): + """ + Uses key from slicing to determine the offset and + size of the buffer to mark for upload to the GPU + """ + upper_bound = self.value.shape[0] - elif isinstance(key, np.ndarray): - offset = key.min() - size = key.max() - offset + if isinstance(key, tuple): + # if multiple dims are sliced, we only need the key for + # the first dimension corresponding to n_datapoints + key: int | np.ndarray[int | bool] | range | slice = key[0] - elif isinstance(key, int): + if isinstance(key, int): + # simplest case offset = key size = 1 - elif isinstance(key, tuple): - key: range | slice = key[0] - upper_bound = self.value.shape[0] + elif isinstance(key, (slice, range)): + # first dimension, corresponding to n_datapoints, sliced offset = key.start if key.start is not None else 0 - # size is number of points so do not subtract 1 from upper bound like in cleanup_key for indexing + + # size is number of points so do not subtract 1 from upper bound since this is not for indexing stop = key.stop if key.stop is not None else upper_bound - offset %= upper_bound - stop %= upper_bound + 1 + # add 1 to upper bound since we want size not index + offset %= (upper_bound + 1) + stop %= (upper_bound + 1) size = stop - offset + elif isinstance(key, np.ndarray): + if key.dtype == bool: + # convert bool mask to integer indices + key = np.nonzero(key)[0] + + if key.size < 1: + # nothing to update + return + + if not np.issubdtype(key.dtype, np.integer): + # fancy indexing doesn't make sense with non-integer types + raise TypeError(key) + + # convert any negative integer indices to positive indices + key %= (upper_bound + 1) + + offset = key.min() + size = key.max() - offset + 1 + else: raise TypeError(key) diff --git a/fastplotlib/graphics/_features/_data.py b/fastplotlib/graphics/_features/_data.py index b912b2bb1..2143f06d3 100644 --- a/fastplotlib/graphics/_features/_data.py +++ b/fastplotlib/graphics/_features/_data.py @@ -41,16 +41,12 @@ def _fix_data(self, data): return to_gpu_supported_dtype(data) def __setitem__(self, key, value): - key = self.cleanup_key(key) - - # put data into right shape if they're only indexing datapoints - if isinstance(key, (slice, int, np.ndarray, np.integer)): - value = self._fix_data(value, self._parent) - # otherwise assume that they have the right shape - # numpy will throw errors if it can't broadcast - + # directly use the key to slice the buffer self.buffer.data[key] = value + # _update_range handles parsing the key to + # determine offset and size for GPU upload self._update_range(key) + # avoid creating dicts constantly if there are no events to handle # if len(self._event_handlers) > 0: # self._feature_changed(key, value) From 97b73038f0e6afc096cb4f807eb548f0aad1b6fa Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 18 May 2024 00:51:20 -0400 Subject: [PATCH 008/196] comment --- fastplotlib/graphics/_base.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index be1d932cd..aaa4f6f73 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -56,6 +56,7 @@ def __init_subclass__(cls, **kwargs): # all graphics give off a feature event when deleted cls.features = {*cls.features}#, "deleted"} + # graphic feature class attributes for f in cls.features: setattr(cls, f, GraphicFeatureDescriptor(f)) @@ -181,15 +182,6 @@ def children(self) -> list[WorldObject]: def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area - def __setattr__(self, key, value): - if hasattr(self, key): - attr = getattr(self, key) - if isinstance(attr, GraphicFeature): - attr._set(value) - return - - super().__setattr__(key, value) - def __repr__(self): rval = f"{self.__class__.__name__} @ {hex(id(self))}" if self.name is not None: From 4585be2562b3033b592c98d420aea5e7ba7c91a2 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 18 May 2024 00:51:32 -0400 Subject: [PATCH 009/196] if statement --- fastplotlib/graphics/_features/_colors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index bb98eb3b3..293974ec4 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -65,7 +65,7 @@ def __setitem__(self, key, value): if isinstance(key, (int, np.ndarray, tuple, slice, range)): key = self.cleanup_key(key) - elif isinstance(key, tuple): + if isinstance(key, tuple): # directly setting RGBA values on every datapoint if not isinstance(value, (float, int, np.ndarray)): raise ValueError( From 261a5a9f8c10a247d33512c2b27d8b66deb68df3 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 00:27:34 -0400 Subject: [PATCH 010/196] simply colors setting --- fastplotlib/graphics/_features/_colors.py | 95 +++++++++-------------- 1 file changed, 35 insertions(+), 60 deletions(-) diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index 293974ec4..01dabc027 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -61,80 +61,55 @@ def __init__( super().__init__(data=data, isolated_buffer=isolated_buffer) - def __setitem__(self, key, value): - if isinstance(key, (int, np.ndarray, tuple, slice, range)): - key = self.cleanup_key(key) + def __setitem__( + self, + key: int | slice | range | np.ndarray[int | bool] | tuple[slice, ...] | tuple[range, ...], + value: str | np.ndarray | tuple[float, float, float, float] | list[str] + ): + # if key is tuple assume they want to edit [n_points, RGBA] directly + # if key is slice | range | int | np.ndarray, they are slicing only n_points, get n_points and parse colors if isinstance(key, tuple): - # directly setting RGBA values on every datapoint - if not isinstance(value, (float, int, np.ndarray)): - raise ValueError( - "If using multiple-fancy indexing for color, you can only set numerical" - "values since this sets the RGBA array data directly." - ) - - if len(key) != 2: - raise ValueError( - "fancy indexing for colors must be 2-dimension, i.e. [n_datapoints, RGBA]" + # directly setting RGBA values, we do no parsing + if not isinstance(value, (int, float, np.ndarray)): + raise TypeError( + "Can only set from int, float, or array to set colors directly by slicing the entire array" ) - # set the user passed data directly - self.buffer.data[key] = value - - # update range - # first slice obj is going to be the datapoints to modify so use key[0] - # key[1] is going to be RGBA so get rid of it to pass to _update_range - key = self.cleanup_key(key[0]) - self._update_range(key) - - self._feature_changed(key, value) - return + elif isinstance(key, int): + # set color of one point + n_colors = 1 + value = parse_colors(value, n_colors) - else: - raise TypeError( - "Graphic features only support integer and numerical fancy indexing" - ) - - new_data_size = len(key) + elif isinstance(key, (slice, range)): + # find n_colors by converting slice to range and then parse colors + key = range(key.start, key.stop, key.step) + n_colors = len(key) + value = parse_colors(value, n_colors) - if not isinstance(value, np.ndarray): - color = np.array(pygfx.Color(value)) # pygfx color parser - # make it of shape [n_colors_modify, 4] - new_colors = np.repeat( - np.array([color]).astype(np.float32), new_data_size, axis=0 - ) + elif isinstance(key, np.ndarray): + # make sure it's 1D + if not key.ndim == 1: + raise TypeError("If slicing colors with an array, it must be a 1D array") - # if already a numpy array - elif isinstance(value, np.ndarray): - # if a single color provided as numpy array - if value.shape == (4,): - new_colors = value.astype(np.float32) - # if there are more than 1 datapoint color to modify - if new_data_size > 1: - new_colors = np.repeat( - np.array([new_colors]).astype(np.float32), new_data_size, axis=0 - ) + if key.dtype == bool: + # make sure len is same + if not key.size == self.buffer.data.shape[0]: + raise IndexError + n_colors = np.count_nonzero(key) - elif value.ndim == 2: - if value.shape[1] != 4 and value.shape[0] != new_data_size: - raise ValueError( - "numpy array passed to color must be of shape (4,) or (n_colors_modify, 4)" - ) - # if there is a single datapoint to change color of but user has provided shape [1, 4] - if new_data_size == 1: - new_colors = value.ravel().astype(np.float32) - else: - new_colors = value.astype(np.float32) + elif np.issubdtype(key.dtype, np.integer): + n_colors = key.size else: - raise ValueError( - "numpy array passed to color must be of shape (4,) or (n_colors_modify, 4)" - ) + raise TypeError + + value = parse_colors(value, n_colors) else: raise TypeError - self.buffer.data[key] = new_colors + self.buffer.data[key] = value self._update_range(key) # self._feature_changed(key, new_colors) From 5d40af863f242026dbfa8ca22dc0c0a17ed13b26 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 00:27:53 -0400 Subject: [PATCH 011/196] type annotation --- fastplotlib/graphics/_features/_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/_features/_data.py b/fastplotlib/graphics/_features/_data.py index 2143f06d3..2be6170ac 100644 --- a/fastplotlib/graphics/_features/_data.py +++ b/fastplotlib/graphics/_features/_data.py @@ -40,7 +40,7 @@ def _fix_data(self, data): return to_gpu_supported_dtype(data) - def __setitem__(self, key, value): + def __setitem__(self, key: int | slice | range | np.ndarray[int | bool] | tuple[slice, ...] | tuple[range, ...], value): # directly use the key to slice the buffer self.buffer.data[key] = value # _update_range handles parsing the key to From 697d50e61885a87a73e925e76e342f941b9ca23b Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 02:00:04 -0400 Subject: [PATCH 012/196] simpler slice parsing --- fastplotlib/graphics/_features/_base.py | 34 +++++++++++++---------- fastplotlib/graphics/_features/_colors.py | 14 ++++------ 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 8bebadba8..4cd51d0cc 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -281,37 +281,38 @@ def __getitem__(self, item): def __setitem__(self, key, value): raise NotImplementedError - def _update_range(self, key: int | slice | range | np.ndarray[int | bool] | tuple[slice, ...] | tuple[range, ...]): + def _update_range(self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...]): """ Uses key from slicing to determine the offset and size of the buffer to mark for upload to the GPU """ + # number of elements in the buffer upper_bound = self.value.shape[0] if isinstance(key, tuple): # if multiple dims are sliced, we only need the key for # the first dimension corresponding to n_datapoints - key: int | np.ndarray[int | bool] | range | slice = key[0] + key: int | np.ndarray[int | bool] | slice = key[0] if isinstance(key, int): # simplest case offset = key size = 1 - elif isinstance(key, (slice, range)): - # first dimension, corresponding to n_datapoints, sliced - offset = key.start if key.start is not None else 0 + elif isinstance(key, slice): + # parse slice to get offset + offset, stop, step = key.indices(upper_bound) - # size is number of points so do not subtract 1 from upper bound since this is not for indexing - stop = key.stop if key.stop is not None else upper_bound + # make range from slice to get size + size = len(range(offset, stop, step)) - # add 1 to upper bound since we want size not index - offset %= (upper_bound + 1) - stop %= (upper_bound + 1) + elif isinstance(key, (np.ndarray, list)): + if isinstance(key, list): + # convert to 1D array + key = np.array(key) + if not key.ndim == 1: + raise TypeError(key) - size = stop - offset - - elif isinstance(key, np.ndarray): if key.dtype == bool: # convert bool mask to integer indices key = np.nonzero(key)[0] @@ -325,10 +326,13 @@ def _update_range(self, key: int | slice | range | np.ndarray[int | bool] | tupl raise TypeError(key) # convert any negative integer indices to positive indices - key %= (upper_bound + 1) + key %= upper_bound + # index of first element to upload offset = key.min() - size = key.max() - offset + 1 + + # number of elements to upload, max - min + 1 + size = np.ptp(key) + 1 else: raise TypeError(key) diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index 01dabc027..cace1d436 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -63,7 +63,7 @@ def __init__( def __setitem__( self, - key: int | slice | range | np.ndarray[int | bool] | tuple[slice, ...] | tuple[range, ...], + key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], value: str | np.ndarray | tuple[float, float, float, float] | list[str] ): # if key is tuple assume they want to edit [n_points, RGBA] directly @@ -81,10 +81,12 @@ def __setitem__( n_colors = 1 value = parse_colors(value, n_colors) - elif isinstance(key, (slice, range)): + elif isinstance(key, slice): # find n_colors by converting slice to range and then parse colors - key = range(key.start, key.stop, key.step) - n_colors = len(key) + start, stop, step = key.indices(self.value.shape[0]) + + n_colors = len(range(start, stop, step)) + value = parse_colors(value, n_colors) elif isinstance(key, np.ndarray): @@ -136,10 +138,6 @@ def _feature_changed(self, key, new_data): self._call_event_handlers(event_data) - def __repr__(self) -> str: - s = f"ColorsFeature for {self._parent}. Call `.colors()` to get values." - return s - # class CmapFeature(ColorFeature): # """ From 08fd17dc81d28b01400501996fe39279e05b22ba Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 02:06:47 -0400 Subject: [PATCH 013/196] remove cleanup_key :D :D :D git status! --- fastplotlib/graphics/_features/_base.py | 83 +++---------------------- 1 file changed, 9 insertions(+), 74 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 4cd51d0cc..6e147d342 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -209,79 +209,13 @@ def value(self) -> NDArray: def buffer(self) -> pygfx.Buffer | pygfx.Texture: return self._buffer - def cleanup_key(self, key: int | np.ndarray[int, bool] | slice | tuple[slice, ...]) -> int | np.ndarray | range | tuple[range, ...]: - """ - Cleanup slice indices for setitem, returns positive indices. Converts negative indices to positive if necessary. - - Returns a cleaned up key corresponding to only the first dimension. - """ - upper_bound = self.value.shape[0] - - if isinstance(key, int): - if abs(key) > upper_bound: # absolute value in case negative index - raise IndexError(f"key value: {key} out of range for dimension with size: {upper_bound}") - return key - - elif isinstance(key, np.ndarray): - if key.ndim > 1: - raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing") - - # if boolean array convert to integer array of indices - if key.dtype == bool: - key = np.nonzero(key)[0] - - if key.size < 1: - return np.array([], dtype=np.int64) - - # make sure indices within bounds of feature buffer range - if key[-1] > upper_bound: - raise IndexError( - f"Index: `{key[-1]}` out of bounds for feature array of size: `{upper_bound}`" - ) - - # make sure indices are integers - if np.issubdtype(key.dtype, np.integer): - return key - - raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing graphic features") - - elif isinstance(key, tuple): - # multiple dimension slicing - if not all([isinstance(k, (int, slice, range, np.ndarray)) for k in key]): - raise TypeError(key) - - cleaned_tuple = list() - # cleanup the key for each dim - for k in key: - cleaned_tuple.append(self.cleanup_key(k)) - - return key - - if not isinstance(key, (slice, range)): - raise TypeError("Must pass slice or int object") - - start = key.start if key.start is not None else 0 - stop = key.stop if key.stop is not None else self.value.shape[0] - 1 - # absolute value of the step in case it's negative - # since we convert start and stop to be positive below it is fine for step to be converted to positive - step = abs(key.step) if key.step is not None else 1 - - # modulus in case of negative indices - start %= upper_bound - stop %= upper_bound - - if start > stop: - raise ValueError(f"start index: {start} greater than stop index: {stop}") - - return range(start, stop, step) - def __getitem__(self, item): return self.buffer.data[item] def __setitem__(self, key, value): raise NotImplementedError - def _update_range(self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...]): + def _update_range(self, key: int | slice | np.ndarray[int | bool] | list[bool | int] | tuple[slice, ...]): """ Uses key from slicing to determine the offset and size of the buffer to mark for upload to the GPU @@ -308,23 +242,24 @@ def _update_range(self, key: int | slice | np.ndarray[int | bool] | tuple[slice, elif isinstance(key, (np.ndarray, list)): if isinstance(key, list): - # convert to 1D array + # convert to array key = np.array(key) - if not key.ndim == 1: - raise TypeError(key) + + if not key.ndim == 1: + raise TypeError(key) if key.dtype == bool: # convert bool mask to integer indices key = np.nonzero(key)[0] - if key.size < 1: - # nothing to update - return - if not np.issubdtype(key.dtype, np.integer): # fancy indexing doesn't make sense with non-integer types raise TypeError(key) + if key.size < 1: + # nothing to update + return + # convert any negative integer indices to positive indices key %= upper_bound From a45651db915ead70e232e241bb96dfbf5c538282 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 02:33:15 -0400 Subject: [PATCH 014/196] all fancy and negative indexing working :D --- fastplotlib/graphics/_features/_base.py | 24 ++++++++++++++++++----- fastplotlib/graphics/_features/_colors.py | 6 +++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 6e147d342..c138b3c8e 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -234,11 +234,23 @@ def _update_range(self, key: int | slice | np.ndarray[int | bool] | list[bool | size = 1 elif isinstance(key, slice): - # parse slice to get offset - offset, stop, step = key.indices(upper_bound) + # parse slice + start, stop, step = key.indices(upper_bound) - # make range from slice to get size - size = len(range(offset, stop, step)) + # account for backwards indexing + if (start > stop) and step < 0: + offset = stop + else: + offset = start + + # slice.indices will give -1 if None is passed + # which just means 0 here since buffers do not + # use negative indexing + offset = max(0, offset) + + # number of elements to upload + # this is indexing so do not add 1 + size = abs(stop - start) elif isinstance(key, (np.ndarray, list)): if isinstance(key, list): @@ -266,7 +278,9 @@ def _update_range(self, key: int | slice | np.ndarray[int | bool] | list[bool | # index of first element to upload offset = key.min() - # number of elements to upload, max - min + 1 + # number of elements to upload + # add 1 because this is direct + # passing of indices, not a start:stop size = np.ptp(key) + 1 else: diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index cace1d436..0b6e094ff 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -89,7 +89,11 @@ def __setitem__( value = parse_colors(value, n_colors) - elif isinstance(key, np.ndarray): + elif isinstance(key, (np.ndarray, list)): + if isinstance(key, list): + # convert to array + key = np.array(key) + # make sure it's 1D if not key.ndim == 1: raise TypeError("If slicing colors with an array, it must be a 1D array") From 9612d60684f75097ca562f34a7ef08e40b34b3b6 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 03:07:51 -0400 Subject: [PATCH 015/196] start tests --- tests/test_buffer_manager.py | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_buffer_manager.py diff --git a/tests/test_buffer_manager.py b/tests/test_buffer_manager.py new file mode 100644 index 000000000..09041e5ab --- /dev/null +++ b/tests/test_buffer_manager.py @@ -0,0 +1,57 @@ +import numpy as np +from numpy import testing as npt + +from fastplotlib.graphics._features import ColorFeature, PointsDataFeature +from fastplotlib.graphics._features.utils import parse_colors + + +def make_colors_buffer(): + return ColorFeature(colors="w", n_colors=10) + + +def make_points_buffer(): + pass + + +def test_int(): + # setting single points + colors = make_colors_buffer() + colors[3] = "r" + npt.assert_almost_equal(colors[3], [1., 0., 0., 1.]) + + colors[6] = [0., 1., 1., 1.] + npt.assert_almost_equal(colors[6], [0., 1., 1., 1.]) + + colors[7] = (0., 1., 1., 1.) + npt.assert_almost_equal(colors[6], [0., 1., 1., 1.]) + + colors[8] = np.array([1, 0, 1, 1]) + npt.assert_almost_equal(colors[8], [1., 0., 1., 1.]) + + colors[2] = [1, 0, 1, 0.5] + npt.assert_almost_equal(colors[2], [1., 0., 1., 0.5]) + + +def test_tuple(): + # setting entire array manually + colors = make_colors_buffer() + colors[1, :] = 0.5 + print(colors[1]) + npt.assert_almost_equal(colors[1], [0.5, 0.5, 0.5, 0.5]) + + colors[1, 0] = 1 + npt.assert_almost_equal(colors[1], [1., 0.5, 0.5, 0.5]) + + colors[1, 2:] = 0.7 + npt.assert_almost_equal(colors[1], [1., 0.5, 0.7, 0.7]) + + colors[1, -1] = 0.2 + npt.assert_almost_equal(colors[1], [1., 0.5, 0.7, 0.2]) + + +def test_slice(): + pass + + +def test_array(): + pass From dda83967dba968d6dd1a6bbb1b06f1e68bf5ae98 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 03:08:11 -0400 Subject: [PATCH 016/196] exception message --- fastplotlib/graphics/_features/_colors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index 0b6e094ff..eedb5563e 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -64,7 +64,7 @@ def __init__( def __setitem__( self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], - value: str | np.ndarray | tuple[float, float, float, float] | list[str] + value: str | np.ndarray | tuple[float, float, float, float] | list[str] | list[float] | int | float ): # if key is tuple assume they want to edit [n_points, RGBA] directly # if key is slice | range | int | np.ndarray, they are slicing only n_points, get n_points and parse colors @@ -96,7 +96,7 @@ def __setitem__( # make sure it's 1D if not key.ndim == 1: - raise TypeError("If slicing colors with an array, it must be a 1D array") + raise TypeError("If slicing colors with an array, it must be a 1D bool or int array") if key.dtype == bool: # make sure len is same @@ -108,7 +108,7 @@ def __setitem__( n_colors = key.size else: - raise TypeError + raise TypeError("If slicing colors with an array, it must be a 1D bool or int array") value = parse_colors(value, n_colors) From e22fe7c21b8a1aa805399a190fbd2711d4945694 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 03:30:33 -0400 Subject: [PATCH 017/196] more on tests --- tests/test_buffer_manager.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/test_buffer_manager.py b/tests/test_buffer_manager.py index 09041e5ab..7b111abcc 100644 --- a/tests/test_buffer_manager.py +++ b/tests/test_buffer_manager.py @@ -5,8 +5,9 @@ from fastplotlib.graphics._features.utils import parse_colors -def make_colors_buffer(): - return ColorFeature(colors="w", n_colors=10) +def make_colors_buffer() -> ColorFeature: + colors = ColorFeature(colors="w", n_colors=10) + return colors def make_points_buffer(): @@ -16,8 +17,12 @@ def make_points_buffer(): def test_int(): # setting single points colors = make_colors_buffer() + # TODO: placeholder until I make a testing figure where we draw frames only on call + colors.buffer._gfx_pending_uploads.clear() + colors[3] = "r" npt.assert_almost_equal(colors[3], [1., 0., 0., 1.]) + assert colors.buffer._gfx_pending_uploads[-1] == (3, 1) colors[6] = [0., 1., 1., 1.] npt.assert_almost_equal(colors[6], [0., 1., 1., 1.]) @@ -35,8 +40,8 @@ def test_int(): def test_tuple(): # setting entire array manually colors = make_colors_buffer() + colors[1, :] = 0.5 - print(colors[1]) npt.assert_almost_equal(colors[1], [0.5, 0.5, 0.5, 0.5]) colors[1, 0] = 1 @@ -50,7 +55,12 @@ def test_tuple(): def test_slice(): - pass + # slicing only first dim + colors = make_colors_buffer() + + colors[1:3] = "r" + npt.assert_almost_equal(colors[1:3], [0.5, 0.5, 0.5, 0.5]) + def test_array(): From 9b4082f8bd6a1de80f332446a06339d05ced7e58 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 19:12:25 -0400 Subject: [PATCH 018/196] refactor sizes, not tested yet --- fastplotlib/graphics/_features/_sizes.py | 85 ++++++++---------------- 1 file changed, 26 insertions(+), 59 deletions(-) diff --git a/fastplotlib/graphics/_features/_sizes.py b/fastplotlib/graphics/_features/_sizes.py index 2ceeb7862..49702ec8f 100644 --- a/fastplotlib/graphics/_features/_sizes.py +++ b/fastplotlib/graphics/_features/_sizes.py @@ -1,97 +1,64 @@ -from typing import Any - import numpy as np -import pygfx - from ._base import ( - GraphicFeatureIndexable, - cleanup_slice, + BufferManager, FeatureEvent, to_gpu_supported_dtype, - cleanup_array_slice, ) -class PointsSizesFeature(GraphicFeatureIndexable): +class PointsSizesFeature(BufferManager): """ Access to the vertex buffer data shown in the graphic. Supports fancy indexing if the data array also supports it. """ - def __init__(self, parent, sizes: Any, collection_index: int = None): - sizes = self._fix_sizes(sizes, parent) - super().__init__(parent, sizes, collection_index=collection_index) - - @property - def buffer(self) -> pygfx.Buffer: - return self._parent.world_object.geometry.sizes - - def __getitem__(self, item): - return self.buffer.data[item] - - def _fix_sizes(self, sizes, parent): - graphic_type = parent.__class__.__name__ - - n_datapoints = parent.data().shape[0] - if not isinstance(sizes, (list, tuple, np.ndarray)): + def __init__( + self, + sizes: np.ndarray | list[int | float] | tuple[int | float], + n_datapoints: int, + isolated_buffer: bool = True + ): + sizes = self._fix_sizes(sizes, n_datapoints) + super().__init__(data=sizes, isolated_buffer=isolated_buffer) + + def _fix_sizes(self, sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], n_datapoints: int): + if np.issubdtype(type(sizes), np.integer): + # single value given sizes = np.full( n_datapoints, sizes, dtype=np.float32 ) # force it into a float to avoid weird gpu errors - elif not isinstance( - sizes, np.ndarray + + elif isinstance( + sizes, (np.ndarray, tuple, list) ): # if it's not a ndarray already, make it one - sizes = np.array(sizes, dtype=np.float32) # read it in as a numpy.float32 - if (sizes.ndim != 1) or (sizes.size != parent.data().shape[0]): + sizes = np.asarray(sizes, dtype=np.float32) # read it in as a numpy.float32 + if (sizes.ndim != 1) or (sizes.size != n_datapoints): raise ValueError( f"sequence of `sizes` must be 1 dimensional with " f"the same length as the number of datapoints" ) - sizes = to_gpu_supported_dtype(sizes) + else: + raise TypeError("sizes must be a single , , or a sequence (array, list, tuple) of int" + "or float with the length equal to the number of datapoints") - if any(s < 0 for s in sizes): + if np.count_nonzero(sizes < 0) > 1: raise ValueError( "All sizes must be positive numbers greater than or equal to 0.0." ) - if sizes.ndim == 1: - if graphic_type == "ScatterGraphic": - sizes = np.array(sizes) - else: - raise ValueError( - f"Sizes must be an array of shape (n,) where n == the number of data points provided.\ - Received shape={sizes.shape}." - ) - - return np.array(sizes) + return sizes def __setitem__(self, key, value): - if isinstance(key, np.ndarray): - # make sure 1D array of int or boolean - key = cleanup_array_slice(key, self._upper_bound) - - # put sizes into right shape if they're only indexing datapoints - if isinstance(key, (slice, int, np.ndarray, np.integer)): - value = self._fix_sizes(value, self._parent) - # otherwise assume that they have the right shape - # numpy will throw errors if it can't broadcast - - if value.size != self.buffer.data[key].size: - raise ValueError( - f"{value.size} is not equal to buffer size {self.buffer.data[key].size}.\ - If you want to set size to a non-scalar value, make sure it's the right length!" - ) - + # this is a very simple 1D buffer, no parsing required, directly set buffer self.buffer.data[key] = value self._update_range(key) + # avoid creating dicts constantly if there are no events to handle if len(self._event_handlers) > 0: self._feature_changed(key, value) - def _update_range(self, key): - self._update_range_indices(key) - def _feature_changed(self, key, new_data): if key is not None: key = cleanup_slice(key, self._upper_bound) From 2a1869ceae97c30b35fa53168d43ee3e035b7ced Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 20:05:13 -0400 Subject: [PATCH 019/196] start parameterizing buffer tests --- tests/test_buffer_manager.py | 74 ++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/tests/test_buffer_manager.py b/tests/test_buffer_manager.py index 7b111abcc..e0685235f 100644 --- a/tests/test_buffer_manager.py +++ b/tests/test_buffer_manager.py @@ -1,10 +1,26 @@ import numpy as np from numpy import testing as npt +import pytest + +import pygfx from fastplotlib.graphics._features import ColorFeature, PointsDataFeature from fastplotlib.graphics._features.utils import parse_colors +# TODO: parameterize every test where the color is given in as str, array, tuple, and list + + +def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple]: + color = pygfx.Color(name) + + s = name + a = np.array(color) + l = list(color) + t = tuple(color) + + return [s, a, l, t] + def make_colors_buffer() -> ColorFeature: colors = ColorFeature(colors="w", n_colors=10) return colors @@ -54,13 +70,63 @@ def test_tuple(): npt.assert_almost_equal(colors[1], [1., 0.5, 0.7, 0.2]) -def test_slice(): +@pytest.mark.parametrize("color1", generate_color_inputs("red")) +@pytest.mark.parametrize("color2", generate_color_inputs("green")) +@pytest.mark.parametrize("color3", generate_color_inputs("blue")) +def test_slice(color1, color2, color3): # slicing only first dim colors = make_colors_buffer() - colors[1:3] = "r" - npt.assert_almost_equal(colors[1:3], [0.5, 0.5, 0.5, 0.5]) - + colors[1:3] = color1 + truth = np.repeat([pygfx.Color(color1)], repeats=2, axis=0) + npt.assert_almost_equal(colors[1:3], truth) + + colors[:] = "w" + npt.assert_almost_equal(colors[:], np.repeat([[1., 1., 1., 1.]], 10, axis=0)) + + colors[2:8:2] = color2 # set index 2, 4, 6 to color2 + truth = np.repeat([pygfx.Color(color2)], repeats=3, axis=0) + npt.assert_almost_equal(colors[2:8:2], truth) + # make sure others are not changed + others = [0, 1, 3, 5, 7, 8, 9] + npt.assert_almost_equal(colors[others], np.repeat([[1., 1., 1., 1.]], repeats=7, axis=0)) + + # set the others to color3 + colors[others] = color3 + truth = np.repeat([pygfx.Color(color3)], repeats=len(others), axis=0) + npt.assert_almost_equal(colors[others], truth) + # make sure color2 items are not touched + npt.assert_almost_equal(colors[2:8:2], np.repeat([pygfx.Color(color2)], repeats=3, axis=0)) + + # reset + colors[:] = (1, 1, 1, 1) + + # negative slicing + colors[-5:] = color1 + truth = np.repeat([pygfx.Color(color1)], repeats=5, axis=0) + npt.assert_almost_equal(colors[-5:], truth) + + # set some to color2 + colors[-5:-1:2] = color2 + truth = np.repeat([pygfx.Color(color2)], 2, axis=0) + npt.assert_almost_equal(colors[[5, 7]], truth) + # make sure non-sliced not touched + npt.assert_almost_equal(colors[[6, 8, 9]], np.repeat([pygfx.Color(color1)], 3, axis=0)) + # make sure white non-sliced not touched + npt.assert_almost_equal(colors[:-5], np.repeat([[1., 1., 1., 1.]], 5, axis=0)) + + # negative slicing backwards, set points 5, 3 + colors[-5:1:-2] = color3 + truth = np.repeat([pygfx.Color(color3)], repeats=2, axis=0) + npt.assert_almost_equal(colors[[5, 3]], truth) + # make sure others are not touched + npt.assert_almost_equal(colors[[0, 1, 2]], np.repeat([[1., 1., 1., 1.]], repeats=3, axis=0)) + # this point should be color2 + npt.assert_almost_equal(colors[7], np.array(pygfx.Color(color2))) + # point 4 should be completely untouched + npt.assert_almost_equal(colors[4], [1, 1, 1, 1]) + # rest should be color1 + npt.assert_almost_equal(colors[[6, 8, 9]], np.repeat([pygfx.Color(color1)], 3, axis=0)) def test_array(): From 29a0a76378359b3a13f52b6b11e0c218c825dfff Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 20:35:46 -0400 Subject: [PATCH 020/196] better buffer tests --- tests/test_buffer_manager.py | 126 +++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 50 deletions(-) diff --git a/tests/test_buffer_manager.py b/tests/test_buffer_manager.py index e0685235f..badc570c1 100644 --- a/tests/test_buffer_manager.py +++ b/tests/test_buffer_manager.py @@ -21,6 +21,68 @@ def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple]: return [s, a, l, t] + +def generate_slice_indices(kind: int): + n_elements = 10 + a = np.arange(n_elements) + + match kind: + case 1: + # everything + s = slice(None, None, None) + indices = list(range(10)) + + case 2: + # positive continuous range + s = slice(1, 5, None) + indices = list(range(1, 5)) + + case 3: + # positive stepped range + s = slice(2, 8, 2) + indices = [2, 4, 6] + + case 4: + # negative continuous range + s = slice(-5, None, None) + indices = [5, 6, 7, 8, 9] + + case 5: + # negative backwards + s = slice(-5, None, -1) + indices = [5, 4, 3, 2, 1, 0] + + case 5: + # negative backwards stepped + s = slice(-5, None, -2) + indices = [5, 3, 1] + + case 6: + # negative stepped forward + s = slice(-5, None, 2) + indices = [5, 7, 9] + + case 7: + # both negative + s = slice(-8, -2, None) + indices = [2, 3, 4, 5, 6, 7] + + case 8: + # both negative and stepped + s = slice(-8, -2, 2) + indices = [2, 4, 6] + + case 9: + # positive, negative, negative + s = slice(8, -9, -2) + indices = [8, 6, 4, 2] + + others = [i for i in a if i not in indices] + + return {"slice": s, "indices": indices, "others": others} + + + def make_colors_buffer() -> ColorFeature: colors = ColorFeature(colors="w", n_colors=10) return colors @@ -70,63 +132,27 @@ def test_tuple(): npt.assert_almost_equal(colors[1], [1., 0.5, 0.7, 0.2]) -@pytest.mark.parametrize("color1", generate_color_inputs("red")) -@pytest.mark.parametrize("color2", generate_color_inputs("green")) -@pytest.mark.parametrize("color3", generate_color_inputs("blue")) -def test_slice(color1, color2, color3): +@pytest.mark.parametrize("color_input", generate_color_inputs("red")) +@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(1, 10)]) +def test_slice(color_input, slice_method: dict): # slicing only first dim colors = make_colors_buffer() - colors[1:3] = color1 - truth = np.repeat([pygfx.Color(color1)], repeats=2, axis=0) - npt.assert_almost_equal(colors[1:3], truth) + s = slice_method["slice"] + indices = slice_method["indices"] + others = slice_method["others"] - colors[:] = "w" - npt.assert_almost_equal(colors[:], np.repeat([[1., 1., 1., 1.]], 10, axis=0)) - - colors[2:8:2] = color2 # set index 2, 4, 6 to color2 - truth = np.repeat([pygfx.Color(color2)], repeats=3, axis=0) - npt.assert_almost_equal(colors[2:8:2], truth) - # make sure others are not changed - others = [0, 1, 3, 5, 7, 8, 9] - npt.assert_almost_equal(colors[others], np.repeat([[1., 1., 1., 1.]], repeats=7, axis=0)) - - # set the others to color3 - colors[others] = color3 - truth = np.repeat([pygfx.Color(color3)], repeats=len(others), axis=0) - npt.assert_almost_equal(colors[others], truth) - # make sure color2 items are not touched - npt.assert_almost_equal(colors[2:8:2], np.repeat([pygfx.Color(color2)], repeats=3, axis=0)) + colors[s] = color_input + truth = np.repeat([pygfx.Color(color_input)], repeats=len(indices), axis=0) + # check that correct indices are modified + npt.assert_almost_equal(colors[s], truth) + # check that others are not touched + others_truth = np.repeat([[1., 1., 1., 1.]], repeats=len(others), axis=0) + npt.assert_almost_equal(colors[others], others_truth) # reset colors[:] = (1, 1, 1, 1) - - # negative slicing - colors[-5:] = color1 - truth = np.repeat([pygfx.Color(color1)], repeats=5, axis=0) - npt.assert_almost_equal(colors[-5:], truth) - - # set some to color2 - colors[-5:-1:2] = color2 - truth = np.repeat([pygfx.Color(color2)], 2, axis=0) - npt.assert_almost_equal(colors[[5, 7]], truth) - # make sure non-sliced not touched - npt.assert_almost_equal(colors[[6, 8, 9]], np.repeat([pygfx.Color(color1)], 3, axis=0)) - # make sure white non-sliced not touched - npt.assert_almost_equal(colors[:-5], np.repeat([[1., 1., 1., 1.]], 5, axis=0)) - - # negative slicing backwards, set points 5, 3 - colors[-5:1:-2] = color3 - truth = np.repeat([pygfx.Color(color3)], repeats=2, axis=0) - npt.assert_almost_equal(colors[[5, 3]], truth) - # make sure others are not touched - npt.assert_almost_equal(colors[[0, 1, 2]], np.repeat([[1., 1., 1., 1.]], repeats=3, axis=0)) - # this point should be color2 - npt.assert_almost_equal(colors[7], np.array(pygfx.Color(color2))) - # point 4 should be completely untouched - npt.assert_almost_equal(colors[4], [1, 1, 1, 1]) - # rest should be color1 - npt.assert_almost_equal(colors[[6, 8, 9]], np.repeat([pygfx.Color(color1)], 3, axis=0)) + npt.assert_almost_equal(colors[:], np.repeat([[1., 1., 1., 1.]], 10, axis=0)) def test_array(): From 054c836a46d61e684478d04dede5d360324ae9c9 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 20:41:40 -0400 Subject: [PATCH 021/196] more variants --- tests/test_buffer_manager.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_buffer_manager.py b/tests/test_buffer_manager.py index badc570c1..a289e9e3b 100644 --- a/tests/test_buffer_manager.py +++ b/tests/test_buffer_manager.py @@ -77,6 +77,16 @@ def generate_slice_indices(kind: int): s = slice(8, -9, -2) indices = [8, 6, 4, 2] + case 10: + # only stepped forward + s = slice(None, None, 2) + indices = [0, 2, 4, 6, 8] + + case 11: + # only stepped backward + s = slice(None, None, -3) + indices = [9, 6, 3, 0] + others = [i for i in a if i not in indices] return {"slice": s, "indices": indices, "others": others} @@ -133,7 +143,7 @@ def test_tuple(): @pytest.mark.parametrize("color_input", generate_color_inputs("red")) -@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(1, 10)]) +@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(1, 12)]) def test_slice(color_input, slice_method: dict): # slicing only first dim colors = make_colors_buffer() From f50614fd7101916c0a6bc919ac5f2688412c245c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 20:51:27 -0400 Subject: [PATCH 022/196] add array fancy indexing to same parameterization --- tests/test_buffer_manager.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/test_buffer_manager.py b/tests/test_buffer_manager.py index a289e9e3b..91778bdfa 100644 --- a/tests/test_buffer_manager.py +++ b/tests/test_buffer_manager.py @@ -87,12 +87,31 @@ def generate_slice_indices(kind: int): s = slice(None, None, -3) indices = [9, 6, 3, 0] + case 12: + # list indices + s = [2, 5, 9] + indices = [2, 5, 9] + + case 13: + # bool indices + s = a > 5 + indices = [6, 7, 8, 9] + + case 14: + # list indices with negatives + s = [1, 4, -2] + indices = [1, 4, 8] + + case 15: + # array indices + s = np.array([1, 4, -7, 9]) + indices = [1, 4, 3, 9] + others = [i for i in a if i not in indices] return {"slice": s, "indices": indices, "others": others} - def make_colors_buffer() -> ColorFeature: colors = ColorFeature(colors="w", n_colors=10) return colors @@ -143,7 +162,7 @@ def test_tuple(): @pytest.mark.parametrize("color_input", generate_color_inputs("red")) -@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(1, 12)]) +@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(1, 16)]) def test_slice(color_input, slice_method: dict): # slicing only first dim colors = make_colors_buffer() From 9ed2bf6c74a0754d6117b91a9f28654bdfcc577a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 21:03:27 -0400 Subject: [PATCH 023/196] parameterize tuple tests --- tests/test_buffer_manager.py | 60 ++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/tests/test_buffer_manager.py b/tests/test_buffer_manager.py index 91778bdfa..bf2760293 100644 --- a/tests/test_buffer_manager.py +++ b/tests/test_buffer_manager.py @@ -27,6 +27,11 @@ def generate_slice_indices(kind: int): a = np.arange(n_elements) match kind: + case 0: + # simplest, just int + s = 2 + indices = [2] + case 1: # everything s = slice(None, None, None) @@ -144,21 +149,54 @@ def test_int(): npt.assert_almost_equal(colors[2], [1., 0., 1., 0.5]) -def test_tuple(): +@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(0, 16)]) +def test_tuple(slice_method): # setting entire array manually colors = make_colors_buffer() - colors[1, :] = 0.5 - npt.assert_almost_equal(colors[1], [0.5, 0.5, 0.5, 0.5]) + s = slice_method["slice"] + indices = slice_method["indices"] + others = slice_method["others"] + + # set all RGBA vals + colors[s, :] = 0.5 + truth = np.repeat([[0.5, 0.5, 0.5, 0.5]], repeats=len(indices), axis=0) + npt.assert_almost_equal(colors[indices], truth) + + # check others are not modified + others_truth = np.repeat([[1., 1., 1., 1.]], repeats=len(others), axis=0) + npt.assert_almost_equal(colors[others], others_truth) + + # reset + colors[:] = (1, 1, 1, 1) + npt.assert_almost_equal(colors[:], np.repeat([[1., 1., 1., 1.]], 10, axis=0)) + + # set just R values + colors[s, 0] = 0.5 + truth = np.repeat([[0.5, 1., 1., 1.]], repeats=len(indices), axis=0) + # check others not modified + npt.assert_almost_equal(colors[indices], truth) + npt.assert_almost_equal(colors[others], others_truth) - colors[1, 0] = 1 - npt.assert_almost_equal(colors[1], [1., 0.5, 0.5, 0.5]) + # reset + colors[:] = (1, 1, 1, 1) + npt.assert_almost_equal(colors[:], np.repeat([[1., 1., 1., 1.]], 10, axis=0)) - colors[1, 2:] = 0.7 - npt.assert_almost_equal(colors[1], [1., 0.5, 0.7, 0.7]) + # set green and blue + colors[s, 1:-1] = 0.7 + truth = np.repeat([[1., 0.7, 0.7, 1.0]], repeats=len(indices), axis=0) + npt.assert_almost_equal(colors[indices], truth) + npt.assert_almost_equal(colors[others], others_truth) - colors[1, -1] = 0.2 - npt.assert_almost_equal(colors[1], [1., 0.5, 0.7, 0.2]) + # reset + colors[:] = (1, 1, 1, 1) + npt.assert_almost_equal(colors[:], np.repeat([[1., 1., 1., 1.]], 10, axis=0)) + + # set only alpha + colors[s, -1] = 0.2 + truth = np.repeat([[1., 1., 1., 0.2]], repeats=len(indices), axis=0) + npt.assert_almost_equal(colors[indices], truth) + npt.assert_almost_equal(colors[others], others_truth) @pytest.mark.parametrize("color_input", generate_color_inputs("red")) @@ -182,7 +220,3 @@ def test_slice(color_input, slice_method: dict): # reset colors[:] = (1, 1, 1, 1) npt.assert_almost_equal(colors[:], np.repeat([[1., 1., 1., 1.]], 10, axis=0)) - - -def test_array(): - pass From 5e1471ec847c72f5c3e7f039482ce4098c36699e Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 21:06:14 -0400 Subject: [PATCH 024/196] remove repr --- fastplotlib/graphics/_features/_sizes.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fastplotlib/graphics/_features/_sizes.py b/fastplotlib/graphics/_features/_sizes.py index 49702ec8f..3c4139c08 100644 --- a/fastplotlib/graphics/_features/_sizes.py +++ b/fastplotlib/graphics/_features/_sizes.py @@ -81,7 +81,3 @@ def _feature_changed(self, key, new_data): event_data = FeatureEvent(type="sizes", pick_info=pick_info) self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"PointsSizesFeature for {self._parent}, call `.sizes()` to get values" - return s From f59fb199e3088937727b8c83e2e6a7ad505ed6ba Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 23:14:44 -0400 Subject: [PATCH 025/196] test offset and size --- tests/test_buffer_manager.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/test_buffer_manager.py b/tests/test_buffer_manager.py index bf2760293..2a5dd8bdc 100644 --- a/tests/test_buffer_manager.py +++ b/tests/test_buffer_manager.py @@ -40,7 +40,7 @@ def generate_slice_indices(kind: int): case 2: # positive continuous range s = slice(1, 5, None) - indices = list(range(1, 5)) + indices = [1, 2, 3, 4] case 3: # positive stepped range @@ -114,7 +114,9 @@ def generate_slice_indices(kind: int): others = [i for i in a if i not in indices] - return {"slice": s, "indices": indices, "others": others} + offset, size = (min(indices), np.ptp(indices) + 1) + + return {"slice": s, "indices": indices, "others": others, "offset": offset, "size": size} def make_colors_buffer() -> ColorFeature: @@ -205,14 +207,29 @@ def test_slice(color_input, slice_method: dict): # slicing only first dim colors = make_colors_buffer() + # TODO: placeholder until I make a testing figure where we draw frames only on call + colors.buffer._gfx_pending_uploads.clear() + s = slice_method["slice"] indices = slice_method["indices"] + offset = slice_method["offset"] + size = slice_method["size"] others = slice_method["others"] colors[s] = color_input truth = np.repeat([pygfx.Color(color_input)], repeats=len(indices), axis=0) # check that correct indices are modified npt.assert_almost_equal(colors[s], truth) + + upload_offset, upload_size = colors.buffer._gfx_pending_uploads[-1] + # sometimes when slicing with step, it will over-estimate offset + # but it overestimates to upload 1 extra point so it's fine + assert (upload_offset == offset) or (upload_offset == offset - 1) + + # sometimes when slicing with step, it will over-estimate size + # but it overestimates to upload 1 extra point so it's fine + assert (upload_size == size) or (upload_size == size + 1) + # check that others are not touched others_truth = np.repeat([[1., 1., 1., 1.]], repeats=len(others), axis=0) npt.assert_almost_equal(colors[others], others_truth) From 4d25daa815b81c9d1ae0171811d1a617e5a1ea65 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 23:14:56 -0400 Subject: [PATCH 026/196] test offset and size --- tests/test_buffer_manager.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/test_buffer_manager.py b/tests/test_buffer_manager.py index 2a5dd8bdc..81c5b17fa 100644 --- a/tests/test_buffer_manager.py +++ b/tests/test_buffer_manager.py @@ -4,11 +4,7 @@ import pygfx -from fastplotlib.graphics._features import ColorFeature, PointsDataFeature -from fastplotlib.graphics._features.utils import parse_colors - - -# TODO: parameterize every test where the color is given in as str, array, tuple, and list +from fastplotlib.graphics._features import ColorFeature def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple]: @@ -124,10 +120,6 @@ def make_colors_buffer() -> ColorFeature: return colors -def make_points_buffer(): - pass - - def test_int(): # setting single points colors = make_colors_buffer() From aa1949b89f41c988a39a310566c75e2116b3cd3e Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 20 May 2024 23:36:43 -0400 Subject: [PATCH 027/196] test create colors --- ...nager.py => test_colors_buffer_manager.py} | 106 ++---------------- 1 file changed, 9 insertions(+), 97 deletions(-) rename tests/{test_buffer_manager.py => test_colors_buffer_manager.py} (65%) diff --git a/tests/test_buffer_manager.py b/tests/test_colors_buffer_manager.py similarity index 65% rename from tests/test_buffer_manager.py rename to tests/test_colors_buffer_manager.py index 81c5b17fa..cd39a0ad3 100644 --- a/tests/test_buffer_manager.py +++ b/tests/test_colors_buffer_manager.py @@ -5,6 +5,7 @@ import pygfx from fastplotlib.graphics._features import ColorFeature +from .utils import generate_slice_indices def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple]: @@ -18,108 +19,18 @@ def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple]: return [s, a, l, t] -def generate_slice_indices(kind: int): - n_elements = 10 - a = np.arange(n_elements) - - match kind: - case 0: - # simplest, just int - s = 2 - indices = [2] - - case 1: - # everything - s = slice(None, None, None) - indices = list(range(10)) - - case 2: - # positive continuous range - s = slice(1, 5, None) - indices = [1, 2, 3, 4] - - case 3: - # positive stepped range - s = slice(2, 8, 2) - indices = [2, 4, 6] - - case 4: - # negative continuous range - s = slice(-5, None, None) - indices = [5, 6, 7, 8, 9] - - case 5: - # negative backwards - s = slice(-5, None, -1) - indices = [5, 4, 3, 2, 1, 0] - - case 5: - # negative backwards stepped - s = slice(-5, None, -2) - indices = [5, 3, 1] - - case 6: - # negative stepped forward - s = slice(-5, None, 2) - indices = [5, 7, 9] - - case 7: - # both negative - s = slice(-8, -2, None) - indices = [2, 3, 4, 5, 6, 7] - - case 8: - # both negative and stepped - s = slice(-8, -2, 2) - indices = [2, 4, 6] - - case 9: - # positive, negative, negative - s = slice(8, -9, -2) - indices = [8, 6, 4, 2] - - case 10: - # only stepped forward - s = slice(None, None, 2) - indices = [0, 2, 4, 6, 8] - - case 11: - # only stepped backward - s = slice(None, None, -3) - indices = [9, 6, 3, 0] - - case 12: - # list indices - s = [2, 5, 9] - indices = [2, 5, 9] - - case 13: - # bool indices - s = a > 5 - indices = [6, 7, 8, 9] - - case 14: - # list indices with negatives - s = [1, 4, -2] - indices = [1, 4, 8] - - case 15: - # array indices - s = np.array([1, 4, -7, 9]) - indices = [1, 4, 3, 9] - - others = [i for i in a if i not in indices] - - offset, size = (min(indices), np.ptp(indices) + 1) - - return {"slice": s, "indices": indices, "others": others, "offset": offset, "size": size} - - def make_colors_buffer() -> ColorFeature: colors = ColorFeature(colors="w", n_colors=10) return colors +@pytest.mark.parametrize("color_input", [*generate_color_inputs("r"), *generate_color_inputs("g"), *generate_color_inputs("b")]) +def test_create_buffer(color_input): + colors = ColorFeature(colors=color_input, n_colors=10) + truth = np.repeat([pygfx.Color(color_input)], 10, axis=0) + npt.assert_almost_equal(colors[:], truth) + + def test_int(): # setting single points colors = make_colors_buffer() @@ -194,6 +105,7 @@ def test_tuple(slice_method): @pytest.mark.parametrize("color_input", generate_color_inputs("red")) +# skip testing with int since that results in shape [1, 4] with np.repeat, int tested in independent unit test @pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(1, 16)]) def test_slice(color_input, slice_method: dict): # slicing only first dim From c74d7b9ddb5cb18458794fcbef08477ed2cc6a90 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 00:32:29 -0400 Subject: [PATCH 028/196] also test with direct truth indices in colors --- tests/test_colors_buffer_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py index cd39a0ad3..1a1908316 100644 --- a/tests/test_colors_buffer_manager.py +++ b/tests/test_colors_buffer_manager.py @@ -124,6 +124,7 @@ def test_slice(color_input, slice_method: dict): truth = np.repeat([pygfx.Color(color_input)], repeats=len(indices), axis=0) # check that correct indices are modified npt.assert_almost_equal(colors[s], truth) + npt.assert_almost_equal(colors[indices], truth) upload_offset, upload_size = colors.buffer._gfx_pending_uploads[-1] # sometimes when slicing with step, it will over-estimate offset From cecb438fc313a16a2910092f4b159505c66c3ae2 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 00:32:43 -0400 Subject: [PATCH 029/196] points tests, works --- tests/test_points_data_buffer_manager.py | 117 +++++++++++++++++++++++ tests/utils.py | 98 +++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 tests/test_points_data_buffer_manager.py create mode 100644 tests/utils.py diff --git a/tests/test_points_data_buffer_manager.py b/tests/test_points_data_buffer_manager.py new file mode 100644 index 000000000..a0f528b08 --- /dev/null +++ b/tests/test_points_data_buffer_manager.py @@ -0,0 +1,117 @@ +from typing import Literal + +import numpy as np +from numpy import testing as npt +import pytest + +import pygfx + +from fastplotlib.graphics._features import PointsDataFeature +from .utils import generate_slice_indices + + +def generate_data(inputs: str) -> np.ndarray: + """ + Generates a spiral/spring + + Only 10 points so a very pointy spiral but easier to spot changes :D + """ + xs = np.linspace(0, 10 * np.pi, 10) + ys = np.sin(xs) + zs = np.cos(xs) + + match inputs: + case "y": + data = ys + + case "xy": + data = np.column_stack([xs, ys]) + + case "xyz": + data = np.column_stack([xs, ys, zs]) + + return data.astype(np.float32) + + +@pytest.mark.parametrize("data", [generate_data(v) for v in ["y", "xy", "xyz"]]) +def test_create_buffer(data): + points_data = PointsDataFeature(data) + + if data.ndim == 1: + # only y-vals specified + npt.assert_almost_equal(points_data[:, 1], generate_data("y")) + # x-vals are auto generated just using arange + npt.assert_almost_equal(points_data[:, 0], np.arange(data.size)) + + elif data.shape[1] == 2: + # test 2D + npt.assert_almost_equal(points_data[:, :-1], generate_data("xy")) + npt.assert_almost_equal(points_data[:, -1], 0.) + + + elif data.shape[1] == 3: + # test 3D spiral + npt.assert_almost_equal(points_data[:], generate_data("xyz")) + + +def test_int(): + data = generate_data("xyz") + # test setting single points + points = PointsDataFeature(data) + + # set all x, y, z points, create a kink in the spiral + points[2] = 1. + npt.assert_almost_equal(points[2], 1.) + # make sure other points are not affected + indices = list(range(10)) + indices.pop(2) + npt.assert_almost_equal(points[indices], data[indices]) + + +@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(1, 16)]) +@pytest.mark.parametrize("test_axis", ["y", "xy", "xyz"]) +def test_slice(slice_method: dict, test_axis: str): + data = generate_data("xyz") + + s = slice_method["slice"] + indices = slice_method["indices"] + offset = slice_method["offset"] + size = slice_method["size"] + others = slice_method["others"] + + points = PointsDataFeature(data) + # TODO: placeholder until I make a testing figure where we draw frames only on call + points.buffer._gfx_pending_uploads.clear() + + match test_axis: + case "y": + points[s, 1] = -data[s, 1] + npt.assert_almost_equal(points[s, 1], -data[s, 1]) + npt.assert_almost_equal(points[indices, 1], -data[indices, 1]) + # make sure other points are not modified + npt.assert_almost_equal(points[others, 1], data[others, 1]) # other points in same dimension + npt.assert_almost_equal(points[:, 2:], data[:, 2:]) # dimensions that are not sliced + + case "xy": + points[s, :-1] = -data[s, :-1] + npt.assert_almost_equal(points[s, :-1], -data[s, :-1]) + npt.assert_almost_equal(points[indices, :-1], -data[s, :-1]) + # make sure other points are not modified + npt.assert_almost_equal(points[others, :-1], data[others, :-1]) # other points in the same dimensions + npt.assert_almost_equal(points[:, -1], data[:, -1]) # dimensions that are not touched + + case "xyz": + points[s] = -data[s] + npt.assert_almost_equal(points[s], -data[s]) + npt.assert_almost_equal(points[indices], -data[s]) + # make sure other points are not modified + npt.assert_almost_equal(points[others], data[others]) + + upload_offset, upload_size = points.buffer._gfx_pending_uploads[-1] + # sometimes when slicing with step, it will over-estimate offset + # but it overestimates to upload 1 extra point so it's fine + assert (upload_offset == offset) or (upload_offset == offset - 1) + + # sometimes when slicing with step, it will over-estimate size + # but it overestimates to upload 1 extra point so it's fine + assert (upload_size == size) or (upload_size == size + 1) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 000000000..683a9ba46 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,98 @@ +import numpy as np + + +def generate_slice_indices(kind: int): + n_elements = 10 + a = np.arange(n_elements) + + match kind: + case 0: + # simplest, just int + s = 2 + indices = [2] + + case 1: + # everything + s = slice(None, None, None) + indices = list(range(10)) + + case 2: + # positive continuous range + s = slice(1, 5, None) + indices = [1, 2, 3, 4] + + case 3: + # positive stepped range + s = slice(2, 8, 2) + indices = [2, 4, 6] + + case 4: + # negative continuous range + s = slice(-5, None, None) + indices = [5, 6, 7, 8, 9] + + case 5: + # negative backwards + s = slice(-5, None, -1) + indices = [5, 4, 3, 2, 1, 0] + + case 5: + # negative backwards stepped + s = slice(-5, None, -2) + indices = [5, 3, 1] + + case 6: + # negative stepped forward + s = slice(-5, None, 2) + indices = [5, 7, 9] + + case 7: + # both negative + s = slice(-8, -2, None) + indices = [2, 3, 4, 5, 6, 7] + + case 8: + # both negative and stepped + s = slice(-8, -2, 2) + indices = [2, 4, 6] + + case 9: + # positive, negative, negative + s = slice(8, -9, -2) + indices = [8, 6, 4, 2] + + case 10: + # only stepped forward + s = slice(None, None, 2) + indices = [0, 2, 4, 6, 8] + + case 11: + # only stepped backward + s = slice(None, None, -3) + indices = [9, 6, 3, 0] + + case 12: + # list indices + s = [2, 5, 9] + indices = [2, 5, 9] + + case 13: + # bool indices + s = a > 5 + indices = [6, 7, 8, 9] + + case 14: + # list indices with negatives + s = [1, 4, -2] + indices = [1, 4, 8] + + case 15: + # array indices + s = np.array([1, 4, -7, 9]) + indices = [1, 4, 3, 9] + + others = [i for i in a if i not in indices] + + offset, size = (min(indices), np.ptp(indices) + 1) + + return {"slice": s, "indices": indices, "others": others, "offset": offset, "size": size} From 0a20008f6bc01e11bada5bdfde624210447c288c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 00:57:27 -0400 Subject: [PATCH 030/196] remove imports --- tests/test_points_data_buffer_manager.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_points_data_buffer_manager.py b/tests/test_points_data_buffer_manager.py index a0f528b08..12975162e 100644 --- a/tests/test_points_data_buffer_manager.py +++ b/tests/test_points_data_buffer_manager.py @@ -1,11 +1,7 @@ -from typing import Literal - import numpy as np from numpy import testing as npt import pytest -import pygfx - from fastplotlib.graphics._features import PointsDataFeature from .utils import generate_slice_indices @@ -68,7 +64,7 @@ def test_int(): npt.assert_almost_equal(points[indices], data[indices]) -@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(1, 16)]) +@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(1, 16)]) # int tested separately @pytest.mark.parametrize("test_axis", ["y", "xy", "xyz"]) def test_slice(slice_method: dict, test_axis: str): data = generate_data("xyz") From a1c8c873996251020e89d3b1ae9b88e6cfa02bb0 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 01:02:08 -0400 Subject: [PATCH 031/196] sizes test working, other cleanup --- tests/__init__.py | 0 tests/test_colors_buffer_manager.py | 12 +--- tests/test_points_data_buffer_manager.py | 12 +--- tests/test_sizes_buffer_manager.py | 74 ++++++++++++++++++++++++ tests/utils.py | 13 +++++ 5 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_sizes_buffer_manager.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py index 1a1908316..884a70eed 100644 --- a/tests/test_colors_buffer_manager.py +++ b/tests/test_colors_buffer_manager.py @@ -5,7 +5,7 @@ import pygfx from fastplotlib.graphics._features import ColorFeature -from .utils import generate_slice_indices +from .utils import generate_slice_indices, assert_pending_uploads def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple]: @@ -126,14 +126,8 @@ def test_slice(color_input, slice_method: dict): npt.assert_almost_equal(colors[s], truth) npt.assert_almost_equal(colors[indices], truth) - upload_offset, upload_size = colors.buffer._gfx_pending_uploads[-1] - # sometimes when slicing with step, it will over-estimate offset - # but it overestimates to upload 1 extra point so it's fine - assert (upload_offset == offset) or (upload_offset == offset - 1) - - # sometimes when slicing with step, it will over-estimate size - # but it overestimates to upload 1 extra point so it's fine - assert (upload_size == size) or (upload_size == size + 1) + # make sure correct offset and size marked for pending upload + assert_pending_uploads(colors.buffer, offset, size) # check that others are not touched others_truth = np.repeat([[1., 1., 1., 1.]], repeats=len(others), axis=0) diff --git a/tests/test_points_data_buffer_manager.py b/tests/test_points_data_buffer_manager.py index 12975162e..ac0e7c784 100644 --- a/tests/test_points_data_buffer_manager.py +++ b/tests/test_points_data_buffer_manager.py @@ -3,7 +3,7 @@ import pytest from fastplotlib.graphics._features import PointsDataFeature -from .utils import generate_slice_indices +from .utils import generate_slice_indices, assert_pending_uploads def generate_data(inputs: str) -> np.ndarray: @@ -103,11 +103,5 @@ def test_slice(slice_method: dict, test_axis: str): # make sure other points are not modified npt.assert_almost_equal(points[others], data[others]) - upload_offset, upload_size = points.buffer._gfx_pending_uploads[-1] - # sometimes when slicing with step, it will over-estimate offset - # but it overestimates to upload 1 extra point so it's fine - assert (upload_offset == offset) or (upload_offset == offset - 1) - - # sometimes when slicing with step, it will over-estimate size - # but it overestimates to upload 1 extra point so it's fine - assert (upload_size == size) or (upload_size == size + 1) + # make sure correct offset and size marked for pending upload + assert_pending_uploads(points.buffer, offset, size) diff --git a/tests/test_sizes_buffer_manager.py b/tests/test_sizes_buffer_manager.py new file mode 100644 index 000000000..0f90353f4 --- /dev/null +++ b/tests/test_sizes_buffer_manager.py @@ -0,0 +1,74 @@ +import numpy as np +from numpy import testing as npt +import pytest + +from fastplotlib.graphics._features import PointsSizesFeature +from .utils import generate_slice_indices, assert_pending_uploads + + +def generate_data(input_type: str) -> np.ndarray | float: + """ + Point sizes varying with a sine wave + + Parameters + ---------- + input_type: str + one of "sine", "cosine", or "float" + """ + if input_type == "float": + return 10. + xs = np.linspace(0, 10 * np.pi, 10) + + if input_type == "sine": + return np.abs(np.sin(xs)).astype(np.float32) + + if input_type == "cosine": + return np.abs(np.cos(xs)).astype(np.float32) + + +@pytest.mark.parametrize("data", [generate_data(v) for v in ["float", "sine"]]) +def test_create_buffer(data): + sizes = PointsSizesFeature(data, n_datapoints=10) + + if isinstance(data, float): + npt.assert_almost_equal(sizes[:], generate_data("float")) + + elif isinstance(data, np.ndarray): + npt.assert_almost_equal(sizes[:], generate_data("sine")) + + +@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(0, 16)]) +@pytest.mark.parametrize("user_input", ["float", "cosine"]) +def test_slice(slice_method: dict, user_input: str): + data = generate_data("sine") + + s = slice_method["slice"] + indices = slice_method["indices"] + offset = slice_method["offset"] + size = slice_method["size"] + others = slice_method["others"] + + sizes = PointsSizesFeature(data, n_datapoints=10) + + # TODO: placeholder until I make a testing figure where we draw frames only on call + sizes.buffer._gfx_pending_uploads.clear() + + match user_input: + case "float": + sizes[s] = 20. + truth = np.full(len(indices), 20.) + npt.assert_almost_equal(sizes[s], truth) + npt.assert_almost_equal(sizes[indices], truth) + # make sure other sizes not modified + npt.assert_almost_equal(sizes[others], data[others]) + + case "cosine": + cosine = generate_data("cosine") + sizes[s] = cosine[s] + npt.assert_almost_equal(sizes[s], cosine[s]) + npt.assert_almost_equal(sizes[indices], cosine[s]) + # make sure other sizes not modified + npt.assert_almost_equal(sizes[others], data[others]) + + # make sure correct offset and size marked for pending upload + assert_pending_uploads(sizes.buffer, offset, size) diff --git a/tests/utils.py b/tests/utils.py index 683a9ba46..df991095a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,7 @@ import numpy as np +import pygfx + def generate_slice_indices(kind: int): n_elements = 10 @@ -96,3 +98,14 @@ def generate_slice_indices(kind: int): offset, size = (min(indices), np.ptp(indices) + 1) return {"slice": s, "indices": indices, "others": others, "offset": offset, "size": size} + + +def assert_pending_uploads(buffer: pygfx.Buffer, offset: int, size: int): + upload_offset, upload_size = buffer._gfx_pending_uploads[-1] + # sometimes when slicing with step, it will over-estimate offset + # but it overestimates to upload 1 extra point so it's fine + assert (upload_offset == offset) or (upload_offset == offset - 1) + + # sometimes when slicing with step, it will over-estimate size + # but it overestimates to upload 1 extra point so it's fine + assert (upload_size == size) or (upload_size == size + 1) \ No newline at end of file From db93f7021594b29ca6680b84ca3508a86a3a65cf Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 01:04:52 -0400 Subject: [PATCH 032/196] export sizes feature again --- fastplotlib/graphics/_features/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index 417472f72..535167ef6 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -1,6 +1,6 @@ from ._colors import ColorFeature#, CmapFeature, ImageCmapFeature, HeatmapCmapFeature from ._data import PointsDataFeature#, ImageDataFeature, HeatmapDataFeature -# from ._sizes import PointsSizesFeature +from ._sizes import PointsSizesFeature # from ._present import PresentFeature # from ._thickness import ThicknessFeature from ._base import ( @@ -41,9 +41,6 @@ class Deleted: class CmapFeature: pass -class PointsSizesFeature: - pass - class ThicknessFeature: pass From baca1fc8378498894f4db249d2918c9ea7e414b5 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 01:05:22 -0400 Subject: [PATCH 033/196] ideas for sharing and unsharing buffers between graphics --- fastplotlib/graphics/_features/_base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index c138b3c8e..40675cba7 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -201,6 +201,8 @@ def __init__( self._event_handlers: list[callable] = list() + self._shared = False + @property def value(self) -> NDArray: return self.buffer.data @@ -209,6 +211,11 @@ def value(self) -> NDArray: def buffer(self) -> pygfx.Buffer | pygfx.Texture: return self._buffer + @property + def shared(self) -> bool: + """If the buffer is shared between multiple graphics""" + return self._shared + def __getitem__(self, item): return self.buffer.data[item] From a177a7f63f6ba149657100f105186c3149225a17 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 01:05:37 -0400 Subject: [PATCH 034/196] ideas for sharing and unsharing buffers between graphics, nto tested --- fastplotlib/graphics/line.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index b4589f413..a21168e55 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -92,11 +92,15 @@ def __init__( n_colors=n_datapoints, cmap_name=cmap, cmap_values=cmap_values ) - self._colors = ColorFeature( - colors, - n_colors=self._data.value.shape[0], - alpha=alpha, - ) + if isinstance(colors, ColorFeature): + self._colors = colors + self._shared = True + else: + self._colors = ColorFeature( + colors, + n_colors=self._data.value.shape[0], + alpha=alpha, + ) # self.cmap = CmapFeature( # self, self.colors(), cmap_name=cmap, cmap_values=cmap_values @@ -122,6 +126,16 @@ def __init__( if z_position is not None: self.position_z = z_position + def unshare_buffer(self, feature: str): + f = getattr(self, feature) + if not f.shared: + raise BufferError + + if isinstance(f, ColorFeature): + self._colors._buffer = pygfx.Buffer(self._colors.value.copy()) + self.world_object.geometry.colors = self._colors.buffer + self._colors._shared = False + def add_linear_selector( self, selection: int = None, padding: float = 50, **kwargs ) -> LinearSelector: From c205b10bc733e2ae06ee70b94576c1e86c0921b0 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 01:05:48 -0400 Subject: [PATCH 035/196] typing --- fastplotlib/graphics/_features/_sizes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastplotlib/graphics/_features/_sizes.py b/fastplotlib/graphics/_features/_sizes.py index 3c4139c08..b45474e5e 100644 --- a/fastplotlib/graphics/_features/_sizes.py +++ b/fastplotlib/graphics/_features/_sizes.py @@ -15,7 +15,7 @@ class PointsSizesFeature(BufferManager): def __init__( self, - sizes: np.ndarray | list[int | float] | tuple[int | float], + sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], n_datapoints: int, isolated_buffer: bool = True ): @@ -23,7 +23,7 @@ def __init__( super().__init__(data=sizes, isolated_buffer=isolated_buffer) def _fix_sizes(self, sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], n_datapoints: int): - if np.issubdtype(type(sizes), np.integer): + if np.issubdtype(type(sizes), np.number): # single value given sizes = np.full( n_datapoints, sizes, dtype=np.float32 From f380b26eeb273f538a15ab3596bf7abea6ce374f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 14:46:59 -0400 Subject: [PATCH 036/196] attach and detach buffers to a graphic, not tested --- fastplotlib/graphics/_base.py | 66 +++++++++++++++++++++++-- fastplotlib/graphics/_features/_base.py | 6 +-- fastplotlib/graphics/line.py | 16 ++---- fastplotlib/graphics/scatter.py | 4 +- 4 files changed, 69 insertions(+), 23 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index aaa4f6f73..88b4d3225 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -7,9 +7,9 @@ import numpy as np import pylinalg as la -from pygfx import WorldObject +import pygfx -from ._features import GraphicFeature, PresentFeature, BufferManager, GraphicFeatureDescriptor, Deleted +from ._features import GraphicFeature, PresentFeature, BufferManager, GraphicFeatureDescriptor, Deleted, PointsDataFeature, ColorFeature, PointsSizesFeature HexStr: TypeAlias = str @@ -112,14 +112,20 @@ def name(self, name: str): self._name = name @property - def world_object(self) -> WorldObject: + def world_object(self) -> pygfx.WorldObject: """Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly.""" # We use weakref to simplify garbage collection return weakref.proxy(WORLD_OBJECTS[self._fpl_address]) - def _set_world_object(self, wo: WorldObject): + def _set_world_object(self, wo: pygfx.WorldObject): WORLD_OBJECTS[self._fpl_address] = wo + def detach_feature(self, feature: str): + raise NotImplementedError + + def attach_feature(self, feature: BufferManager): + raise NotImplementedError + @property def position(self) -> np.ndarray: """position of the graphic, [x, y, z]""" @@ -175,7 +181,7 @@ def visible(self, v: bool): self.world_object.visible = v @property - def children(self) -> list[WorldObject]: + def children(self) -> list[pygfx.WorldObject]: """Return the children of the WorldObject.""" return self.world_object.children @@ -264,6 +270,56 @@ def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"): self.rotation = la.quat_mul(rot, self.rotation) +class PositionsGraphic(Graphic): + """Base class for LineGraphic and ScatterGraphic""" + + def detach_feature(self, feature: str): + if not isinstance(feature, str): + raise TypeError + + f = getattr(self, feature) + if f.shared == 0: + raise BufferError("Cannot detach an independent buffer") + + if feature == "colors": + self._colors._buffer = pygfx.Buffer(self._colors.value.copy()) + self.world_object.geometry.colors = self._colors.buffer + self._colors._shared -= 1 + + elif feature == "data": + self._data._buffer = pygfx.Buffer(self._data.value.copy()) + self.world_object.geometry.positions = self._data.buffer + self._data._shared -= 1 + + elif feature == "sizes": + self._sizes._buffer = pygfx.Buffer(self._sizes.value.copy()) + self.world_object.geometry.positions = self._sizes.buffer + self._sizes._shared -= 1 + + def attach_feature(self, feature: PointsDataFeature | ColorFeature | PointsSizesFeature): + if isinstance(feature, PointsDataFeature): + # TODO: check if this causes a memory leak + self._data._shared -= 1 + + self._data = feature + self._data._shared += 1 + self.world_object.geometry.positions = self._data.buffer + + elif isinstance(feature, ColorFeature): + self._colors._shared -= 1 + + self._colors = feature + self._colors._shared += 1 + self.world_object.geometry.colors = self._colors.buffer + + elif isinstance(feature, PointsSizesFeature): + self._sizes._shared -= 1 + + self._sizes = feature + self._sizes._shared += 1 + self.world_object.geometry.sizes = self._sizes.buffer + + class Interaction(ABC): """Mixin class that makes graphics interactive""" diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 40675cba7..49dc6f09d 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -201,7 +201,7 @@ def __init__( self._event_handlers: list[callable] = list() - self._shared = False + self._shared: int = 0 @property def value(self) -> NDArray: @@ -212,8 +212,8 @@ def buffer(self) -> pygfx.Buffer | pygfx.Texture: return self._buffer @property - def shared(self) -> bool: - """If the buffer is shared between multiple graphics""" + def shared(self) -> int: + """Number of graphics that share this buffer""" return self._shared def __getitem__(self, item): diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index a21168e55..b775dbd93 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -6,12 +6,12 @@ import pygfx from ..utils import parse_cmap_values -from ._base import Graphic, Interaction, PreviouslyModifiedData +from ._base import PositionsGraphic, Interaction, PreviouslyModifiedData from ._features import GraphicFeatureDescriptor, PointsDataFeature, ColorFeature#, CmapFeature, ThicknessFeature from .selectors import LinearRegionSelector, LinearSelector -class LineGraphic(Graphic, Interaction): +class LineGraphic(PositionsGraphic, Interaction): features = {"data", "colors"}#, "cmap", "thickness", "present"} def __init__( @@ -94,7 +94,7 @@ def __init__( if isinstance(colors, ColorFeature): self._colors = colors - self._shared = True + self._colors._shared += 1 else: self._colors = ColorFeature( colors, @@ -126,16 +126,6 @@ def __init__( if z_position is not None: self.position_z = z_position - def unshare_buffer(self, feature: str): - f = getattr(self, feature) - if not f.shared: - raise BufferError - - if isinstance(f, ColorFeature): - self._colors._buffer = pygfx.Buffer(self._colors.value.copy()) - self.world_object.geometry.colors = self._colors.buffer - self._colors._shared = False - def add_linear_selector( self, selection: int = None, padding: float = 50, **kwargs ) -> LinearSelector: diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 8682df3d5..eddf0fac3 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -4,11 +4,11 @@ import pygfx from ..utils import parse_cmap_values -from ._base import Graphic +from ._base import PositionsGraphic from ._features import PointsDataFeature, ColorFeature, CmapFeature, PointsSizesFeature -class ScatterGraphic(Graphic): +class ScatterGraphic(PositionsGraphic): feature_events = {"data", "sizes", "colors", "cmap", "present"} def __init__( From 3585c9ec4d37bc771b72a96806298cb2bfbdd263 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 14:47:15 -0400 Subject: [PATCH 037/196] import --- fastplotlib/graphics/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 88b4d3225..a93b1cace 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -16,7 +16,7 @@ # dict that holds all world objects for a given python kernel/session # Graphic objects only use proxies to WorldObjects -WORLD_OBJECTS: dict[HexStr, WorldObject] = dict() #: {hex id str: WorldObject} +WORLD_OBJECTS: dict[HexStr, pygfx.WorldObject] = dict() #: {hex id str: WorldObject} PYGFX_EVENTS = [ From 8d32134a17e5e40daebb4e1cb085fb5cf08134e0 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 16:17:33 -0400 Subject: [PATCH 038/196] more int point tests --- tests/test_points_data_buffer_manager.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_points_data_buffer_manager.py b/tests/test_points_data_buffer_manager.py index ac0e7c784..eac03664a 100644 --- a/tests/test_points_data_buffer_manager.py +++ b/tests/test_points_data_buffer_manager.py @@ -63,6 +63,20 @@ def test_int(): indices.pop(2) npt.assert_almost_equal(points[indices], data[indices]) + # reset + points = data + npt.assert_almost_equal(points[:], data) + + # just set y value + points[3, 1] = 1. + npt.assert_almost_equal(points[3, 1], 1.) + # make sure others not modified + npt.assert_almost_equal(points[3, 0], data[3, 0]) + npt.assert_almost_equal(points[3, 2], data[3, 2]) + indices = list(range(10)) + indices.pop(3) + npt.assert_almost_equal(points[indices], data[indices]) + @pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(1, 16)]) # int tested separately @pytest.mark.parametrize("test_axis", ["y", "xy", "xyz"]) From deceeeb22ce3ea0290bdfdb6cb3b7e317e0464b4 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 19:02:11 -0400 Subject: [PATCH 039/196] Graphic.add_event_handler --- fastplotlib/graphics/_base.py | 70 +++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index a93b1cace..26f5ea57d 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,3 +1,5 @@ +from collections import defaultdict +from functools import partial from typing import Any, Literal, TypeAlias import weakref from warnings import warn @@ -6,6 +8,7 @@ import numpy as np import pylinalg as la +from wgpu.gui.base import log_exception import pygfx @@ -93,6 +96,8 @@ def __init__( self._plot_area = None + self._event_handlers = defaultdict(set) + @property def name(self) -> str | None: """str name reference for this item""" @@ -185,6 +190,71 @@ def children(self) -> list[pygfx.WorldObject]: """Return the children of the WorldObject.""" return self.world_object.children + def add_event_handler(self, *args): + """ + Register an event handler. + + Parameters + ---------- + callback: callable, the first argument + Event handler, must accept a single event argument + *types: list of strings + A list of event types, ex: "click", "data", "colors", "pointer_down" + + For the available renderer event types, see + https://jupyter-rfb.readthedocs.io/en/stable/events.html + + All feature support events, i.e. ``graphic.features`` will give a set of + all features that are evented + + Can also be used as a decorator. + + Example + ------- + + .. code-block:: py + + def my_handler(event): + print(event) + + graphic.add_event_handler(my_handler, "pointer_up", "pointer_down") + + Decorator usage example: + + .. code-block:: py + + @graphic.add_event_handler("click") + def my_handler(event): + print(event) + """ + + decorating = not callable(args[0]) + callback = None if decorating else args[0] + types = args if decorating else args[1:] + + def decorator(_callback): + _callback_injector = partial(self._handle_event, _callback) # adds graphic instance as attribute + + for type in types: + if type in self.features: + # fpl feature event + feature = getattr(self, f"_{type}") + feature.add_event_handler(_callback_injector) + else: + # wrap pygfx event + self.world_object._event_handlers[type].add(_callback_injector) + return _callback + + if decorating: + return decorator + + return decorator(callback) + + def _handle_event(self, callback, event: pygfx.Event): + """Wrap pygfx event to add graphic to pick_info""" + event.graphic = self + callback(event) + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area From 66b5e6dc4d7ecea75a2ce97408b69169af7d0e83 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 21:08:28 -0400 Subject: [PATCH 040/196] adding and removing data feature event works and tested --- fastplotlib/graphics/_base.py | 53 ++++++++++++- fastplotlib/graphics/_features/_base.py | 54 ++++++------- fastplotlib/graphics/_features/_colors.py | 52 ++----------- fastplotlib/graphics/_features/_data.py | 32 +------- fastplotlib/graphics/line.py | 12 +-- fastplotlib/graphics/scatter.py | 29 ++++--- tests/events.py | 92 +++++++++++++++++++++++ 7 files changed, 197 insertions(+), 127 deletions(-) create mode 100644 tests/events.py diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 26f5ea57d..2992cf875 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -96,8 +96,12 @@ def __init__( self._plot_area = None + # event handlers self._event_handlers = defaultdict(set) + # maps callbacks to their partials + self._event_handler_wrappers = defaultdict(set) + @property def name(self) -> str | None: """str name reference for this item""" @@ -190,6 +194,14 @@ def children(self) -> list[pygfx.WorldObject]: """Return the children of the WorldObject.""" return self.world_object.children + @property + def event_handlers(self) -> list[tuple[str, callable, ...]]: + """ + Registered event handlers. Read-only use ``add_event_handler()`` + and ``remove_event_handler()`` to manage callbacks + """ + return list(self._event_handlers.items()) + def add_event_handler(self, *args): """ Register an event handler. @@ -235,14 +247,20 @@ def my_handler(event): def decorator(_callback): _callback_injector = partial(self._handle_event, _callback) # adds graphic instance as attribute - for type in types: - if type in self.features: + for t in types: + # add to our record + self._event_handlers[t].add(_callback) + + if t in self.features: # fpl feature event - feature = getattr(self, f"_{type}") + feature = getattr(self, f"_{t}") feature.add_event_handler(_callback_injector) else: # wrap pygfx event - self.world_object._event_handlers[type].add(_callback_injector) + self.world_object._event_handlers[t].add(_callback_injector) + + # keep track of the partial too + self._event_handler_wrappers[t].add((_callback, _callback_injector)) return _callback if decorating: @@ -253,8 +271,35 @@ def decorator(_callback): def _handle_event(self, callback, event: pygfx.Event): """Wrap pygfx event to add graphic to pick_info""" event.graphic = self + + if event.type in self.features: + # for feature events + event._target = self.world_object + callback(event) + def remove_event_handler(self, callback, *types): + # remove from our record first + for t in types: + for wrapper_map in self._event_handler_wrappers[t]: + # TODO: not sure if we can handle this mapping in a better way + if wrapper_map[0] == callback: + wrapper = wrapper_map[1] + self._event_handler_wrappers[t].remove(wrapper_map) + break + else: + raise KeyError(f"event type: {t} with callback: {callback} is not registered") + + self._event_handlers[t].remove(callback) + # remove callback wrapper from world object if pygfx event + if t in PYGFX_EVENTS: + print("pygfx event") + print(wrapper) + self.world_object.remove_event_handler(wrapper, t) + else: + feature = getattr(self, f"_{t}") + feature.remove_event_handler(wrapper) + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 49dc6f09d..dd2e060ba 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -6,6 +6,8 @@ import numpy as np from numpy.typing import NDArray +from wgpu.gui.base import log_exception + import pygfx @@ -41,7 +43,7 @@ def to_gpu_supported_dtype(array): return array -class FeatureEvent: +class FeatureEvent(pygfx.Event): """ Dataclass that holds feature event information. Has ``type`` and ``pick_info`` attributes. @@ -64,16 +66,9 @@ class FeatureEvent: """ - def __init__(self, type: str, pick_info: dict): - self.type = type - self.pick_info = pick_info - - def __repr__(self): - return ( - f"{self.__class__.__name__} @ {hex(id(self))}\n" - f"type: {self.type}\n" - f"pick_info: {self.pick_info}\n" - ) + def __init__(self, type: str, info: dict): + super().__init__(type=type) + self.info = info class GraphicFeature: @@ -100,7 +95,7 @@ def block_events(self, val: bool): def add_event_handler(self, handler: callable): """ - Add an event handler. All added event handlers are calledcollection_ind when this feature changes. + Add an event handler. All added event handlers are called when this feature changes. The ``handler`` can optionally accept a :class:`.FeatureEvent` as the first and only argument. The ``FeatureEvent`` only has 2 attributes, ``type`` which denotes the type of event @@ -141,32 +136,13 @@ def clear_event_handlers(self): """Clear all event handlers""" self._event_handlers.clear() - # TODO: maybe this can be implemented right here in the base class - @abstractmethod - def _feature_changed(self,new_data: Any, key: int | slice | tuple[slice] | None = None): - """Called whenever a feature changes, and it calls all funcs in self._event_handlers""" - pass - def _call_event_handlers(self, event_data: FeatureEvent): if self._block_events: return for func in self._event_handlers: - try: - args = getfullargspec(func).args - - if len(args) > 0: - if args[0] == "self" and not len(args) > 1: - func() - else: - func(event_data) - else: - func() - except TypeError: - warn( - f"Event handler {func} has an unresolvable argspec, calling it without arguments" - ) - func() + with log_exception(f"Error during handling {self.__class__.__name__} event"): + func(event_data) def __repr__(self) -> str: raise NotImplementedError @@ -295,6 +271,18 @@ def _update_range(self, key: int | slice | np.ndarray[int | bool] | list[bool | self.buffer.update_range(offset=offset, size=size) + def _emit_event(self, type: str, key, value): + if len(self._event_handlers) < 1: + return + + event_info = { + "key": key, + "value": value, + } + event = FeatureEvent(type, info=event_info) + + super()._call_event_handlers(event) + def __repr__(self): return f"{self.__class__.__name__} buffer data:\n" \ f"{self.value.__repr__()}" diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index eedb5563e..4fd40ac0e 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -18,23 +18,11 @@ class ColorFeature(BufferManager): """ Manages the color buffer for :class:`LineGraphic` or :class:`ScatterGraphic` - - **event pick info:** - - ==================== =============================== ========================================================================= - key type description - ==================== =============================== ========================================================================= - "index" ``numpy.ndarray`` or ``None`` changed indices in the buffer - "new_data" ``numpy.ndarray`` or ``None`` new buffer data at the changed indices - "collection-index" int the index of the graphic within the collection that triggered the event - "world_object" pygfx.WorldObject world object - ==================== =============================== ========================================================================= - """ def __init__( self, - colors, + colors: str | np.ndarray | tuple[float, float, float, float] | list[str] | list[float] | int | float, n_colors: int, alpha: float = None, isolated_buffer: bool = True, @@ -44,16 +32,14 @@ def __init__( Parameters ---------- - parent: Graphic or GraphicCollection - - colors: str, array, or iterable - specify colors as a single human readable string, RGBA array, + colors: str | np.ndarray | tuple[float, float, float, float] | list[str] | list[float] | int | float + specify colors as a single human-readable string, RGBA array, or an iterable of strings or RGBA arrays n_colors: int - number of colors to hold, if passing in a single str or single RGBA array + number of colors, if passing in a single str or single RGBA array - alpha: float + alpha: float, optional alpha value for the colors """ @@ -66,11 +52,8 @@ def __setitem__( key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], value: str | np.ndarray | tuple[float, float, float, float] | list[str] | list[float] | int | float ): - # if key is tuple assume they want to edit [n_points, RGBA] directly - # if key is slice | range | int | np.ndarray, they are slicing only n_points, get n_points and parse colors - if isinstance(key, tuple): - # directly setting RGBA values, we do no parsing + # directly setting RGBA values for points, we do no parsing if not isinstance(value, (int, float, np.ndarray)): raise TypeError( "Can only set from int, float, or array to set colors directly by slicing the entire array" @@ -118,29 +101,8 @@ def __setitem__( self.buffer.data[key] = value self._update_range(key) - # self._feature_changed(key, new_colors) - - def _feature_changed(self, key, new_data): - key = cleanup_slice(key, self._upper_bound) - if isinstance(key, int): - indices = [key] - elif isinstance(key, slice): - indices = range(key.start, key.stop, key.step) - elif isinstance(key, np.ndarray): - indices = key - else: - raise TypeError("feature changed key must be slice or int") - - pick_info = { - "index": indices, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="colors", pick_info=pick_info) - self._call_event_handlers(event_data) + self._emit_event("colors", key, value) # class CmapFeature(ColorFeature): diff --git a/fastplotlib/graphics/_features/_data.py b/fastplotlib/graphics/_features/_data.py index 2be6170ac..e7a422d93 100644 --- a/fastplotlib/graphics/_features/_data.py +++ b/fastplotlib/graphics/_features/_data.py @@ -43,40 +43,12 @@ def _fix_data(self, data): def __setitem__(self, key: int | slice | range | np.ndarray[int | bool] | tuple[slice, ...] | tuple[range, ...], value): # directly use the key to slice the buffer self.buffer.data[key] = value + # _update_range handles parsing the key to # determine offset and size for GPU upload self._update_range(key) - # avoid creating dicts constantly if there are no events to handle - # if len(self._event_handlers) > 0: - # self._feature_changed(key, value) - - def _feature_changed(self, key, new_data): - if key is not None: - key = cleanup_slice(key, self._upper_bound) - if isinstance(key, (int, np.integer)): - indices = [key] - elif isinstance(key, slice): - indices = range(key.start, key.stop, key.step) - elif isinstance(key, np.ndarray): - indices = key - elif key is None: - indices = None - - pick_info = { - "index": indices, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="data", pick_info=pick_info) - - self._call_event_handlers(event_data) - - # def __repr__(self) -> str: - # s = f"PointsDataFeature for {self._parent}, call `.data()` to get values" - # return s + self._emit_event("data", key, value) # # class ImageDataFeature(GraphicFeatureIndexable): diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index b775dbd93..6685e38a6 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -118,7 +118,7 @@ def __init__( world_object: pygfx.Line = pygfx.Line( # self.data.feature_data because data is a Buffer geometry=pygfx.Geometry(positions=self._data.buffer, colors=self._colors.buffer), - material=material(thickness=thickness, color_mode="vertex"), + material=material(thickness=thickness, color_mode="vertex", pick_write=True), ) self._set_world_object(world_object) @@ -231,7 +231,7 @@ def add_linear_region_selector( # TODO: this method is a bit of a mess, can refactor later def _get_linear_selector_init_args(self, padding: float, **kwargs): # computes initial bounds, limits, size and origin of linear selectors - data = self.data() + data = self.data.value if "axis" in kwargs.keys(): axis = kwargs["axis"] @@ -255,8 +255,8 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): # endpoints of the data range # used by linear selector but not linear region end_points = ( - self.data()[:, 1].min() - padding, - self.data()[:, 1].max() + padding, + self.data.value[:, 1].min() - padding, + self.data.value[:, 1].max() + padding, ) else: offset = self.position_y @@ -273,8 +273,8 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): origin = (position_x + self.position_x, limits[0] - offset) end_points = ( - self.data()[:, 0].min() - padding, - self.data()[:, 0].max() + padding, + self.data.value[:, 0].min() - padding, + self.data.value[:, 0].max() + padding, ) # initial bounds are 20% of the limits range diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index eddf0fac3..d3f83d5e6 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -9,7 +9,7 @@ class ScatterGraphic(PositionsGraphic): - feature_events = {"data", "sizes", "colors", "cmap", "present"} + features = {"data", "sizes", "colors"}#, "cmap", "present"} def __init__( self, @@ -20,6 +20,7 @@ def __init__( cmap: str = None, cmap_values: np.ndarray | List = None, z_position: float = 0.0, + isolated_buffer: bool = True, *args, **kwargs, ): @@ -73,25 +74,35 @@ def __init__( Control the presence of the Graphic in the scene, set to ``True`` or ``False`` """ - self.data = PointsDataFeature(self, data) - n_datapoints = self.data().shape[0] + self._data = PointsDataFeature(data, isolated_buffer=isolated_buffer) + + n_datapoints = self._data.value.shape[0] if cmap is not None: colors = parse_cmap_values( n_colors=n_datapoints, cmap_name=cmap, cmap_values=cmap_values ) - self.colors = ColorFeature(self, colors, n_colors=n_datapoints, alpha=alpha) - self.cmap = CmapFeature( - self, self.colors(), cmap_name=cmap, cmap_values=cmap_values - ) + if isinstance(colors, ColorFeature): + self._colors = colors + self._colors._shared += 1 + else: + self._colors = ColorFeature( + colors, + n_colors=self._data.value.shape[0], + alpha=alpha, + ) + + # self.cmap = CmapFeature( + # self, self.colors(), cmap_name=cmap, cmap_values=cmap_values + # ) - self.sizes = PointsSizesFeature(self, sizes) + self._sizes = PointsSizesFeature(sizes, n_datapoints=n_datapoints) super().__init__(*args, **kwargs) world_object = pygfx.Points( pygfx.Geometry( - positions=self.data(), sizes=self.sizes(), colors=self.colors() + positions=self._data.buffer, sizes=self._sizes.buffer, colors=self._colors.buffer ), material=pygfx.PointsMaterial( color_mode="vertex", size_mode="vertex", pick_write=True diff --git a/tests/events.py b/tests/events.py new file mode 100644 index 000000000..57fde6886 --- /dev/null +++ b/tests/events.py @@ -0,0 +1,92 @@ +from functools import partial +import pytest +import numpy as np +from numpy import testing as npt +import pygfx + +import fastplotlib as fpl +from fastplotlib.graphics._features import FeatureEvent + + +def make_positions_data() -> np.ndarray: + xs = np.linspace(0, 10 * np.pi, 10) + ys = np.sin(xs) + return np.column_stack([xs, ys]) + + +def make_line_graphic() -> fpl.LineGraphic: + return fpl.LineGraphic(make_positions_data()) + + +def make_scatter_graphic() -> fpl.ScatterGraphic: + return fpl.ScatterGraphic(make_positions_data()) + + +event_instance: FeatureEvent = None + + +def event_handler(event): + global event_instance + event_instance = event + + +decorated_event_instance: FeatureEvent = None + + +@pytest.mark.parametrize("graphic", [make_line_graphic(), make_scatter_graphic()]) +def test_positions_data_event(graphic: fpl.LineGraphic | fpl.ScatterGraphic): + global decorated_event_instance + global event_instance + + value = np.cos(np.linspace(0, 10 * np.pi, 10))[3:8] + + info = { + "key": (slice(3, 8, None), 1), + "value": value + } + + expected = FeatureEvent(type="data", info=info) + + def validate(graphic, handler, expected_feature_event, event_to_test): + assert expected_feature_event.type == event_to_test.type + assert expected_feature_event.info["key"] == event_to_test.info["key"] + + npt.assert_almost_equal(expected_feature_event.info["value"], event_to_test.info["value"]) + + # should only have one event handler + assert graphic._event_handlers["data"] == {handler} + + # make sure wrappers are correct + wrapper_map = tuple(graphic._event_handler_wrappers["data"])[0] + assert wrapper_map[0] is handler + assert isinstance(wrapper_map[1], partial) + assert wrapper_map[1].func == graphic._handle_event + assert wrapper_map[1].args[0] is handler + + # test remove handler + graphic.remove_event_handler(handler, "data") + assert len(graphic._event_handlers["click"]) == 0 + assert len(graphic._event_handler_wrappers["click"]) == 0 + assert len(graphic.world_object._event_handlers["click"]) == 0 + + # reset data + graphic.data[:, :-1] = make_positions_data() + event_to_test = None + + # test decorated function + @graphic.add_event_handler("data") + def decorated_handler(event): + global decorated_event_instance + decorated_event_instance = event + + # test decorated + graphic.data[3:8, 1] = value + validate(graphic, decorated_handler, expected, decorated_event_instance) + + # test regular + graphic.add_event_handler(event_handler, "data") + graphic.data[3:8, 1] = value + + validate(graphic, event_handler, expected, event_instance) + + event_instance = None From 3a58feb207e59518f14f1722acc71990e39db6b5 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 21 May 2024 21:44:41 -0400 Subject: [PATCH 041/196] common features, WIP --- fastplotlib/graphics/_base.py | 81 +-------------- fastplotlib/graphics/_features/__init__.py | 9 +- fastplotlib/graphics/_features/_base.py | 31 ++++-- fastplotlib/graphics/_features/_common.py | 103 +++++++++++++++++++ fastplotlib/graphics/_features/_deleted.py | 41 -------- fastplotlib/graphics/_features/_present.py | 72 ------------- fastplotlib/graphics/_features/_thickness.py | 45 ++------ 7 files changed, 140 insertions(+), 242 deletions(-) create mode 100644 fastplotlib/graphics/_features/_common.py delete mode 100644 fastplotlib/graphics/_features/_deleted.py delete mode 100644 fastplotlib/graphics/_features/_present.py diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 2992cf875..48ca37e12 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -12,7 +12,7 @@ import pygfx -from ._features import GraphicFeature, PresentFeature, BufferManager, GraphicFeatureDescriptor, Deleted, PointsDataFeature, ColorFeature, PointsSizesFeature +from ._features import GraphicFeature, BufferManager, GraphicFeatureDescriptor, Deleted, PointsDataFeature, ColorFeature, PointsSizesFeature, Name, Offset, Rotation, Visible HexStr: TypeAlias = str @@ -56,8 +56,7 @@ class Graphic(BaseGraphic): def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) - # all graphics give off a feature event when deleted - cls.features = {*cls.features}#, "deleted"} + cls.features = {*cls.features, "name", "offset", "rotation", "visible", "deleted"} # graphic feature class attributes for f in cls.features: @@ -83,7 +82,7 @@ def __init__( if (name is not None) and (not isinstance(name, str)): raise TypeError("Graphic `name` must be of type ") - self._name = name + self._name = Name(name) self.metadata = metadata self.collection_index = collection_index self.registered_callbacks = dict() @@ -92,7 +91,7 @@ def __init__( # store hex id str of Graphic instance mem location self._fpl_address: HexStr = hex(id(self)) - # self.deleted = Deleted(self, False) + self._deleted = Deleted(False) self._plot_area = None @@ -102,24 +101,6 @@ def __init__( # maps callbacks to their partials self._event_handler_wrappers = defaultdict(set) - @property - def name(self) -> str | None: - """str name reference for this item""" - return self._name - - @name.setter - def name(self, name: str): - if self.name == name: - return - - if not isinstance(name, str): - raise TypeError("`Graphic` name must be of type ") - - if self._plot_area is not None: - self._plot_area._check_graphic_name_exists(name) - - self._name = name - @property def world_object(self) -> pygfx.WorldObject: """Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly.""" @@ -135,60 +116,6 @@ def detach_feature(self, feature: str): def attach_feature(self, feature: BufferManager): raise NotImplementedError - @property - def position(self) -> np.ndarray: - """position of the graphic, [x, y, z]""" - return self.world_object.world.position - - @property - def position_x(self) -> float: - """x-axis position of the graphic""" - return self.world_object.world.x - - @property - def position_y(self) -> float: - """y-axis position of the graphic""" - return self.world_object.world.y - - @property - def position_z(self) -> float: - """z-axis position of the graphic""" - return self.world_object.world.z - - @position.setter - def position(self, val): - self.world_object.world.position = val - - @position_x.setter - def position_x(self, val): - self.world_object.world.x = val - - @position_y.setter - def position_y(self, val): - self.world_object.world.y = val - - @position_z.setter - def position_z(self, val): - self.world_object.world.z = val - - @property - def rotation(self): - return self.world_object.local.rotation - - @rotation.setter - def rotation(self, val): - self.world_object.local.rotation = val - - @property - def visible(self) -> bool: - """Access or change the visibility.""" - return self.world_object.visible - - @visible.setter - def visible(self, v: bool): - """Access or change the visibility.""" - self.world_object.visible = v - @property def children(self) -> list[pygfx.WorldObject]: """Return the children of the WorldObject.""" diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index 535167ef6..d81f8b432 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -11,8 +11,7 @@ to_gpu_supported_dtype, ) from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature -from ._deleted import Deleted -# +from ._common import Name, Offset, Rotation, Visible, Deleted # __all__ = [ # "ColorFeature", # "CmapFeature", @@ -32,12 +31,6 @@ # "Deleted", # ] -class PresentFeature: - pass - -class Deleted: - pass - class CmapFeature: pass diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index dd2e060ba..573bf7d83 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -75,12 +75,14 @@ class GraphicFeature: def __init__(self, **kwargs): self._event_handlers = list() self._block_events = False - self.collection_index: int = None @property def value(self) -> Any: raise NotImplemented + def set_value(self, graphic, value: float): + raise NotImplementedError + def block_events(self, val: bool): """ Block all events from this feature @@ -183,6 +185,10 @@ def __init__( def value(self) -> NDArray: return self.buffer.data + def set_value(self, graphic, value): + """Sets values on entire array""" + self[:] = value + @property def buffer(self) -> pygfx.Buffer | pygfx.Texture: return self._buffer @@ -289,16 +295,23 @@ def __repr__(self): class GraphicFeatureDescriptor: - def __init__(self, name): - self.name = name + def __init__(self, feature_name): + self.feature_name = feature_name def _get_feature(self, instance): - feature: GraphicFeature = getattr(instance, f"_{self.name}") + feature: GraphicFeature = getattr(instance, f"_{self.feature_name}") return feature - def __get__(self, instance, owner): - return self._get_feature(instance) + def __get__(self, graphic, owner): + f = self._get_feature(graphic) + if isinstance(f, BufferManager): + return f + else: + return f.value - def __set__(self, obj, value): - feature = self._get_feature(obj) - feature[:] = value + def __set__(self, graphic, value): + feature = self._get_feature(graphic) + if isinstance(feature, BufferManager): + feature[:] = value + else: + feature.set_value(graphic, value) diff --git a/fastplotlib/graphics/_features/_common.py b/fastplotlib/graphics/_features/_common.py new file mode 100644 index 000000000..cf8186966 --- /dev/null +++ b/fastplotlib/graphics/_features/_common.py @@ -0,0 +1,103 @@ +from ._base import GraphicFeature, FeatureEvent + + +class Name(GraphicFeature): + """Graphic name""" + def __init__(self, value: str): + self._value = value + super().__init__() + + @property + def value(self) -> str: + return self._value + + def set_value(self, graphic, value: bool): + if not isinstance(value, str): + raise TypeError("`Graphic` name must be of type ") + + if graphic._plot_area is not None: + graphic._plot_area._check_graphic_name_exists(value) + + self._value = value + + event = FeatureEvent(type="name", info={"value": value}) + self._call_event_handlers(event) + + +class Offset(GraphicFeature): + """Offset position of the graphic, [x, y, z]""" + def __init__(self, value: tuple[float, float, float]): + self._value = value + super().__init__() + + @property + def value(self) -> tuple[float, float, float]: + return self._value + + def set_value(self, graphic, value: tuple[float, float, float]): + if not len(value) == 3: + raise ValueError("offset must be a list, tuple, or array of 3 float values") + + graphic.position = value + self._value = value + + event = FeatureEvent(type="offset", info={"value": value}) + self._call_event_handlers(event) + + +class Rotation(GraphicFeature): + """Graphic rotation quaternion""" + def __init__(self, value: tuple[float, float, float, float]): + self._value = value + super().__init__() + + @property + def value(self) -> tuple[float, float, float, float]: + return self._value + + def set_value(self, graphic, value: tuple[float, float, float, float]): + if not len(value) == 4: + raise ValueError("rotation must be a list, tuple, or array of 4 float values" + "representing a quaternion") + + graphic.rotation = value + self._value = value + + event = FeatureEvent(type="rotation", info={"value": value}) + self._call_event_handlers(event) + + +class Visible(GraphicFeature): + """Access or change the visibility.""" + def __init__(self, value: bool): + self._value = value + super().__init__() + + @property + def value(self) -> bool: + return self._value + + def set_value(self, graphic, value: bool): + graphic.world_object.visible = value + self._value = value + + event = FeatureEvent(type="visible", info={"value": value}) + self._call_event_handlers(event) + + +class Deleted(GraphicFeature): + """ + Used when a graphic is deleted, triggers events that can be useful to indicate this graphic has been deleted + """ + def __init__(self, value: bool): + self._value = value + super().__init__() + + @property + def value(self) -> bool: + return self._value + + def set_value(self, graphic, value: bool): + self._value = value + event = FeatureEvent(type="deleted", info={"value": value}) + self._call_event_handlers(event) diff --git a/fastplotlib/graphics/_features/_deleted.py b/fastplotlib/graphics/_features/_deleted.py deleted file mode 100644 index 7900385eb..000000000 --- a/fastplotlib/graphics/_features/_deleted.py +++ /dev/null @@ -1,41 +0,0 @@ -from ._base import GraphicFeature, FeatureEvent - - -class Deleted(GraphicFeature): - """ - Used when a graphic is deleted, triggers events that can be useful to indicate this graphic has been deleted - - **event pick info:** - - ==================== ======================== ========================================================================= - key type description - ==================== ======================== ========================================================================= - "collection-index" int the index of the graphic within the collection that triggered the event - "world_object" pygfx.WorldObject world object - ==================== ======================== ========================================================================= - """ - - def __init__(self, parent, value: bool): - super().__init__(parent, value) - - def _set(self, value: bool): - value = self._parse_set_value(value) - self._feature_changed(key=None, new_data=value) - - def _feature_changed(self, key, new_data): - # this is a non-indexable feature so key=None - - pick_info = { - "index": None, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="deleted", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"DeletedFeature for {self._parent}" - return s diff --git a/fastplotlib/graphics/_features/_present.py b/fastplotlib/graphics/_features/_present.py deleted file mode 100644 index a73d66523..000000000 --- a/fastplotlib/graphics/_features/_present.py +++ /dev/null @@ -1,72 +0,0 @@ -from pygfx import Scene, Group - -from ._base import GraphicFeature, FeatureEvent - - -class PresentFeature(GraphicFeature): - """ - Toggles if the object is present in the scene, different from visible. - Useful for computing bounding boxes from the Scene to only include graphics - that are present. - - **event pick info:** - - ==================== ======================== ========================================================================= - key type description - ==================== ======================== ========================================================================= - "index" ``None`` not used - "new_data" ``bool`` new data, ``True`` or ``False`` - "collection-index" int the index of the graphic within the collection that triggered the event - "world_object" pygfx.WorldObject world object - ==================== ======================== ========================================================================= - """ - - def __init__(self, parent, present: bool = True, collection_index: int = False): - self._scene = None - super().__init__(parent, present, collection_index) - - def _set(self, present: bool): - present = self._parse_set_value(present) - - i = 0 - wo = self._parent.world_object - while not isinstance(self._scene, (Group, Scene)): - wo_parent = wo.parent - self._scene = wo_parent - wo = wo_parent - i += 1 - - if i > 100: - raise RecursionError( - "Exceeded scene graph depth threshold, cannot find Scene associated with" - "this graphic." - ) - - if present: - if self._parent.world_object not in self._scene.children: - self._scene.add(self._parent.world_object) - - else: - if self._parent.world_object in self._scene.children: - self._scene.remove(self._parent.world_object) - - self._data = present - self._feature_changed(key=None, new_data=present) - - def _feature_changed(self, key, new_data): - # this is a non-indexable feature so key=None - - pick_info = { - "index": None, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="present", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"PresentFeature for {self._parent}, call `.present()` to get values" - return s diff --git a/fastplotlib/graphics/_features/_thickness.py b/fastplotlib/graphics/_features/_thickness.py index fc90ef96f..d13c2b727 100644 --- a/fastplotlib/graphics/_features/_thickness.py +++ b/fastplotlib/graphics/_features/_thickness.py @@ -4,43 +4,18 @@ class ThicknessFeature(GraphicFeature): """ Used by Line graphics for line material thickness. - - **event pick info:** - - ==================== ======================== ========================================================================= - key type description - ==================== ======================== ========================================================================= - "index" ``None`` not used - "new_data" ``float`` new thickness value - "collection-index" int the index of the graphic within the collection that triggered the event - "world_object" pygfx.WorldObject world object - ==================== ======================== ========================================================================= """ - def __init__(self, parent, thickness: float): - self._scene = None - super().__init__(parent, thickness) - - def _set(self, value: float): - value = self._parse_set_value(value) - - self._parent.world_object.material.thickness = value - self._feature_changed(key=None, new_data=value) - - def _feature_changed(self, key, new_data): - # this is a non-indexable feature so key=None - - pick_info = { - "index": None, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - } + def __init__(self, thickness: float): + self._value = thickness + super().__init__() - event_data = FeatureEvent(type="thickness", pick_info=pick_info) + @property + def value(self) -> float: + return self._value - self._call_event_handlers(event_data) + def set_value(self, parent, value: float): + parent.world_object.material.thickness = value - def __repr__(self) -> str: - s = f"ThicknessFeature for {self._parent}, call `.thickness()` to get value" - return s + event = FeatureEvent("thickness", {"value": value}) + self._call_event_handlers(event) From adcc13fb8bbfd9bcb28fcea1a3fb1a55cd1884b2 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 22 May 2024 04:00:50 -0400 Subject: [PATCH 042/196] regular features and refactor line and scatter into positions graphic --- fastplotlib/graphics/_base.py | 164 +++++++++++++++--- fastplotlib/graphics/_features/__init__.py | 5 +- fastplotlib/graphics/_features/_base.py | 25 --- fastplotlib/graphics/_features/_common.py | 2 +- fastplotlib/graphics/_features/_data.py | 52 ------ .../{_colors.py => _positions_graphics.py} | 67 ++++++- fastplotlib/graphics/line.py | 50 +++--- tests/test_colors_buffer_manager.py | 8 +- tests/test_points_data_buffer_manager.py | 8 +- 9 files changed, 238 insertions(+), 143 deletions(-) rename fastplotlib/graphics/_features/{_colors.py => _positions_graphics.py} (80%) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 48ca37e12..dc6954160 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -12,8 +12,8 @@ import pygfx -from ._features import GraphicFeature, BufferManager, GraphicFeatureDescriptor, Deleted, PointsDataFeature, ColorFeature, PointsSizesFeature, Name, Offset, Rotation, Visible - +from ._features import GraphicFeature, BufferManager, Deleted, VertexPositions, VertexColors, PointsSizesFeature, Name, Offset, Rotation, Visible, UniformColor +from ..utils import parse_cmap_values HexStr: TypeAlias = str @@ -38,9 +38,56 @@ ] -class BaseGraphic: +class Graphic: + features = {} + + @property + def name(self) -> str | None: + """Graphic name""" + return self._name.value + + @name.setter + def name(self, value: str): + self._name.set_value(self, value) + + @property + def offset(self) -> tuple: + """Offset position of the graphic, [x, y, z]""" + return self._offset.value + + @offset.setter + def offset(self, value: tuple[float, float, float]): + self._offset.set_value(self, value) + + @property + def rotation(self) -> np.ndarray: + """Orientation of the graphic as a quaternion""" + return self._rotation.value + + @rotation.setter + def rotation(self, value: tuple[float, float, float, float]): + self._rotation.set_value(self, value) + + @property + def visible(self) -> bool: + """Whether the graphic is visible""" + return self._visible.value + + @visible.setter + def visible(self, value: bool): + self._visible.set_value(self, value) + + @property + def deleted(self) -> bool: + """used to emit an event after the graphic is deleted""" + return self._deleted.value + + @deleted.setter + def deleted(self, value: bool): + self._deleted.set_value(self, value) + def __init_subclass__(cls, **kwargs): - """set the type of the graphic in lower case like "image", "line_collection", etc.""" + # set the type of the graphic in lower case like "image", "line_collection", etc. cls.type = ( cls.__name__.lower() .replace("graphic", "") @@ -48,23 +95,14 @@ def __init_subclass__(cls, **kwargs): .replace("stack", "_stack") ) - super().__init_subclass__(**kwargs) - - -class Graphic(BaseGraphic): - features = {} - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) + # set of all features cls.features = {*cls.features, "name", "offset", "rotation", "visible", "deleted"} - - # graphic feature class attributes - for f in cls.features: - setattr(cls, f, GraphicFeatureDescriptor(f)) + super().__init_subclass__(**kwargs) def __init__( self, name: str = None, + offset: tuple[float] = (0., 0., 0.), metadata: Any = None, collection_index: int = None, ): @@ -82,7 +120,6 @@ def __init__( if (name is not None) and (not isinstance(name, str)): raise TypeError("Graphic `name` must be of type ") - self._name = Name(name) self.metadata = metadata self.collection_index = collection_index self.registered_callbacks = dict() @@ -91,8 +128,6 @@ def __init__( # store hex id str of Graphic instance mem location self._fpl_address: HexStr = hex(id(self)) - self._deleted = Deleted(False) - self._plot_area = None # event handlers @@ -101,6 +136,13 @@ def __init__( # maps callbacks to their partials self._event_handler_wrappers = defaultdict(set) + # all the common features + self._name = Name(name) + self._deleted = Deleted(False) + self._rotation = None # set later when world object is set + self._offset = Offset(offset) + self._visible = Visible(True) + @property def world_object(self) -> pygfx.WorldObject: """Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly.""" @@ -110,6 +152,8 @@ def world_object(self) -> pygfx.WorldObject: def _set_world_object(self, wo: pygfx.WorldObject): WORLD_OBJECTS[self._fpl_address] = wo + self._rotation = Rotation(self.world_object.world.rotation[:]) + def detach_feature(self, feature: str): raise NotImplementedError @@ -203,7 +247,8 @@ def _handle_event(self, callback, event: pygfx.Event): # for feature events event._target = self.world_object - callback(event) + with log_exception(f"Error during handling {event.type} event"): + callback(event) def remove_event_handler(self, callback, *types): # remove from our record first @@ -315,6 +360,77 @@ def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"): class PositionsGraphic(Graphic): """Base class for LineGraphic and ScatterGraphic""" + @property + def data(self) -> VertexPositions: + """Get or set the vertex positions data""" + return self._data + + @data.setter + def data(self, value): + self._data[:] = value + + @property + def colors(self) -> VertexColors | pygfx.Color: + """Get or set the colors data""" + if isinstance(self._colors, VertexColors): + return self._colors + + elif isinstance(self._colors, UniformColor): + return self._colors.value + + @colors.setter + def colors(self, value): + if isinstance(self._colors, VertexColors): + self._colors[:] = value + + elif isinstance(self._colors, UniformColor): + self._colors.set_value(self, value) + + def __init__( + self, + data: Any, + colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", + uniform_colors: bool = False, + alpha: float = 1.0, + cmap: str = None, + cmap_values: np.ndarray = None, + isolated_buffer: bool = True, + *args, + **kwargs, + ): + self._data = VertexPositions(data, isolated_buffer=isolated_buffer) + + if cmap is not None: + if uniform_colors: + raise TypeError( + "Cannot use cmap if uniform_colors=True" + ) + + n_datapoints = self._data.value.shape[0] + + colors = parse_cmap_values( + n_colors=n_datapoints, cmap_name=cmap, cmap_values=cmap_values + ) + + if isinstance(colors, VertexColors): + if uniform_colors: + raise TypeError( + "Cannot use vertex colors from existing instance if uniform_colors=True" + ) + self._colors = colors + self._colors._shared += 1 + else: + if uniform_colors: + self._colors = UniformColor(colors) + else: + self._colors = VertexColors( + colors, + n_colors=self._data.value.shape[0], + alpha=alpha, + ) + + super().__init__(*args, **kwargs) + def detach_feature(self, feature: str): if not isinstance(feature, str): raise TypeError @@ -323,7 +439,7 @@ def detach_feature(self, feature: str): if f.shared == 0: raise BufferError("Cannot detach an independent buffer") - if feature == "colors": + if feature == "colors" and isinstance(feature, VertexColors): self._colors._buffer = pygfx.Buffer(self._colors.value.copy()) self.world_object.geometry.colors = self._colors.buffer self._colors._shared -= 1 @@ -338,8 +454,8 @@ def detach_feature(self, feature: str): self.world_object.geometry.positions = self._sizes.buffer self._sizes._shared -= 1 - def attach_feature(self, feature: PointsDataFeature | ColorFeature | PointsSizesFeature): - if isinstance(feature, PointsDataFeature): + def attach_feature(self, feature: VertexPositions | VertexColors | PointsSizesFeature): + if isinstance(feature, VertexPositions): # TODO: check if this causes a memory leak self._data._shared -= 1 @@ -347,7 +463,7 @@ def attach_feature(self, feature: PointsDataFeature | ColorFeature | PointsSizes self._data._shared += 1 self.world_object.geometry.positions = self._data.buffer - elif isinstance(feature, ColorFeature): + elif isinstance(feature, VertexColors): self._colors._shared -= 1 self._colors = feature diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index d81f8b432..63b438355 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -1,12 +1,11 @@ -from ._colors import ColorFeature#, CmapFeature, ImageCmapFeature, HeatmapCmapFeature -from ._data import PointsDataFeature#, ImageDataFeature, HeatmapDataFeature +from ._positions_graphics import VertexColors, UniformColor, \ + VertexPositions # , CmapFeature, ImageCmapFeature, HeatmapCmapFeature from ._sizes import PointsSizesFeature # from ._present import PresentFeature # from ._thickness import ThicknessFeature from ._base import ( GraphicFeature, BufferManager, - GraphicFeatureDescriptor, FeatureEvent, to_gpu_supported_dtype, ) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 573bf7d83..06e53b163 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -1,5 +1,3 @@ -from abc import abstractmethod -from inspect import getfullargspec from warnings import warn from typing import Any, Literal @@ -292,26 +290,3 @@ def _emit_event(self, type: str, key, value): def __repr__(self): return f"{self.__class__.__name__} buffer data:\n" \ f"{self.value.__repr__()}" - - -class GraphicFeatureDescriptor: - def __init__(self, feature_name): - self.feature_name = feature_name - - def _get_feature(self, instance): - feature: GraphicFeature = getattr(instance, f"_{self.feature_name}") - return feature - - def __get__(self, graphic, owner): - f = self._get_feature(graphic) - if isinstance(f, BufferManager): - return f - else: - return f.value - - def __set__(self, graphic, value): - feature = self._get_feature(graphic) - if isinstance(feature, BufferManager): - feature[:] = value - else: - feature.set_value(graphic, value) diff --git a/fastplotlib/graphics/_features/_common.py b/fastplotlib/graphics/_features/_common.py index cf8186966..aa44bde5a 100644 --- a/fastplotlib/graphics/_features/_common.py +++ b/fastplotlib/graphics/_features/_common.py @@ -11,7 +11,7 @@ def __init__(self, value: str): def value(self) -> str: return self._value - def set_value(self, graphic, value: bool): + def set_value(self, graphic, value: str): if not isinstance(value, str): raise TypeError("`Graphic` name must be of type ") diff --git a/fastplotlib/graphics/_features/_data.py b/fastplotlib/graphics/_features/_data.py index e7a422d93..d0f4bd4a4 100644 --- a/fastplotlib/graphics/_features/_data.py +++ b/fastplotlib/graphics/_features/_data.py @@ -1,55 +1,3 @@ -from typing import * - -import numpy as np - -import pygfx - -from ._base import ( - BufferManager, - FeatureEvent, - to_gpu_supported_dtype, -) - - -class PointsDataFeature(BufferManager): - """ - Access to the vertex buffer data shown in the graphic. - Supports fancy indexing if the data array also supports it. - """ - - def __init__(self, data: Any, isolated_buffer: bool = True): - data = self._fix_data(data) - super().__init__(data, isolated_buffer=isolated_buffer) - - def _fix_data(self, data): - # data = to_gpu_supported_dtype(data) - - if data.ndim == 1: - # if user provides a 1D array, assume these are y-values - data = np.column_stack([np.arange(data.size, dtype=data.dtype), data]) - - if data.shape[1] != 3: - if data.shape[1] != 2: - raise ValueError(f"Must pass 1D, 2D or 3D data") - - # zeros for z - zs = np.zeros(data.shape[0], dtype=data.dtype) - - # column stack [x, y, z] to make data of shape [n_points, 3] - data = np.column_stack([data[:, 0], data[:, 1], zs]) - - return to_gpu_supported_dtype(data) - - def __setitem__(self, key: int | slice | range | np.ndarray[int | bool] | tuple[slice, ...] | tuple[range, ...], value): - # directly use the key to slice the buffer - self.buffer.data[key] = value - - # _update_range handles parsing the key to - # determine offset and size for GPU upload - self._update_range(key) - - self._emit_event("data", key, value) - # # class ImageDataFeature(GraphicFeatureIndexable): # """ diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_positions_graphics.py similarity index 80% rename from fastplotlib/graphics/_features/_colors.py rename to fastplotlib/graphics/_features/_positions_graphics.py index 4fd40ac0e..2e73294a7 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -1,5 +1,8 @@ +from typing import Any + import numpy as np import pygfx +from ._base import BufferManager, to_gpu_supported_dtype from ...utils import ( make_colors, @@ -15,14 +18,14 @@ from .utils import parse_colors -class ColorFeature(BufferManager): +class VertexColors(BufferManager): """ Manages the color buffer for :class:`LineGraphic` or :class:`ScatterGraphic` """ def __init__( self, - colors: str | np.ndarray | tuple[float, float, float, float] | list[str] | list[float] | int | float, + colors: str | np.ndarray | tuple[float] | list[float] | list[str], n_colors: int, alpha: float = None, isolated_buffer: bool = True, @@ -50,7 +53,7 @@ def __init__( def __setitem__( self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], - value: str | np.ndarray | tuple[float, float, float, float] | list[str] | list[float] | int | float + value: str | np.ndarray | tuple[float] | list[float] | list[str] ): if isinstance(key, tuple): # directly setting RGBA values for points, we do no parsing @@ -105,6 +108,64 @@ def __setitem__( self._emit_event("colors", key, value) +class UniformColor(GraphicFeature): + def __init__(self, value: str | np.ndarray | tuple | list | pygfx.Color): + self._value = pygfx.Color(value) + super().__init__() + + @property + def value(self) -> pygfx.Color: + return self._value + + def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Color): + value = pygfx.Color(value) + graphic.world_object.material.color = value + self._value = value + + event = FeatureEvent(type="colors", info={"value": value}) + self._call_event_handlers(event) + + +class VertexPositions(BufferManager): + """ + Manages the vertex positions buffer shown in the graphic. + Supports fancy indexing if the data array also supports it. + """ + + def __init__(self, data: Any, isolated_buffer: bool = True): + data = self._fix_data(data) + super().__init__(data, isolated_buffer=isolated_buffer) + + def _fix_data(self, data): + # data = to_gpu_supported_dtype(data) + + if data.ndim == 1: + # if user provides a 1D array, assume these are y-values + data = np.column_stack([np.arange(data.size, dtype=data.dtype), data]) + + if data.shape[1] != 3: + if data.shape[1] != 2: + raise ValueError(f"Must pass 1D, 2D or 3D data") + + # zeros for z + zs = np.zeros(data.shape[0], dtype=data.dtype) + + # column stack [x, y, z] to make data of shape [n_points, 3] + data = np.column_stack([data[:, 0], data[:, 1], zs]) + + return to_gpu_supported_dtype(data) + + def __setitem__(self, key: int | slice | range | np.ndarray[int | bool] | tuple[slice, ...] | tuple[range, ...], value): + # directly use the key to slice the buffer + self.buffer.data[key] = value + + # _update_range handles parsing the key to + # determine offset and size for GPU upload + self._update_range(key) + + self._emit_event("data", key, value) + + # class CmapFeature(ColorFeature): # """ # Indexable colormap feature, mostly wraps colors and just provides a way to set colormaps. diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 6685e38a6..524618767 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -5,9 +5,7 @@ import pygfx -from ..utils import parse_cmap_values from ._base import PositionsGraphic, Interaction, PreviouslyModifiedData -from ._features import GraphicFeatureDescriptor, PointsDataFeature, ColorFeature#, CmapFeature, ThicknessFeature from .selectors import LinearRegionSelector, LinearSelector @@ -19,6 +17,7 @@ def __init__( data: Any, thickness: float = 2.0, colors: str | np.ndarray | Iterable = "w", + uniform_colors: bool = False, alpha: float = 1.0, cmap: str = None, cmap_values: np.ndarray | Iterable = None, @@ -83,42 +82,39 @@ def __init__( """ - self._data = PointsDataFeature(data, isolated_buffer=isolated_buffer) - - if cmap is not None: - n_datapoints = self._data.value.shape[0] - - colors = parse_cmap_values( - n_colors=n_datapoints, cmap_name=cmap, cmap_values=cmap_values - ) - - if isinstance(colors, ColorFeature): - self._colors = colors - self._colors._shared += 1 - else: - self._colors = ColorFeature( - colors, - n_colors=self._data.value.shape[0], - alpha=alpha, - ) - # self.cmap = CmapFeature( # self, self.colors(), cmap_name=cmap, cmap_values=cmap_values # ) - super().__init__(*args, **kwargs) + super().__init__( + data=data, + colors=colors, + uniform_colors=uniform_colors, + alpha=alpha, + cmap=cmap, + cmap_values=cmap_values, + isolated_buffer=isolated_buffer, + *args, + **kwargs + ) if thickness < 1.1: - material = pygfx.LineThinMaterial + MaterialCls = pygfx.LineThinMaterial + else: + MaterialCls = pygfx.LineMaterial + + if uniform_colors: + geometry = pygfx.Geometry(positions=self._data.buffer) + material = MaterialCls(thickness=thickness, color_mode="uniform", pick_write=True) else: - material = pygfx.LineMaterial + material = MaterialCls(thickness=thickness, color_mode="vertex", pick_write=True) + geometry = pygfx.Geometry(positions=self._data.buffer, colors=self._colors.buffer) # self.thickness = ThicknessFeature(self, thickness) world_object: pygfx.Line = pygfx.Line( - # self.data.feature_data because data is a Buffer - geometry=pygfx.Geometry(positions=self._data.buffer, colors=self._colors.buffer), - material=material(thickness=thickness, color_mode="vertex", pick_write=True), + geometry=geometry, + material=material ) self._set_world_object(world_object) diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py index 884a70eed..3479fcd59 100644 --- a/tests/test_colors_buffer_manager.py +++ b/tests/test_colors_buffer_manager.py @@ -4,7 +4,7 @@ import pygfx -from fastplotlib.graphics._features import ColorFeature +from fastplotlib.graphics._features import VertexColors from .utils import generate_slice_indices, assert_pending_uploads @@ -19,14 +19,14 @@ def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple]: return [s, a, l, t] -def make_colors_buffer() -> ColorFeature: - colors = ColorFeature(colors="w", n_colors=10) +def make_colors_buffer() -> VertexColors: + colors = VertexColors(colors="w", n_colors=10) return colors @pytest.mark.parametrize("color_input", [*generate_color_inputs("r"), *generate_color_inputs("g"), *generate_color_inputs("b")]) def test_create_buffer(color_input): - colors = ColorFeature(colors=color_input, n_colors=10) + colors = VertexColors(colors=color_input, n_colors=10) truth = np.repeat([pygfx.Color(color_input)], 10, axis=0) npt.assert_almost_equal(colors[:], truth) diff --git a/tests/test_points_data_buffer_manager.py b/tests/test_points_data_buffer_manager.py index eac03664a..86181adfa 100644 --- a/tests/test_points_data_buffer_manager.py +++ b/tests/test_points_data_buffer_manager.py @@ -2,7 +2,7 @@ from numpy import testing as npt import pytest -from fastplotlib.graphics._features import PointsDataFeature +from fastplotlib.graphics._features import VertexPositions from .utils import generate_slice_indices, assert_pending_uploads @@ -31,7 +31,7 @@ def generate_data(inputs: str) -> np.ndarray: @pytest.mark.parametrize("data", [generate_data(v) for v in ["y", "xy", "xyz"]]) def test_create_buffer(data): - points_data = PointsDataFeature(data) + points_data = VertexPositions(data) if data.ndim == 1: # only y-vals specified @@ -53,7 +53,7 @@ def test_create_buffer(data): def test_int(): data = generate_data("xyz") # test setting single points - points = PointsDataFeature(data) + points = VertexPositions(data) # set all x, y, z points, create a kink in the spiral points[2] = 1. @@ -89,7 +89,7 @@ def test_slice(slice_method: dict, test_axis: str): size = slice_method["size"] others = slice_method["others"] - points = PointsDataFeature(data) + points = VertexPositions(data) # TODO: placeholder until I make a testing figure where we draw frames only on call points.buffer._gfx_pending_uploads.clear() From 762fcbbe031d1dd6b5a9bc5590292a31cf80685f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 22 May 2024 04:23:47 -0400 Subject: [PATCH 043/196] uniform sizes --- .../graphics/_features/_positions_graphics.py | 18 +++++ fastplotlib/graphics/scatter.py | 71 ++++++++++--------- 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index 2e73294a7..d25eb64a8 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -126,6 +126,24 @@ def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Colo self._call_event_handlers(event) +class UniformSizes(GraphicFeature): + def __init__(self, value: int | float): + self._value = pygfx.Color(value) + super().__init__() + + @property + def value(self) -> pygfx.Color: + return self._value + + def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Color): + value = pygfx.Color(value) + graphic.world_object.material.color = value + self._value = value + + event = FeatureEvent(type="colors", info={"value": value}) + self._call_event_handlers(event) + + class VertexPositions(BufferManager): """ Manages the vertex positions buffer shown in the graphic. diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index d3f83d5e6..9d4380faa 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -5,7 +5,7 @@ from ..utils import parse_cmap_values from ._base import PositionsGraphic -from ._features import PointsDataFeature, ColorFeature, CmapFeature, PointsSizesFeature +from ._features import CmapFeature, PointsSizesFeature class ScatterGraphic(PositionsGraphic): @@ -13,14 +13,15 @@ class ScatterGraphic(PositionsGraphic): def __init__( self, - data: np.ndarray, - sizes: float | np.ndarray | Iterable[float] = 1, - colors: str | np.ndarray | Iterable[str] = "w", + data: Any, + colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", + uniform_colors: bool = False, alpha: float = 1.0, cmap: str = None, - cmap_values: np.ndarray | List = None, - z_position: float = 0.0, + cmap_values: np.ndarray = None, isolated_buffer: bool = True, + sizes: float | np.ndarray | Iterable[float] = 1, + uniform_sizes: bool = False, *args, **kwargs, ): @@ -74,41 +75,43 @@ def __init__( Control the presence of the Graphic in the scene, set to ``True`` or ``False`` """ - self._data = PointsDataFeature(data, isolated_buffer=isolated_buffer) - - n_datapoints = self._data.value.shape[0] - - if cmap is not None: - colors = parse_cmap_values( - n_colors=n_datapoints, cmap_name=cmap, cmap_values=cmap_values - ) - - if isinstance(colors, ColorFeature): - self._colors = colors - self._colors._shared += 1 - else: - self._colors = ColorFeature( - colors, - n_colors=self._data.value.shape[0], - alpha=alpha, - ) - # self.cmap = CmapFeature( # self, self.colors(), cmap_name=cmap, cmap_values=cmap_values # ) + super().__init__( + data=data, + colors=colors, + uniform_colors=uniform_colors, + alpha=alpha, + cmap=cmap, + cmap_values=cmap_values, + isolated_buffer=isolated_buffer, + *args, + **kwargs + ) + + n_datapoints = self.data.value.shape[0] self._sizes = PointsSizesFeature(sizes, n_datapoints=n_datapoints) - super().__init__(*args, **kwargs) + + geo_kwargs = {"positions": self._data.buffer} + material_kwargs = {"pick_write": True} + + if uniform_colors: + material_kwargs["color_mode"] = "uniform" + else: + material_kwargs["color_mode"] = "vertex" + geo_kwargs["colors"] = self._colors.buffer + + if uniform_sizes: + material_kwargs["size_mode"] = "uniform" + else: + material_kwargs["size_mode"] = "vertex" + geo_kwargs["sizes"] = self._sizes.buffer world_object = pygfx.Points( - pygfx.Geometry( - positions=self._data.buffer, sizes=self._sizes.buffer, colors=self._colors.buffer - ), - material=pygfx.PointsMaterial( - color_mode="vertex", size_mode="vertex", pick_write=True - ), + pygfx.Geometry(**geo_kwargs), + material=pygfx.PointsMaterial(**material_kwargs), ) self._set_world_object(world_object) - - self.position_z = z_position From 3017b7e06942b4ab72235b1289e9a912a4dcbdf6 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 22 May 2024 04:37:01 -0400 Subject: [PATCH 044/196] implement sizes and uniform size for scatter --- fastplotlib/graphics/_features/__init__.py | 4 +- .../graphics/_features/_positions_graphics.py | 61 ++++++++++++-- fastplotlib/graphics/_features/_sizes.py | 80 ------------------- fastplotlib/graphics/scatter.py | 25 +++++- 4 files changed, 79 insertions(+), 91 deletions(-) diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index 63b438355..b2a32dd47 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -1,6 +1,4 @@ -from ._positions_graphics import VertexColors, UniformColor, \ - VertexPositions # , CmapFeature, ImageCmapFeature, HeatmapCmapFeature -from ._sizes import PointsSizesFeature +from ._positions_graphics import VertexColors, UniformColor, UniformSizes, VertexPositions, PointsSizesFeature # , CmapFeature, ImageCmapFeature, HeatmapCmapFeature # from ._present import PresentFeature # from ._thickness import ThicknessFeature from ._base import ( diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index d25eb64a8..f32968045 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -2,7 +2,6 @@ import numpy as np import pygfx -from ._base import BufferManager, to_gpu_supported_dtype from ...utils import ( make_colors, @@ -14,6 +13,7 @@ GraphicFeature, BufferManager, FeatureEvent, + to_gpu_supported_dtype, ) from .utils import parse_colors @@ -128,19 +128,19 @@ def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Colo class UniformSizes(GraphicFeature): def __init__(self, value: int | float): - self._value = pygfx.Color(value) + self._value = float(value) super().__init__() @property - def value(self) -> pygfx.Color: + def value(self) -> float: return self._value def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Color): value = pygfx.Color(value) - graphic.world_object.material.color = value + graphic.world_object.material.size = value self._value = value - event = FeatureEvent(type="colors", info={"value": value}) + event = FeatureEvent(type="sizes", info={"value": value}) self._call_event_handlers(event) @@ -184,6 +184,57 @@ def __setitem__(self, key: int | slice | range | np.ndarray[int | bool] | tuple[ self._emit_event("data", key, value) +class PointsSizesFeature(BufferManager): + """ + Access to the vertex buffer data shown in the graphic. + Supports fancy indexing if the data array also supports it. + """ + + def __init__( + self, + sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], + n_datapoints: int, + isolated_buffer: bool = True + ): + sizes = self._fix_sizes(sizes, n_datapoints) + super().__init__(data=sizes, isolated_buffer=isolated_buffer) + + def _fix_sizes(self, sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], n_datapoints: int): + if np.issubdtype(type(sizes), np.number): + # single value given + sizes = np.full( + n_datapoints, sizes, dtype=np.float32 + ) # force it into a float to avoid weird gpu errors + + elif isinstance( + sizes, (np.ndarray, tuple, list) + ): # if it's not a ndarray already, make it one + sizes = np.asarray(sizes, dtype=np.float32) # read it in as a numpy.float32 + if (sizes.ndim != 1) or (sizes.size != n_datapoints): + raise ValueError( + f"sequence of `sizes` must be 1 dimensional with " + f"the same length as the number of datapoints" + ) + + else: + raise TypeError("sizes must be a single , , or a sequence (array, list, tuple) of int" + "or float with the length equal to the number of datapoints") + + if np.count_nonzero(sizes < 0) > 1: + raise ValueError( + "All sizes must be positive numbers greater than or equal to 0.0." + ) + + return sizes + + def __setitem__(self, key, value): + # this is a very simple 1D buffer, no parsing required, directly set buffer + self.buffer.data[key] = value + self._update_range(key) + + self._emit_event("sizes", key, value) + + # class CmapFeature(ColorFeature): # """ # Indexable colormap feature, mostly wraps colors and just provides a way to set colormaps. diff --git a/fastplotlib/graphics/_features/_sizes.py b/fastplotlib/graphics/_features/_sizes.py index b45474e5e..b28b04f64 100644 --- a/fastplotlib/graphics/_features/_sizes.py +++ b/fastplotlib/graphics/_features/_sizes.py @@ -1,83 +1,3 @@ -import numpy as np -from ._base import ( - BufferManager, - FeatureEvent, - to_gpu_supported_dtype, -) -class PointsSizesFeature(BufferManager): - """ - Access to the vertex buffer data shown in the graphic. - Supports fancy indexing if the data array also supports it. - """ - - def __init__( - self, - sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], - n_datapoints: int, - isolated_buffer: bool = True - ): - sizes = self._fix_sizes(sizes, n_datapoints) - super().__init__(data=sizes, isolated_buffer=isolated_buffer) - - def _fix_sizes(self, sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], n_datapoints: int): - if np.issubdtype(type(sizes), np.number): - # single value given - sizes = np.full( - n_datapoints, sizes, dtype=np.float32 - ) # force it into a float to avoid weird gpu errors - - elif isinstance( - sizes, (np.ndarray, tuple, list) - ): # if it's not a ndarray already, make it one - sizes = np.asarray(sizes, dtype=np.float32) # read it in as a numpy.float32 - if (sizes.ndim != 1) or (sizes.size != n_datapoints): - raise ValueError( - f"sequence of `sizes` must be 1 dimensional with " - f"the same length as the number of datapoints" - ) - - else: - raise TypeError("sizes must be a single , , or a sequence (array, list, tuple) of int" - "or float with the length equal to the number of datapoints") - - if np.count_nonzero(sizes < 0) > 1: - raise ValueError( - "All sizes must be positive numbers greater than or equal to 0.0." - ) - - return sizes - - def __setitem__(self, key, value): - # this is a very simple 1D buffer, no parsing required, directly set buffer - self.buffer.data[key] = value - self._update_range(key) - - # avoid creating dicts constantly if there are no events to handle - if len(self._event_handlers) > 0: - self._feature_changed(key, value) - - def _feature_changed(self, key, new_data): - if key is not None: - key = cleanup_slice(key, self._upper_bound) - if isinstance(key, (int, np.integer)): - indices = [key] - elif isinstance(key, slice): - indices = range(key.start, key.stop, key.step) - elif isinstance(key, np.ndarray): - indices = key - elif key is None: - indices = None - - pick_info = { - "index": indices, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="sizes", pick_info=pick_info) - - self._call_event_handlers(event_data) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 9d4380faa..e98ba5452 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -5,12 +5,29 @@ from ..utils import parse_cmap_values from ._base import PositionsGraphic -from ._features import CmapFeature, PointsSizesFeature +from ._features import CmapFeature, PointsSizesFeature, UniformSizes class ScatterGraphic(PositionsGraphic): features = {"data", "sizes", "colors"}#, "cmap", "present"} + @property + def sizes(self) -> PointsSizesFeature | float: + """Get or set the scatter point size(s)""" + if isinstance(self._sizes, PointsSizesFeature): + return self._sizes + + elif isinstance(self._sizes, UniformSizes): + return self._sizes.value + + @sizes.setter + def sizes(self, value): + if isinstance(self._sizes, PointsSizesFeature): + self._sizes[:] = value + + elif isinstance(self._sizes, UniformSizes): + self._sizes.set_value(self, value) + def __init__( self, data: Any, @@ -99,15 +116,17 @@ def __init__( if uniform_colors: material_kwargs["color_mode"] = "uniform" + material_kwargs["color"] = self.colors.value else: material_kwargs["color_mode"] = "vertex" - geo_kwargs["colors"] = self._colors.buffer + geo_kwargs["colors"] = self.colors.buffer if uniform_sizes: material_kwargs["size_mode"] = "uniform" + material_kwargs["size"] = self.sizes.value else: material_kwargs["size_mode"] = "vertex" - geo_kwargs["sizes"] = self._sizes.buffer + geo_kwargs["sizes"] = self.sizes.buffer world_object = pygfx.Points( pygfx.Geometry(**geo_kwargs), From a45b3f9f378b2c77e8cd06dee9a6282903b05e29 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 22 May 2024 05:35:41 -0400 Subject: [PATCH 045/196] VertexCmap feature, not yet tested --- fastplotlib/graphics/_base.py | 16 ++- fastplotlib/graphics/_features/__init__.py | 6 +- fastplotlib/graphics/_features/_base.py | 36 ++++-- .../graphics/_features/_positions_graphics.py | 117 +++++++++--------- fastplotlib/graphics/_features/utils.py | 6 +- fastplotlib/graphics/line.py | 2 +- fastplotlib/graphics/scatter.py | 4 +- 7 files changed, 107 insertions(+), 80 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index dc6954160..737f80a20 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -12,7 +12,7 @@ import pygfx -from ._features import GraphicFeature, BufferManager, Deleted, VertexPositions, VertexColors, PointsSizesFeature, Name, Offset, Rotation, Visible, UniformColor +from ._features import GraphicFeature, BufferManager, Deleted, VertexPositions, VertexColors, VertexCmap, PointsSizesFeature, Name, Offset, Rotation, Visible, UniformColor from ..utils import parse_cmap_values HexStr: TypeAlias = str @@ -386,6 +386,18 @@ def colors(self, value): elif isinstance(self._colors, UniformColor): self._colors.set_value(self, value) + @property + def cmap(self) -> VertexCmap: + """Control cmap""" + return self._cmap + + @cmap.setter + def cmap(self, name: str): + if self._cmap is None: + raise BufferError("Cannot use cmap with uniform_colors=True") + + self._cmap[:] = name + def __init__( self, data: Any, @@ -422,12 +434,14 @@ def __init__( else: if uniform_colors: self._colors = UniformColor(colors) + self._cmap = None else: self._colors = VertexColors( colors, n_colors=self._data.value.shape[0], alpha=alpha, ) + self._cmap = VertexCmap(self._colors, cmap_name=cmap, cmap_values=cmap_values) super().__init__(*args, **kwargs) diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index b2a32dd47..1c908e703 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -1,4 +1,5 @@ -from ._positions_graphics import VertexColors, UniformColor, UniformSizes, VertexPositions, PointsSizesFeature # , CmapFeature, ImageCmapFeature, HeatmapCmapFeature +from ._positions_graphics import VertexColors, UniformColor, UniformSizes, VertexPositions, PointsSizesFeature, VertexCmap +# , CmapFeature, ImageCmapFeature, HeatmapCmapFeature # from ._present import PresentFeature # from ._thickness import ThicknessFeature from ._base import ( @@ -28,9 +29,6 @@ # "Deleted", # ] -class CmapFeature: - pass - class ThicknessFeature: pass diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 06e53b163..254852121 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -153,7 +153,7 @@ class BufferManager(GraphicFeature): def __init__( self, - data: NDArray, + data: NDArray | pygfx.Buffer, buffer_type: Literal["buffer", "texture"] = "buffer", isolated_buffer: bool = True, texture_dim: int = 2, @@ -168,12 +168,18 @@ def __init__( # user's input array is used as the buffer bdata = data - if buffer_type == "buffer": + if isinstance(data, pygfx.Buffer): + # already a buffer, probably used for + # managing another BufferManager, example: VertexCmap manages VertexColors + self._buffer = data + elif buffer_type == "buffer": self._buffer = pygfx.Buffer(bdata) elif buffer_type == "texture": self._buffer = pygfx.Texture(bdata, dim=texture_dim) else: - raise ValueError("`buffer_type` must be one of: 'buffer' or 'texture'") + raise ValueError( + "`data` must be a pygfx.Buffer instance or `buffer_type` must be one of: 'buffer' or 'texture'" + ) self._event_handlers: list[callable] = list() @@ -202,11 +208,7 @@ def __getitem__(self, item): def __setitem__(self, key, value): raise NotImplementedError - def _update_range(self, key: int | slice | np.ndarray[int | bool] | list[bool | int] | tuple[slice, ...]): - """ - Uses key from slicing to determine the offset and - size of the buffer to mark for upload to the GPU - """ + def _parse_offset_size(self, key: int | slice | np.ndarray[int | bool] | list[bool | int] | tuple[slice, ...]): # number of elements in the buffer upper_bound = self.value.shape[0] @@ -219,8 +221,12 @@ def _update_range(self, key: int | slice | np.ndarray[int | bool] | list[bool | # simplest case offset = key size = 1 + n_elements = 1 elif isinstance(key, slice): + # TODO: off-by-one sometimes when step is used + # the offset can be one to the left or the size + # is one extra so it's not really an issue for now # parse slice start, stop, step = key.indices(upper_bound) @@ -238,6 +244,7 @@ def _update_range(self, key: int | slice | np.ndarray[int | bool] | list[bool | # number of elements to upload # this is indexing so do not add 1 size = abs(stop - start) + n_elements = len(range(start, stop, step)) elif isinstance(key, (np.ndarray, list)): if isinstance(key, list): @@ -265,14 +272,25 @@ def _update_range(self, key: int | slice | np.ndarray[int | bool] | list[bool | # index of first element to upload offset = key.min() - # number of elements to upload + # size range to upload # add 1 because this is direct # passing of indices, not a start:stop size = np.ptp(key) + 1 + # number of elements indexed + n_elements = key.size + else: raise TypeError(key) + return offset, size, n_elements + + def _update_range(self, key: int | slice | np.ndarray[int | bool] | list[bool | int] | tuple[slice, ...]): + """ + Uses key from slicing to determine the offset and + size of the buffer to mark for upload to the GPU + """ + offset, size, n_elements = self._parse_offset_size(key) self.buffer.update_range(offset=offset, size=size) def _emit_event(self, type: str, key, value): diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index f32968045..83ef3869d 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -235,66 +235,63 @@ def __setitem__(self, key, value): self._emit_event("sizes", key, value) -# class CmapFeature(ColorFeature): -# """ -# Indexable colormap feature, mostly wraps colors and just provides a way to set colormaps. -# -# Same event pick info as :class:`ColorFeature` -# """ -# -# def __init__(self, parent, colors, cmap_name: str, cmap_values: np.ndarray): -# # Skip the ColorFeature's __init__ -# super(ColorFeature, self).__init__(parent, colors) -# -# self._cmap_name = cmap_name -# self._cmap_values = cmap_values -# -# def __setitem__(self, key, cmap_name): -# key = cleanup_slice(key, self._upper_bound) -# if not isinstance(key, (slice, np.ndarray)): -# raise TypeError( -# "Cannot set cmap on single indices, must pass a slice object, " -# "numpy.ndarray or set it on the entire data." -# ) -# -# if isinstance(key, slice): -# n_colors = len(range(key.start, key.stop, key.step)) -# -# else: -# # numpy array -# n_colors = key.size -# -# colors = parse_cmap_values( -# n_colors=n_colors, cmap_name=cmap_name, cmap_values=self._cmap_values -# ) -# -# self._cmap_name = cmap_name -# super().__setitem__(key, colors) -# -# @property -# def name(self) -> str: -# return self._cmap_name -# -# @property -# def values(self) -> np.ndarray: -# return self._cmap_values -# -# @values.setter -# def values(self, values: np.ndarray): -# if not isinstance(values, np.ndarray): -# values = np.array(values) -# -# colors = parse_cmap_values( -# n_colors=self().shape[0], cmap_name=self._cmap_name, cmap_values=values -# ) -# -# self._cmap_values = values -# -# super().__setitem__(slice(None), colors) -# -# def __repr__(self) -> str: -# s = f"CmapFeature for {self._parent}, to get name or values: `.cmap.name`, `.cmap.values`" -# return s +class VertexCmap(BufferManager): + """ + Sliceable colormap feature, manages a VertexColors instance and just provides a way to set colormaps. + """ + + def __init__(self, vertex_colors: VertexColors, cmap_name: str, cmap_values: np.ndarray): + super().__init__(data=vertex_colors) + + self._vertex_colors = vertex_colors + self._cmap_name = cmap_name + self._cmap_values = cmap_values + + def __setitem__(self, key, cmap_name): + if isinstance(key, slice): + if key.step is not None: + raise TypeError( + "step sized indexing not currently supported for setting VertexCmap, " + "continuous regions are recommended" + ) + + offset, size, n_elements = self._parse_offset_size(key) + + colors = parse_cmap_values( + n_colors=n_elements, cmap_name=cmap_name, cmap_values=self._cmap_values + ) + + self._cmap_name = cmap_name + self._vertex_colors[key] = colors + + @property + def name(self) -> str: + return self._cmap_name + + @property + def values(self) -> np.ndarray: + return self._cmap_values + + @values.setter + def values(self, values: np.ndarray | list[float | int], indices: slice | list | np.ndarray = None): + if self._cmap_name is None: + raise AttributeError( + "cmap is not set, set the cmap before setting the cmap_values" + ) + + values = np.asarray(values) + + colors = parse_cmap_values( + n_colors=self.value.shape[0], cmap_name=self._cmap_name, cmap_values=values + ) + + self._cmap_values = values + + if indices is None: + indices = slice(None) + + self._vertex_colors[indices] = colors + # # # class ImageCmapFeature(GraphicFeature): diff --git a/fastplotlib/graphics/_features/utils.py b/fastplotlib/graphics/_features/utils.py index 316014881..c3cd59ccc 100644 --- a/fastplotlib/graphics/_features/utils.py +++ b/fastplotlib/graphics/_features/utils.py @@ -7,7 +7,7 @@ def parse_colors( - colors: str | np.ndarray | Iterable[str], + colors: str | np.ndarray | list[str] | tuple[str], n_colors: int | None, alpha: float | None = None, key: int | tuple | slice | None = None, @@ -48,8 +48,8 @@ def parse_colors( "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" ) - # if the color is provided as an iterable - elif isinstance(colors, (list, tuple, np.ndarray)): + # if the color is provided as list or tuple + elif isinstance(colors, (list, tuple)): # if iterable of str if all([isinstance(val, str) for val in colors]): if not len(colors) == n_colors: diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 524618767..422f89013 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -10,7 +10,7 @@ class LineGraphic(PositionsGraphic, Interaction): - features = {"data", "colors"}#, "cmap", "thickness", "present"} + features = {"data", "colors", "cmap"}#, "thickness"} def __init__( self, diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index e98ba5452..60c237cea 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -5,11 +5,11 @@ from ..utils import parse_cmap_values from ._base import PositionsGraphic -from ._features import CmapFeature, PointsSizesFeature, UniformSizes +from ._features import PointsSizesFeature, UniformSizes class ScatterGraphic(PositionsGraphic): - features = {"data", "sizes", "colors"}#, "cmap", "present"} + features = {"data", "sizes", "colors", "cmap"} @property def sizes(self) -> PointsSizesFeature | float: From 09ce4f577d8df6e92d64e13931a0925047be0321 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 22 May 2024 06:05:22 -0400 Subject: [PATCH 046/196] start image features, not tested, add thickness, not tested --- fastplotlib/graphics/_features/__init__.py | 6 +- fastplotlib/graphics/_features/_image.py | 66 ++++++++++ .../graphics/_features/_positions_graphics.py | 119 ++++-------------- fastplotlib/graphics/image.py | 26 +++- fastplotlib/graphics/line.py | 24 ++-- fastplotlib/graphics/scatter.py | 3 - 6 files changed, 125 insertions(+), 119 deletions(-) create mode 100644 fastplotlib/graphics/_features/_image.py diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index 1c908e703..fd4ef7bf2 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -1,4 +1,5 @@ -from ._positions_graphics import VertexColors, UniformColor, UniformSizes, VertexPositions, PointsSizesFeature, VertexCmap +from ._positions_graphics import VertexColors, UniformColor, UniformSizes, Thickness, VertexPositions, PointsSizesFeature, VertexCmap +from ._image import Cmap, Vmin, Vmax # , CmapFeature, ImageCmapFeature, HeatmapCmapFeature # from ._present import PresentFeature # from ._thickness import ThicknessFeature @@ -29,9 +30,6 @@ # "Deleted", # ] -class ThicknessFeature: - pass - class ImageCmapFeature: pass diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py new file mode 100644 index 000000000..b1fbcb7ff --- /dev/null +++ b/fastplotlib/graphics/_features/_image.py @@ -0,0 +1,66 @@ +from ._base import GraphicFeature, FeatureEvent + +from ...utils import ( + make_colors, + get_cmap_texture, + quick_min_max, +) + + +class Vmin(GraphicFeature): + """lower contrast limit""" + def __init__(self, value: float): + self._value = value + super().__init__() + + @property + def value(self) -> float: + return self._value + + def set_value(self, graphic, value: float): + vmax = graphic.world_object.material.clim[1] + graphic.world_object.material.clim = (value, vmax) + self._value = value + + event = FeatureEvent(type="vmin", info={"value": value}) + self._call_event_handlers(event) + + +class Vmax(GraphicFeature): + """upper contrast limit""" + def __init__(self, value: float): + self._value = value + super().__init__() + + @property + def value(self) -> float: + return self._value + + def set_value(self, graphic, value: float): + vmin = graphic.world_object.material.clim[0] + graphic.world_object.material.clim = (vmin, value) + self._value = value + + event = FeatureEvent(type="vmax", info={"value": value}) + self._call_event_handlers(event) + + +class Cmap(GraphicFeature): + """colormap for texture""" + def __init__(self, value: str): + self._value = value + self.texture = get_cmap_texture(value) + super().__init__() + + @property + def value(self) -> str: + return self._value + + def set_value(self, graphic, value: str): + new_colors = make_colors(256, value) + graphic.world_object.material.map.data[:] = new_colors + graphic.world_object.material.map.data.update_range((0, 0, 0), size=(256, 1, 1)) + + self._value = value + event = FeatureEvent(type="cmap", info={"value": value}) + self._call_event_handlers(event) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index 83ef3869d..3658059c6 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -4,10 +4,7 @@ import pygfx from ...utils import ( - make_colors, - get_cmap_texture, parse_cmap_values, - quick_min_max, ) from ._base import ( GraphicFeature, @@ -235,6 +232,24 @@ def __setitem__(self, key, value): self._emit_event("sizes", key, value) +class Thickness(GraphicFeature): + """line thickness""" + def __init__(self, value: float): + self._value = value + super().__init__() + + @property + def value(self) -> float: + return self._value + + def set_value(self, graphic, value: float): + graphic.world_object.material.thickness = value + self._value = value + + event = FeatureEvent(type="thickness", info={"value": value}) + self._call_event_handlers(event) + + class VertexCmap(BufferManager): """ Sliceable colormap feature, manages a VertexColors instance and just provides a way to set colormaps. @@ -264,6 +279,8 @@ def __setitem__(self, key, cmap_name): self._cmap_name = cmap_name self._vertex_colors[key] = colors + self._emit_event("cmap", key, cmap_name) + @property def name(self) -> str: return self._cmap_name @@ -292,100 +309,8 @@ def values(self, values: np.ndarray | list[float | int], indices: slice | list | self._vertex_colors[indices] = colors -# -# -# class ImageCmapFeature(GraphicFeature): -# """ -# Colormap for :class:`ImageGraphic`. -# -# .cmap() returns the Texture buffer for the cmap. -# -# .cmap.name returns the cmap name as a str. -# -# **event pick info:** -# -# ================ =================== =============== -# key type description -# ================ =================== =============== -# "index" ``None`` not used -# "name" ``str`` colormap name -# "world_object" pygfx.WorldObject world object -# "vmin" ``float`` minimum value -# "vmax" ``float`` maximum value -# ================ =================== =============== -# -# """ -# -# def __init__(self, parent, cmap: str): -# cmap_texture_view = get_cmap_texture(cmap) -# super().__init__(parent, cmap_texture_view) -# self._name = cmap -# -# def _set(self, cmap_name: str): -# if self._parent.data().ndim > 2: -# return -# -# self._parent.world_object.material.map.data[:] = make_colors(256, cmap_name) -# self._parent.world_object.material.map.update_range((0, 0, 0), size=(256, 1, 1)) -# self._name = cmap_name -# -# self._feature_changed(key=None, new_data=self._name) -# -# @property -# def name(self) -> str: -# return self._name -# -# @property -# def vmin(self) -> float: -# """Minimum contrast limit.""" -# return self._parent.world_object.material.clim[0] -# -# @vmin.setter -# def vmin(self, value: float): -# """Minimum contrast limit.""" -# self._parent.world_object.material.clim = ( -# value, -# self._parent.world_object.material.clim[1], -# ) -# self._feature_changed(key=None, new_data=None) -# -# @property -# def vmax(self) -> float: -# """Maximum contrast limit.""" -# return self._parent.world_object.material.clim[1] -# -# @vmax.setter -# def vmax(self, value: float): -# """Maximum contrast limit.""" -# self._parent.world_object.material.clim = ( -# self._parent.world_object.material.clim[0], -# value, -# ) -# self._feature_changed(key=None, new_data=None) -# -# def reset_vmin_vmax(self): -# """Reset vmin vmax values based on current data""" -# self.vmin, self.vmax = quick_min_max(self._parent.data()) -# -# def _feature_changed(self, key, new_data): -# # this is a non-indexable feature so key=None -# -# pick_info = { -# "index": None, -# "world_object": self._parent.world_object, -# "name": self._name, -# "vmin": self.vmin, -# "vmax": self.vmax, -# } -# -# event_data = FeatureEvent(type="cmap", pick_info=pick_info) -# -# self._call_event_handlers(event_data) -# -# def __repr__(self) -> str: -# s = f"ImageCmapFeature for {self._parent}. Use `.cmap.name` to get str name of cmap." -# return s -# + self._emit_event("cmap.name", indices, values) + # # class HeatmapCmapFeature(ImageCmapFeature): # """ diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index ce736dab2..e87eaad0f 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -11,7 +11,9 @@ from ._base import Graphic, Interaction from .selectors import LinearSelector, LinearRegionSelector from ._features import ( - ImageCmapFeature, + Cmap, + Vmin, + Vmax, ImageDataFeature, HeatmapDataFeature, HeatmapCmapFeature, @@ -196,7 +198,16 @@ def _add_plot_area_hook(self, plot_area): class ImageGraphic(Graphic, Interaction, _AddSelectorsMixin): - feature_events = {"data", "cmap", "present"} + features = {"data", "cmap", "vmin", "vmax"} + + @property + def cmap(self) -> str: + """Graphic name""" + return self._cmap.value + + @cmap.setter + def cmap(self, name: str): + self._cmap.set_value(self, name) def __init__( self, @@ -277,7 +288,7 @@ def __init__( geometry = pygfx.Geometry(grid=texture) - self.cmap = ImageCmapFeature(self, cmap) + self._cmap = Cmap(cmap) # if data is RGB or RGBA if data.ndim > 2: @@ -288,7 +299,7 @@ def __init__( else: material = pygfx.ImageBasicMaterial( clim=(vmin, vmax), - map=self.cmap(), + map=self._cmap.texture, map_interpolation=filter, pick_write=True, ) @@ -297,8 +308,8 @@ def __init__( self._set_world_object(world_object) - self.cmap.vmin = vmin - self.cmap.vmax = vmax + self.vmin = Vmin(vmin) + self.vmax = Vmax(Vmax) self.data = ImageDataFeature(self, data) # TODO: we need to organize and do this better @@ -307,6 +318,9 @@ def __init__( # set it with the actual data self.data = data + # def reset_vmin_vmax(self): + # vmin, vmax = quick_min_max(data) + def set_feature(self, feature: str, new_data: Any, indices: Any): pass diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 422f89013..62160c164 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -7,10 +7,20 @@ from ._base import PositionsGraphic, Interaction, PreviouslyModifiedData from .selectors import LinearRegionSelector, LinearSelector +from ._features import Thickness class LineGraphic(PositionsGraphic, Interaction): - features = {"data", "colors", "cmap"}#, "thickness"} + features = {"data", "colors", "cmap", "thickness"} + + @property + def thickness(self) -> float: + """Graphic name""" + return self._thickness.value + + @thickness.setter + def thickness(self, value: float): + self._thickness.set_value(self, value) def __init__( self, @@ -82,10 +92,6 @@ def __init__( """ - # self.cmap = CmapFeature( - # self, self.colors(), cmap_name=cmap, cmap_values=cmap_values - # ) - super().__init__( data=data, colors=colors, @@ -98,6 +104,8 @@ def __init__( **kwargs ) + self._thickness = Thickness(thickness) + if thickness < 1.1: MaterialCls = pygfx.LineThinMaterial else: @@ -105,13 +113,11 @@ def __init__( if uniform_colors: geometry = pygfx.Geometry(positions=self._data.buffer) - material = MaterialCls(thickness=thickness, color_mode="uniform", pick_write=True) + material = MaterialCls(thickness=self.thickness, color_mode="uniform", pick_write=True) else: - material = MaterialCls(thickness=thickness, color_mode="vertex", pick_write=True) + material = MaterialCls(thickness=self.thickness, color_mode="vertex", pick_write=True) geometry = pygfx.Geometry(positions=self._data.buffer, colors=self._colors.buffer) - # self.thickness = ThicknessFeature(self, thickness) - world_object: pygfx.Line = pygfx.Line( geometry=geometry, material=material diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 60c237cea..f3afcd31a 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -92,9 +92,6 @@ def __init__( Control the presence of the Graphic in the scene, set to ``True`` or ``False`` """ - # self.cmap = CmapFeature( - # self, self.colors(), cmap_name=cmap, cmap_values=cmap_values - # ) super().__init__( data=data, From 93e7a9dc21e88cacbaa66a954a8b4145d2a92757 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 23 May 2024 03:16:20 -0400 Subject: [PATCH 047/196] better cmap parsing --- fastplotlib/graphics/_base.py | 72 ++++++++++++------- .../graphics/_features/_positions_graphics.py | 43 ++++++++--- 2 files changed, 81 insertions(+), 34 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 737f80a20..52d1465e2 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -404,44 +404,68 @@ def __init__( colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", uniform_colors: bool = False, alpha: float = 1.0, - cmap: str = None, + cmap: str | VertexCmap = None, cmap_values: np.ndarray = None, isolated_buffer: bool = True, *args, **kwargs, ): - self._data = VertexPositions(data, isolated_buffer=isolated_buffer) + if isinstance(data, VertexPositions): + self._data = data + else: + self._data = VertexPositions(data, isolated_buffer=isolated_buffer) if cmap is not None: + # if a cmap is specified it overrides colors argument if uniform_colors: raise TypeError( "Cannot use cmap if uniform_colors=True" ) - n_datapoints = self._data.value.shape[0] - - colors = parse_cmap_values( - n_colors=n_datapoints, cmap_name=cmap, cmap_values=cmap_values - ) - - if isinstance(colors, VertexColors): - if uniform_colors: - raise TypeError( - "Cannot use vertex colors from existing instance if uniform_colors=True" - ) - self._colors = colors - self._colors._shared += 1 - else: - if uniform_colors: - self._colors = UniformColor(colors) - self._cmap = None + if isinstance(cmap, str): + # make colors from cmap + if isinstance(colors, VertexColors): + # share buffer with existing colors instance for the cmap + self._colors = colors + self._colors._shared += 1 + else: + # create vertex colors buffer + self._colors = VertexColors("w", n_colors=self._data.value.shape[0]) + # make cmap using vertex colors buffer + self._cmap = VertexCmap( + self._colors, + cmap_name=cmap, + cmap_values=cmap_values + ) + elif isinstance(cmap, VertexCmap): + # use existing cmap instance + self._cmap = cmap + self._colors = cmap._vertex_colors else: - self._colors = VertexColors( - colors, - n_colors=self._data.value.shape[0], - alpha=alpha, + raise TypeError + else: + # no cmap given + if isinstance(colors, VertexColors): + # share buffer with existing colors instance + self._colors = colors + self._colors._shared += 1 + # blank colormap instance + self._cmap = VertexCmap( + self._colors, + cmap_name=None, + cmap_values=None ) - self._cmap = VertexCmap(self._colors, cmap_name=cmap, cmap_values=cmap_values) + else: + if uniform_colors: + self._colors = UniformColor(colors) + self._cmap = None + else: + self._colors = VertexColors( + colors, + n_colors=self._data.value.shape[0], + alpha=alpha, + ) + self._cmap = VertexCmap(self._colors, cmap_name=None, cmap_values=None) super().__init__(*args, **kwargs) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index 3658059c6..37ac355a5 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -170,7 +170,7 @@ def _fix_data(self, data): return to_gpu_supported_dtype(data) - def __setitem__(self, key: int | slice | range | np.ndarray[int | bool] | tuple[slice, ...] | tuple[range, ...], value): + def __setitem__(self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], value): # directly use the key to slice the buffer self.buffer.data[key] = value @@ -255,22 +255,42 @@ class VertexCmap(BufferManager): Sliceable colormap feature, manages a VertexColors instance and just provides a way to set colormaps. """ - def __init__(self, vertex_colors: VertexColors, cmap_name: str, cmap_values: np.ndarray): + def __init__(self, vertex_colors: VertexColors, cmap_name: str | None, cmap_values: np.ndarray | None): super().__init__(data=vertex_colors) self._vertex_colors = vertex_colors self._cmap_name = cmap_name self._cmap_values = cmap_values - def __setitem__(self, key, cmap_name): - if isinstance(key, slice): - if key.step is not None: - raise TypeError( - "step sized indexing not currently supported for setting VertexCmap, " - "continuous regions are recommended" - ) + if self._cmap_name is not None: + if not isinstance(self._cmap_name, str): + raise TypeError + if not isinstance(self._cmap_values, np.ndarray): + raise TypeError + + n_datapoints = vertex_colors.value.shape[0] + + colors = parse_cmap_values( + n_colors=n_datapoints, cmap_name=self._cmap_name, cmap_values=self._cmap_values + ) + # set vertex colors from cmap + self._vertex_colors[:] = colors + + def __setitem__(self, key: slice, cmap_name): + if not isinstance(key, slice): + raise TypeError( + "fancy indexing not supported for VertexCmap, only slices " + "of a continuous are supported for apply a cmap" + ) + if key.step is not None: + raise TypeError( + "step sized indexing not currently supported for setting VertexCmap, " + "slices must be a continuous region" + ) - offset, size, n_elements = self._parse_offset_size(key) + # parse slice + start, stop, step = key.indices(self.value.shape[0]) + n_elements = len(range(start, stop, step)) colors = parse_cmap_values( n_colors=n_elements, cmap_name=cmap_name, cmap_values=self._cmap_values @@ -279,6 +299,9 @@ def __setitem__(self, key, cmap_name): self._cmap_name = cmap_name self._vertex_colors[key] = colors + # TODO: should we block vertex_colors from emitting an event? + # Because currently this will result in 2 emitted events, one + # for cmap and another from the colors self._emit_event("cmap", key, cmap_name) @property From 2be22f63ac42a902afdd6ece44b1a0b37f3c10dc Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 23 May 2024 03:23:03 -0400 Subject: [PATCH 048/196] cleanup --- fastplotlib/graphics/_base.py | 1 - fastplotlib/graphics/_features/__init__.py | 30 ++------------------ fastplotlib/graphics/_features/_base.py | 32 +++++++++++----------- fastplotlib/graphics/_features/utils.py | 2 -- 4 files changed, 18 insertions(+), 47 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 52d1465e2..443602b2c 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -13,7 +13,6 @@ import pygfx from ._features import GraphicFeature, BufferManager, Deleted, VertexPositions, VertexColors, VertexCmap, PointsSizesFeature, Name, Offset, Rotation, Visible, UniformColor -from ..utils import parse_cmap_values HexStr: TypeAlias = str diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index fd4ef7bf2..ace671805 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -1,8 +1,5 @@ from ._positions_graphics import VertexColors, UniformColor, UniformSizes, Thickness, VertexPositions, PointsSizesFeature, VertexCmap -from ._image import Cmap, Vmin, Vmax -# , CmapFeature, ImageCmapFeature, HeatmapCmapFeature -# from ._present import PresentFeature -# from ._thickness import ThicknessFeature +from ._image import ImageData, ImageCmap, ImageVmin, ImageVmax from ._base import ( GraphicFeature, BufferManager, @@ -11,33 +8,10 @@ ) from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature from ._common import Name, Offset, Rotation, Visible, Deleted -# __all__ = [ -# "ColorFeature", -# "CmapFeature", -# "ImageCmapFeature", -# "HeatmapCmapFeature", -# "PointsDataFeature", -# "PointsSizesFeature", -# "ImageDataFeature", -# "HeatmapDataFeature", -# "PresentFeature", -# "ThicknessFeature", -# "GraphicFeature", -# "FeatureEvent", -# "to_gpu_supported_dtype", -# "LinearSelectionFeature", -# "LinearRegionSelectionFeature", -# "Deleted", -# ] -class ImageCmapFeature: - pass - -class ImageDataFeature: - pass class HeatmapDataFeature: pass class HeatmapCmapFeature: - pass \ No newline at end of file + pass diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 254852121..d603e2a33 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -208,20 +208,14 @@ def __getitem__(self, item): def __setitem__(self, key, value): raise NotImplementedError - def _parse_offset_size(self, key: int | slice | np.ndarray[int | bool] | list[bool | int] | tuple[slice, ...]): - # number of elements in the buffer - upper_bound = self.value.shape[0] - - if isinstance(key, tuple): - # if multiple dims are sliced, we only need the key for - # the first dimension corresponding to n_datapoints - key: int | np.ndarray[int | bool] | slice = key[0] - + def _parse_offset_size(self, key: int | slice | np.ndarray[int | bool] | list[bool | int], upper_bound: int): + """ + parse offset and size for one dimension + """ if isinstance(key, int): # simplest case offset = key size = 1 - n_elements = 1 elif isinstance(key, slice): # TODO: off-by-one sometimes when step is used @@ -244,7 +238,6 @@ def _parse_offset_size(self, key: int | slice | np.ndarray[int | bool] | list[bo # number of elements to upload # this is indexing so do not add 1 size = abs(stop - start) - n_elements = len(range(start, stop, step)) elif isinstance(key, (np.ndarray, list)): if isinstance(key, list): @@ -277,20 +270,27 @@ def _parse_offset_size(self, key: int | slice | np.ndarray[int | bool] | list[bo # passing of indices, not a start:stop size = np.ptp(key) + 1 - # number of elements indexed - n_elements = key.size - else: raise TypeError(key) - return offset, size, n_elements + return offset, size def _update_range(self, key: int | slice | np.ndarray[int | bool] | list[bool | int] | tuple[slice, ...]): """ Uses key from slicing to determine the offset and size of the buffer to mark for upload to the GPU """ - offset, size, n_elements = self._parse_offset_size(key) + upper_bound = self.value.shape[0] + + if isinstance(key, tuple): + if any([k is Ellipsis for k in key]): + # let's worry about ellipsis later + raise TypeError("ellipses not supported for indexing buffers") + # if multiple dims are sliced, we only need the key for + # the first dimension corresponding to n_datapoints + key: int | np.ndarray[int | bool] | slice = key[0] + + offset, size = self._parse_offset_size(key, upper_bound) self.buffer.update_range(offset=offset, size=size) def _emit_event(self, type: str, key, value): diff --git a/fastplotlib/graphics/_features/utils.py b/fastplotlib/graphics/_features/utils.py index c3cd59ccc..e2f6e3428 100644 --- a/fastplotlib/graphics/_features/utils.py +++ b/fastplotlib/graphics/_features/utils.py @@ -1,6 +1,5 @@ import pygfx import numpy as np -from typing import Iterable from ._base import to_gpu_supported_dtype from ...utils import make_pygfx_colors @@ -10,7 +9,6 @@ def parse_colors( colors: str | np.ndarray | list[str] | tuple[str], n_colors: int | None, alpha: float | None = None, - key: int | tuple | slice | None = None, ): """ From eedee8ffdfbbf7b5980fcb8e8ec1b105c1ec514f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 23 May 2024 03:24:52 -0400 Subject: [PATCH 049/196] image features --- fastplotlib/graphics/_features/_image.py | 61 +++++++++++++++-- fastplotlib/graphics/image.py | 83 +++++++++++++----------- 2 files changed, 102 insertions(+), 42 deletions(-) diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py index b1fbcb7ff..c32f14bd1 100644 --- a/fastplotlib/graphics/_features/_image.py +++ b/fastplotlib/graphics/_features/_image.py @@ -1,13 +1,64 @@ -from ._base import GraphicFeature, FeatureEvent +import numpy as np + +import pygfx +from ._base import GraphicFeature, BufferManager, FeatureEvent from ...utils import ( make_colors, get_cmap_texture, - quick_min_max, ) -class Vmin(GraphicFeature): +class ImageData(BufferManager): + def __init__(self, data, isolated_buffer: bool = True): + data = self._fix_data(data) + super().__init__(data, buffer_type="texture", isolated_buffer=isolated_buffer) + + @property + def buffer(self) -> pygfx.Texture: + return self._buffer + + def _fix_data(self, data): + if data.ndim not in (2, 3): + raise ValueError( + "image data must be 2D with or without an RGB(A) dimension, i.e. " + "it must be of shape [x, y], [x, y, 3] or [x, y, 4]" + ) + + # let's just cast to float32 always + return data.astype(np.float32) + + def __setitem__(self, key: int | slice | np.ndarray[int | bool] | tuple[slice | np.ndarray[int | bool]], value): + # offset and size should be (width, height, depth), i.e. (columns, rows, depth) + # offset and size for depth should always be 0, 1 for 2D images + if isinstance(key, tuple): + # multiple dims sliced + if any([k is Ellipsis for k in key]): + # let's worry about ellipsis later + raise TypeError("ellipses not supported for indexing buffers") + if len(key) in (2, 3): + dim_os = list() # hold offset and size for each dim + for dim, k in enumerate(key[:2]): # we only need width and height + dim_os.append(self._parse_offset_size(k, self.value.shape[dim])) + + # offset and size for each dim into individual offset and size tuple + # note that this is flipped since we need (width, height) from (rows, cols) + offset = (*tuple(os[1] for os in dim_os), 0) + size = (*tuple(os[1] for os in dim_os), 0) + else: + raise IndexError + + else: + # only first dim (rows) indexed + row_offset, row_size = self._parse_offset_size(key, self.value.shape[0]) + offset = (0, row_offset, 0) + size = (self.value.shape[1], row_size, 1) + + self.buffer.update_range(offset, size) + self._emit_event("data", key, value) + + +class ImageVmin(GraphicFeature): """lower contrast limit""" def __init__(self, value: float): self._value = value @@ -26,7 +77,7 @@ def set_value(self, graphic, value: float): self._call_event_handlers(event) -class Vmax(GraphicFeature): +class ImageVmax(GraphicFeature): """upper contrast limit""" def __init__(self, value: float): self._value = value @@ -45,7 +96,7 @@ def set_value(self, graphic, value: float): self._call_event_handlers(event) -class Cmap(GraphicFeature): +class ImageCmap(GraphicFeature): """colormap for texture""" def __init__(self, value: str): self._value = value diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index e87eaad0f..ef05631fc 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -11,10 +11,10 @@ from ._base import Graphic, Interaction from .selectors import LinearSelector, LinearRegionSelector from ._features import ( - Cmap, - Vmin, - Vmax, - ImageDataFeature, + ImageData, + ImageCmap, + ImageVmin, + ImageVmax, HeatmapDataFeature, HeatmapCmapFeature, to_gpu_supported_dtype, @@ -200,15 +200,42 @@ def _add_plot_area_hook(self, plot_area): class ImageGraphic(Graphic, Interaction, _AddSelectorsMixin): features = {"data", "cmap", "vmin", "vmax"} + @property + def data(self) -> ImageData: + """Get or set the image data""" + return self._data + + @data.setter + def data(self, data): + self._data[:] = data + @property def cmap(self) -> str: - """Graphic name""" + """colormap name""" return self._cmap.value @cmap.setter def cmap(self, name: str): self._cmap.set_value(self, name) + @property + def vmin(self) -> float: + """lower contrast limit""" + return self._vmin.value + + @vmin.setter + def vmin(self, value: float): + self._vmin.set_value(self, value) + + @property + def vmax(self) -> float: + """upper contrast limit""" + return self._vmax.value + + @vmax.setter + def vmax(self, value: float): + self._vmax.set_value(self, value) + def __init__( self, data: Any, @@ -268,37 +295,29 @@ def __init__( """ super().__init__(*args, **kwargs) - - data = to_gpu_supported_dtype(data) - - # TODO: we need to organize and do this better - if isolated_buffer: - # initialize a buffer with the same shape as the input data - # we do not directly use the input data array as the buffer - # because if the input array is a read-only type, such as - # numpy memmaps, we would not be able to change the image data - buffer_init = np.zeros(shape=data.shape, dtype=data.dtype) - else: - buffer_init = data + self._data = ImageData(data, isolated_buffer=isolated_buffer) + self._cmap = ImageCmap(cmap) if (vmin is None) or (vmax is None): vmin, vmax = quick_min_max(data) - texture = pygfx.Texture(buffer_init, dim=2) + self._vmin = ImageVmin(vmin) + self._vmax = ImageVmax(vmax) - geometry = pygfx.Geometry(grid=texture) + clim = (self.vmin, self.vmax) - self._cmap = Cmap(cmap) + # make grid geometry from image data Texture + geometry = pygfx.Geometry(grid=self._data.buffer) - # if data is RGB or RGBA - if data.ndim > 2: + if self._data.value.ndim > 2: + # if data is RGB or RGBA material = pygfx.ImageBasicMaterial( - clim=(vmin, vmax), map_interpolation=filter, pick_write=True + clim=clim, map_interpolation=filter, pick_write=True ) - # if data is just 2D without color information, use colormap LUT else: + # if data is just 2D without color information, use colormap LUT material = pygfx.ImageBasicMaterial( - clim=(vmin, vmax), + clim=clim, map=self._cmap.texture, map_interpolation=filter, pick_write=True, @@ -308,18 +327,8 @@ def __init__( self._set_world_object(world_object) - self.vmin = Vmin(vmin) - self.vmax = Vmax(Vmax) - - self.data = ImageDataFeature(self, data) - # TODO: we need to organize and do this better - if isolated_buffer: - # if the buffer was initialized with zeros - # set it with the actual data - self.data = data - - # def reset_vmin_vmax(self): - # vmin, vmax = quick_min_max(data) + def reset_vmin_vmax(self): + self.vmin, self.vmax = quick_min_max(self._data.value) def set_feature(self, feature: str, new_data: Any, indices: Any): pass From 67ad32724143dc8f41ce88803d4c5e493123ef43 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 23 May 2024 03:27:16 -0400 Subject: [PATCH 050/196] cleanup --- fastplotlib/graphics/_features/_thickness.py | 21 -------------------- 1 file changed, 21 deletions(-) delete mode 100644 fastplotlib/graphics/_features/_thickness.py diff --git a/fastplotlib/graphics/_features/_thickness.py b/fastplotlib/graphics/_features/_thickness.py deleted file mode 100644 index d13c2b727..000000000 --- a/fastplotlib/graphics/_features/_thickness.py +++ /dev/null @@ -1,21 +0,0 @@ -from ._base import GraphicFeature, FeatureEvent - - -class ThicknessFeature(GraphicFeature): - """ - Used by Line graphics for line material thickness. - """ - - def __init__(self, thickness: float): - self._value = thickness - super().__init__() - - @property - def value(self) -> float: - return self._value - - def set_value(self, parent, value: float): - parent.world_object.material.thickness = value - - event = FeatureEvent("thickness", {"value": value}) - self._call_event_handlers(event) From b417190e382c127124e1da31f26188a86daffe5f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 23 May 2024 04:11:25 -0400 Subject: [PATCH 051/196] start selection feature refactor --- .../graphics/_features/_selection_features.py | 71 ++++++------------- 1 file changed, 21 insertions(+), 50 deletions(-) diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index 21e5d0a09..4c8cdaa99 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -10,69 +10,40 @@ class LinearSelectionFeature(GraphicFeature): # A bit much to have a class for this but this allows it to integrate with the fastplotlib callback system """ Manages the linear selection and callbacks - - **event pick info** - - =================== =============================== ================================================================================================= - key type selection - =================== =============================== ================================================================================================= - "selected_index" ``int`` the graphic data index that corresponds to the selector position - "world_object" ``pygfx.WorldObject`` pygfx WorldObject - "new_data" ``numpy.ndarray`` or ``None`` the new selector position in world coordinates, not necessarily the same as "selected_index" - "graphic" ``Graphic`` the selector graphic - "delta" ``numpy.ndarray`` the delta vector of the graphic in NDC - "pygfx_event" ``pygfx.Event`` pygfx Event - =================== =============================== ================================================================================================= - """ - def __init__(self, parent, axis: str, value: float, limits: Tuple[int, int]): - super().__init__(parent, data=value) + def __init__(self, axis: str, value: float, limits: Tuple[int, int]): + super().__init__() self._axis = axis self._limits = limits + self._value = value - def _set(self, value: float): + @property + def value(self) -> float: + """ + selection index w.r.t. the graphic.data + not world position, graphic.offset is subtracted + """ + return self._value + + def set_value(self, graphic, value: float): if not (self._limits[0] <= value <= self._limits[1]): return - if self._axis == "x": - self._parent.position_x = value - else: - self._parent.position_y = value - - self._data = value - self._feature_changed(key=None, new_data=value) - - def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): - if len(self._event_handlers) < 1: - return + offset = list(graphic.offset) - if self._parent.parent is not None: - g_ix = self._parent.get_selected_index() + if self._axis == "x": + offset[0] = value else: - g_ix = None - - # get pygfx event and reset it - pygfx_ev = self._parent._pygfx_event - self._parent._pygfx_event = None - - pick_info = { - "world_object": self._parent.world_object, - "new_data": new_data, - "selected_index": g_ix, - "graphic": self._parent, - "pygfx_event": pygfx_ev, - "delta": self._parent.delta, - } - - event_data = FeatureEvent(type="selection", pick_info=pick_info) + offset[1] = value - self._call_event_handlers(event_data) + graphic.offset = offset - def __repr__(self) -> str: - s = f"LinearSelectionFeature for {self._parent}" - return s + self._value = value + event = FeatureEvent("selection", {"value": value}) + self._call_event_handlers(event) + # TODO: selector event handlers can call event.get_selected_index() to get the data index class LinearRegionSelectionFeature(GraphicFeature): From eb4740842aaab50ebdf421d35ea8004136f36354 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 23 May 2024 04:34:15 -0400 Subject: [PATCH 052/196] more on selection features --- .../graphics/_features/_selection_features.py | 120 +++++++----------- 1 file changed, 47 insertions(+), 73 deletions(-) diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index 4c8cdaa99..9c27f3326 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -12,7 +12,21 @@ class LinearSelectionFeature(GraphicFeature): Manages the linear selection and callbacks """ - def __init__(self, axis: str, value: float, limits: Tuple[int, int]): + def __init__(self, axis: str, value: float, limits: Tuple[float, float]): + """ + + Parameters + ---------- + axis: "x" | "y" + axis the selector is restricted to + + value: float + position of the slider in world space, NOT data space + limits: (float, float) + min, max limits of the selector + + """ + super().__init__() self._axis = axis @@ -22,9 +36,10 @@ def __init__(self, axis: str, value: float, limits: Tuple[int, int]): @property def value(self) -> float: """ - selection index w.r.t. the graphic.data - not world position, graphic.offset is subtracted + selection in world space, NOT data space """ + # TODO: Not sure if we should make this public since it's in world space, not data space + # need to decide if we give a value based on the selector's parent graphic, if there is one return self._value def set_value(self, graphic, value: float): @@ -41,48 +56,37 @@ def set_value(self, graphic, value: float): graphic.offset = offset self._value = value - event = FeatureEvent("selection", {"value": value}) + event = FeatureEvent("selection", {"index": graphic.get_selected_index()}) self._call_event_handlers(event) - # TODO: selector event handlers can call event.get_selected_index() to get the data index class LinearRegionSelectionFeature(GraphicFeature): """ Feature for a linearly bounding region - - **event pick info** - - ===================== =============================== ======================================================================================= - key type description - ===================== =============================== ======================================================================================= - "selected_indices" ``numpy.ndarray`` or ``None`` selected graphic data indices - "world_object" ``pygfx.WorldObject`` pygfx World Object - "new_data" ``(float, float)`` current bounds in world coordinates, NOT necessarily the same as "selected_indices". - "graphic" ``Graphic`` the selection graphic - "delta" ``numpy.ndarray`` the delta vector of the graphic in NDC - "pygfx_event" ``pygfx.Event`` pygfx Event - "selected_data" ``numpy.ndarray`` or ``None`` selected graphic data - "move_info" ``MoveInfo`` last position and event source (pygfx.Mesh or pygfx.Line) - ===================== =============================== ======================================================================================= - """ def __init__( - self, parent, selection: Tuple[int, int], axis: str, limits: Tuple[int, int] + self, value: Tuple[int, int], axis: str, limits: Tuple[int, int] ): - super().__init__(parent, data=selection) + super().__init__() self._axis = axis self._limits = limits + self._value = value - self._set(selection) + @property + def value(self) -> float: + """ + selection in world space, NOT data space + """ + return self._value @property def axis(self) -> str: """one of "x" | "y" """ return self._axis - def _set(self, value: Tuple[float, float]): + def set_value(self, graphic, value: Tuple[float, float]): # sets new bounds if not isinstance(value, tuple): raise TypeError( @@ -103,71 +107,41 @@ def _set(self, value: Tuple[float, float]): if self.axis == "x": # change left x position of the fill mesh - self._parent.fill.geometry.positions.data[mesh_masks.x_left] = value[0] + graphic.fill.geometry.positions.data[mesh_masks.x_left] = value[0] # change right x position of the fill mesh - self._parent.fill.geometry.positions.data[mesh_masks.x_right] = value[1] + graphic.fill.geometry.positions.data[mesh_masks.x_right] = value[1] # change x position of the left edge line - self._parent.edges[0].geometry.positions.data[:, 0] = value[0] + graphic.edges[0].geometry.positions.data[:, 0] = value[0] # change x position of the right edge line - self._parent.edges[1].geometry.positions.data[:, 0] = value[1] + graphic.edges[1].geometry.positions.data[:, 0] = value[1] elif self.axis == "y": # change bottom y position of the fill mesh - self._parent.fill.geometry.positions.data[mesh_masks.y_bottom] = value[0] + graphic.fill.geometry.positions.data[mesh_masks.y_bottom] = value[0] # change top position of the fill mesh - self._parent.fill.geometry.positions.data[mesh_masks.y_top] = value[1] + graphic.fill.geometry.positions.data[mesh_masks.y_top] = value[1] # change y position of the bottom edge line - self._parent.edges[0].geometry.positions.data[:, 1] = value[0] + graphic.edges[0].geometry.positions.data[:, 1] = value[0] # change y position of the top edge line - self._parent.edges[1].geometry.positions.data[:, 1] = value[1] + graphic.edges[1].geometry.positions.data[:, 1] = value[1] - self._data = value # (value[0], value[1]) + self._value = value # (value[0], value[1]) # send changes to GPU - self._parent.fill.geometry.positions.update_range() - - self._parent.edges[0].geometry.positions.update_range() - self._parent.edges[1].geometry.positions.update_range() + graphic.fill.geometry.positions.update_range() - # calls any events - self._feature_changed(key=None, new_data=value) + graphic.edges[0].geometry.positions.update_range() + graphic.edges[1].geometry.positions.update_range() - def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): - if len(self._event_handlers) < 1: - return - - if self._parent.parent is not None: - selected_ixs = self._parent.get_selected_indices() - selected_data = self._parent.get_selected_data() - else: - selected_ixs = None - selected_data = None - - # get pygfx event and reset it - pygfx_ev = self._parent._pygfx_event - self._parent._pygfx_event = None - - pick_info = { - "world_object": self._parent.world_object, - "new_data": new_data, - "selected_indices": selected_ixs, - "selected_data": selected_data, - "graphic": self._parent, - "delta": self._parent.delta, - "pygfx_event": pygfx_ev, - "move_info": self._parent._move_info, - } - - event_data = FeatureEvent(type="selection", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"LinearRegionSelectionFeature for {self._parent}" - return s + # send event + event = FeatureEvent("selection", {"value": value}) + self._call_event_handlers(event) + # TODO: user's selector event handlers can call event.graphic.get_selected_indices() to get the data index, + # and event.graphic.get_selected_data() to get the data under the selection + # this is probably a good idea so that the data isn't sliced until it's actually necessary From ab80033c9e229b6817eee7ae65158cbd2b9b0eba Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 25 May 2024 05:14:51 -0400 Subject: [PATCH 053/196] offset and rotation for base graphic --- fastplotlib/graphics/_base.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 443602b2c..bbb2051f5 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -50,12 +50,12 @@ def name(self, value: str): self._name.set_value(self, value) @property - def offset(self) -> tuple: - """Offset position of the graphic, [x, y, z]""" + def offset(self) -> np.ndarray: + """Offset position of the graphic, array: [x, y, z]""" return self._offset.value @offset.setter - def offset(self, value: tuple[float, float, float]): + def offset(self, value: np.ndarray | list | tuple): self._offset.set_value(self, value) @property @@ -64,7 +64,7 @@ def rotation(self) -> np.ndarray: return self._rotation.value @rotation.setter - def rotation(self, value: tuple[float, float, float, float]): + def rotation(self, value: np.ndarray | list | tuple): self._rotation.set_value(self, value) @property @@ -101,7 +101,8 @@ def __init_subclass__(cls, **kwargs): def __init__( self, name: str = None, - offset: tuple[float] = (0., 0., 0.), + offset: np.ndarray | list | tuple = (0., 0., 0.), + rotation: np.ndarray | list | tuple = (0., 0., 0., 1.), metadata: Any = None, collection_index: int = None, ): @@ -122,7 +123,6 @@ def __init__( self.metadata = metadata self.collection_index = collection_index self.registered_callbacks = dict() - # self.present = PresentFeature(parent=self) # store hex id str of Graphic instance mem location self._fpl_address: HexStr = hex(id(self)) @@ -138,7 +138,7 @@ def __init__( # all the common features self._name = Name(name) self._deleted = Deleted(False) - self._rotation = None # set later when world object is set + self._rotation = Rotation(rotation) # set later when world object is set self._offset = Offset(offset) self._visible = Visible(True) @@ -151,7 +151,13 @@ def world_object(self) -> pygfx.WorldObject: def _set_world_object(self, wo: pygfx.WorldObject): WORLD_OBJECTS[self._fpl_address] = wo - self._rotation = Rotation(self.world_object.world.rotation[:]) + # set offset if it's not (0., 0., 0.) + if not all(self.world_object.world.position == self.offset): + self.offset = self.offset + + # set rotation if it's not (0., 0., 0., 1.) + if not all(self.world_object.world.rotation == self.rotation): + self.rotation = self.rotation def detach_feature(self, feature: str): raise NotImplementedError From da433ded5906d2e7daabb215f147c8a0e4f63426 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 25 May 2024 05:15:09 -0400 Subject: [PATCH 054/196] feature event table --- fastplotlib/graphics/_features/_base.py | 33 +++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index d603e2a33..2761bf994 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -43,24 +43,21 @@ def to_gpu_supported_dtype(array): class FeatureEvent(pygfx.Event): """ - Dataclass that holds feature event information. Has ``type`` and ``pick_info`` attributes. - - Attributes - ---------- - type: str, example "colors" - - pick_info: dict: - - ============== ============================================================================= - key value - ============== ============================================================================= - "index" indices where feature data was changed, ``range`` object or ``List[int]`` - "world_object" world object the feature belongs to - "new_data: the new data for this feature - ============== ============================================================================= - - .. note:: - pick info varies between features, this is just the general structure + **All event instances have the following attributes** + + +------------+-------------+-----------------------------------------------+ + | attribute | type | description | + +============+=============+===============================================+ + | type | str | "colors" - name of the event | + +------------+-------------+-----------------------------------------------+ + | graphic | Graphic | graphic instance that the event is from | + +------------+-------------+-----------------------------------------------+ + | info | dict | event info dictionary (see below) | + +------------+-------------+-----------------------------------------------+ + | target | WorldObject | pygfx rendering engine object for the graphic | + +------------+-------------+-----------------------------------------------+ + | time_stamp | float | time when the event occured, in ms | + +------------+-------------+-----------------------------------------------+ """ From d12d4faec367e4865e7aee123d775da2cf4ab6bc Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 25 May 2024 05:15:34 -0400 Subject: [PATCH 055/196] position feature event tables --- .../graphics/_features/_positions_graphics.py | 68 +++++++++++++++---- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index 37ac355a5..a90e36806 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -17,7 +17,18 @@ class VertexColors(BufferManager): """ - Manages the color buffer for :class:`LineGraphic` or :class:`ScatterGraphic` + + **info dict** + +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ + | dict key | value type | value description | + +============+===========================================================+==================================================================================+ + | key | int | slice | np.ndarray[int | bool] | tuple[slice, ...] | key at which colors were indexed/sliced | + +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ + | value | np.ndarray | new color values for points that were changed, shape is [n_points_changed, RGBA] | + +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ + | user_value | str | np.ndarray | tuple[float] | list[float] | list[str] | user input value that was parsed into the RGBA array | + +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ + """ def __init__( @@ -28,7 +39,7 @@ def __init__( isolated_buffer: bool = True, ): """ - ColorFeature + Manages the vertex color buffer for :class:`LineGraphic` or :class:`ScatterGraphic` Parameters ---------- @@ -50,19 +61,20 @@ def __init__( def __setitem__( self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], - value: str | np.ndarray | tuple[float] | list[float] | list[str] + user_value: str | np.ndarray | tuple[float] | list[float] | list[str] ): if isinstance(key, tuple): # directly setting RGBA values for points, we do no parsing - if not isinstance(value, (int, float, np.ndarray)): + if not isinstance(user_value, (int, float, np.ndarray)): raise TypeError( "Can only set from int, float, or array to set colors directly by slicing the entire array" ) + value = user_value elif isinstance(key, int): # set color of one point n_colors = 1 - value = parse_colors(value, n_colors) + value = parse_colors(user_value, n_colors) elif isinstance(key, slice): # find n_colors by converting slice to range and then parse colors @@ -70,7 +82,7 @@ def __setitem__( n_colors = len(range(start, stop, step)) - value = parse_colors(value, n_colors) + value = parse_colors(user_value, n_colors) elif isinstance(key, (np.ndarray, list)): if isinstance(key, list): @@ -93,7 +105,7 @@ def __setitem__( else: raise TypeError("If slicing colors with an array, it must be a 1D bool or int array") - value = parse_colors(value, n_colors) + value = parse_colors(user_value, n_colors) else: raise TypeError @@ -102,7 +114,16 @@ def __setitem__( self._update_range(key) - self._emit_event("colors", key, value) + if len(self._event_handlers) < 1: + return + + event_info = { + "key": key, + "value": value, + "user_value": user_value, + } + event = FeatureEvent("colors", info=event_info) + self._call_event_handlers(event) class UniformColor(GraphicFeature): @@ -143,11 +164,22 @@ def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Colo class VertexPositions(BufferManager): """ - Manages the vertex positions buffer shown in the graphic. - Supports fancy indexing if the data array also supports it. + +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ + | dict key | value type | value description | + +==========+==========================================================+==========================================================================================+ + | key | int | slice | np.ndarray[int | bool] | tuple[slice, ...] | key at which vertex positions data were indexed/sliced | + +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ + | value | np.ndarray | float | list[float] | new data values for points that were changed, shape depends on the indices that were set | + +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ + """ def __init__(self, data: Any, isolated_buffer: bool = True): + """ + Manages the vertex positions buffer shown in the graphic. + Supports fancy indexing if the data array also supports it. + """ + data = self._fix_data(data) super().__init__(data, isolated_buffer=isolated_buffer) @@ -170,7 +202,7 @@ def _fix_data(self, data): return to_gpu_supported_dtype(data) - def __setitem__(self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], value): + def __setitem__(self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], value: np.ndarray | float | list[float]): # directly use the key to slice the buffer self.buffer.data[key] = value @@ -183,8 +215,13 @@ def __setitem__(self, key: int | slice | np.ndarray[int | bool] | tuple[slice, . class PointsSizesFeature(BufferManager): """ - Access to the vertex buffer data shown in the graphic. - Supports fancy indexing if the data array also supports it. + +----------+-------------------------------------------------------------------+----------------------------------------------+ + | dict key | value type | value description | + +==========+===================================================================+==============================================+ + | key | int | slice | np.ndarray[int | bool] | list[int | bool] | key at which point sizes indexed/sliced | + +----------+-------------------------------------------------------------------+----------------------------------------------+ + | value | int | float | np.ndarray | list[int | float] | tuple[int | float] | new size values for points that were changed | + +----------+-------------------------------------------------------------------+----------------------------------------------+ """ def __init__( @@ -193,6 +230,9 @@ def __init__( n_datapoints: int, isolated_buffer: bool = True ): + """ + Manages sizes buffer of scatter points. + """ sizes = self._fix_sizes(sizes, n_datapoints) super().__init__(data=sizes, isolated_buffer=isolated_buffer) @@ -224,7 +264,7 @@ def _fix_sizes(self, sizes: int | float | np.ndarray | list[int | float] | tuple return sizes - def __setitem__(self, key, value): + def __setitem__(self, key: int | slice | np.ndarray[int | bool] | list[int | bool], value: int | float | np.ndarray | list[int | float] | tuple[int | float]): # this is a very simple 1D buffer, no parsing required, directly set buffer self.buffer.data[key] = value self._update_range(key) From 570683a84bf4c3dfa9f0fdf9334b725e2779e9cc Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 25 May 2024 05:15:57 -0400 Subject: [PATCH 056/196] rotation and offset features --- fastplotlib/graphics/_features/_common.py | 47 +++++++++++++++-------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/fastplotlib/graphics/_features/_common.py b/fastplotlib/graphics/_features/_common.py index aa44bde5a..bd2604386 100644 --- a/fastplotlib/graphics/_features/_common.py +++ b/fastplotlib/graphics/_features/_common.py @@ -1,3 +1,5 @@ +import numpy as np + from ._base import GraphicFeature, FeatureEvent @@ -26,20 +28,26 @@ def set_value(self, graphic, value: str): class Offset(GraphicFeature): """Offset position of the graphic, [x, y, z]""" - def __init__(self, value: tuple[float, float, float]): - self._value = value + def __init__(self, value: np.ndarray | list | tuple): + self._validate(value) + self._value = np.array(value) + self._value.flags.writeable = False super().__init__() + def _validate(self, value): + if not len(value) == 3: + raise ValueError("offset must be a list, tuple, or array of 3 float values") + @property - def value(self) -> tuple[float, float, float]: + def value(self) -> np.ndarray: return self._value - def set_value(self, graphic, value: tuple[float, float, float]): - if not len(value) == 3: - raise ValueError("offset must be a list, tuple, or array of 3 float values") + def set_value(self, graphic, value: np.ndarray | list | tuple): + self._validate(value) - graphic.position = value - self._value = value + graphic.world_object.world.position = value + self._value = graphic.world_object.world.position.copy() + self._value.flags.writeable = False event = FeatureEvent(type="offset", info={"value": value}) self._call_event_handlers(event) @@ -47,21 +55,26 @@ def set_value(self, graphic, value: tuple[float, float, float]): class Rotation(GraphicFeature): """Graphic rotation quaternion""" - def __init__(self, value: tuple[float, float, float, float]): - self._value = value + def __init__(self, value: np.ndarray | list | tuple): + self._validate(value) + self._value = np.array(value) + self._value.flags.writeable = False super().__init__() + def _validate(self, value): + if not len(value) == 4: + raise ValueError("rotation quaternion must be a list, tuple, or array of 4 float values") + @property - def value(self) -> tuple[float, float, float, float]: + def value(self) -> np.ndarray: return self._value - def set_value(self, graphic, value: tuple[float, float, float, float]): - if not len(value) == 4: - raise ValueError("rotation must be a list, tuple, or array of 4 float values" - "representing a quaternion") + def set_value(self, graphic, value: np.ndarray | list | tuple): + self._validate(value) - graphic.rotation = value - self._value = value + graphic.world_object.world.rotation = value + self._value = graphic.world_object.world.rotation.copy() + self._value.flags.writeable = False event = FeatureEvent(type="rotation", info={"value": value}) self._call_event_handlers(event) From ec6b2f43455cbe84899e4562629c49a7528858a4 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 25 May 2024 05:17:06 -0400 Subject: [PATCH 057/196] work on selectors, WIP, linear region selector inits properly and moves for x-axis --- .../graphics/_features/_selection_features.py | 65 ++-- fastplotlib/graphics/line.py | 62 +++- .../graphics/selectors/_base_selector.py | 11 +- fastplotlib/graphics/selectors/_linear.py | 83 +++-- .../graphics/selectors/_linear_region.py | 348 ++++++++++-------- 5 files changed, 342 insertions(+), 227 deletions(-) diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index 9c27f3326..9601468fd 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -1,5 +1,3 @@ -from typing import Tuple, Union, Any - import numpy as np from ...utils import mesh_masks @@ -12,7 +10,7 @@ class LinearSelectionFeature(GraphicFeature): Manages the linear selection and callbacks """ - def __init__(self, axis: str, value: float, limits: Tuple[float, float]): + def __init__(self, axis: str, value: float, limits: tuple[float, float]): """ Parameters @@ -42,21 +40,21 @@ def value(self) -> float: # need to decide if we give a value based on the selector's parent graphic, if there is one return self._value - def set_value(self, graphic, value: float): + def set_value(self, selector, value: float): if not (self._limits[0] <= value <= self._limits[1]): return - offset = list(graphic.offset) + offset = list(selector.offset) if self._axis == "x": offset[0] = value else: offset[1] = value - graphic.offset = offset + selector.offset = offset self._value = value - event = FeatureEvent("selection", {"index": graphic.get_selected_index()}) + event = FeatureEvent("selection", {"index": selector.get_selected_index()}) self._call_event_handlers(event) @@ -66,16 +64,16 @@ class LinearRegionSelectionFeature(GraphicFeature): """ def __init__( - self, value: Tuple[int, int], axis: str, limits: Tuple[int, int] + self, value: tuple[int, int], axis: str, limits: tuple[float, float] ): super().__init__() self._axis = axis self._limits = limits - self._value = value + self._value = tuple(int(v) for v in value) @property - def value(self) -> float: + def value(self) -> np.ndarray[int]: """ selection in world space, NOT data space """ @@ -86,14 +84,26 @@ def axis(self) -> str: """one of "x" | "y" """ return self._axis - def set_value(self, graphic, value: Tuple[float, float]): - # sets new bounds + def set_value(self, selector, value: tuple[float, float]): + """ + Set start, stop range of selector + + Parameters + ---------- + selector: LinearRegionSelector + + value: (float, float) + in world space, NOT data space + + """ if not isinstance(value, tuple): raise TypeError( "Bounds must be a tuple in the form of `(min_bound, max_bound)`, " "where `min_bound` and `max_bound` are numeric values." ) + # value = tuple(int(v) for v in value) + # make sure bounds not exceeded for v in value: if not (self._limits[0] <= v <= self._limits[1]): @@ -107,41 +117,44 @@ def set_value(self, graphic, value: Tuple[float, float]): if self.axis == "x": # change left x position of the fill mesh - graphic.fill.geometry.positions.data[mesh_masks.x_left] = value[0] + selector.fill.geometry.positions.data[mesh_masks.x_left] = value[0] # change right x position of the fill mesh - graphic.fill.geometry.positions.data[mesh_masks.x_right] = value[1] + selector.fill.geometry.positions.data[mesh_masks.x_right] = value[1] # change x position of the left edge line - graphic.edges[0].geometry.positions.data[:, 0] = value[0] + selector.edges[0].geometry.positions.data[:, 0] = value[0] # change x position of the right edge line - graphic.edges[1].geometry.positions.data[:, 0] = value[1] + selector.edges[1].geometry.positions.data[:, 0] = value[1] elif self.axis == "y": # change bottom y position of the fill mesh - graphic.fill.geometry.positions.data[mesh_masks.y_bottom] = value[0] + selector.fill.geometry.positions.data[mesh_masks.y_bottom] = value[0] # change top position of the fill mesh - graphic.fill.geometry.positions.data[mesh_masks.y_top] = value[1] + selector.fill.geometry.positions.data[mesh_masks.y_top] = value[1] # change y position of the bottom edge line - graphic.edges[0].geometry.positions.data[:, 1] = value[0] + selector.edges[0].geometry.positions.data[:, 1] = value[0] # change y position of the top edge line - graphic.edges[1].geometry.positions.data[:, 1] = value[1] + selector.edges[1].geometry.positions.data[:, 1] = value[1] - self._value = value # (value[0], value[1]) + self._value = np.array(value) # (value[0], value[1]) # send changes to GPU - graphic.fill.geometry.positions.update_range() + selector.fill.geometry.positions.update_range() - graphic.edges[0].geometry.positions.update_range() - graphic.edges[1].geometry.positions.update_range() + selector.edges[0].geometry.positions.update_range() + selector.edges[1].geometry.positions.update_range() # send event - event = FeatureEvent("selection", {"value": value}) - self._call_event_handlers(event) + if len(self._event_handlers) > 0: + return + + # event = FeatureEvent("selection", {"indices": selector.get_selected_indices()}) + # self._call_event_handlers(event) # TODO: user's selector event handlers can call event.graphic.get_selected_indices() to get the data index, # and event.graphic.get_selected_data() to get the data under the selection # this is probably a good idea so that the data isn't sliced until it's actually necessary diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 62160c164..7a46f5195 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -177,12 +177,12 @@ def add_linear_selector( ) self._plot_area.add_graphic(selector, center=False) - selector.position_z = self.position_z + 1 + selector.offset = selector.offset + (0., 0., self.offset[-1] + 1) return weakref.proxy(selector) def add_linear_region_selector( - self, padding: float = 100.0, **kwargs + self, padding: float = 100.0, axis="x", **kwargs ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, @@ -203,28 +203,54 @@ def add_linear_region_selector( """ - ( - bounds_init, - limits, - size, - origin, - axis, - end_points, - ) = self._get_linear_selector_init_args(padding, **kwargs) + # ( + # bounds_init, + # limits, + # size, + # origin, + # axis, + # end_points, + # ) = self._get_linear_selector_init_args(padding, **kwargs) + + 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 = (magn_vals.min() - padding, magn_vals.max() + padding) + + # center of selector along the other axis + center = np.nanmean(magn_vals) # create selector selector = LinearRegionSelector( - bounds=bounds_init, + selection=bounds_init, limits=limits, size=size, - origin=origin, + center=center, parent=self, **kwargs, ) self._plot_area.add_graphic(selector, center=False) - # so that it is below this graphic - selector.position_z = self.position_z - 1 + + # place selector below this graphic + selector.offset = selector.offset + (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 @@ -241,7 +267,7 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): axis = "x" if axis == "x": - offset = self.position_x + offset = self.offset[0] # x limits limits = (data[0, 0] + offset, data[-1, 0] + offset) @@ -252,7 +278,7 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): position_y = (data[:, 1].min() + data[:, 1].max()) / 2 # need y offset too for this - origin = (limits[0] - offset, position_y + self.position_y) + origin = (limits[0] - offset, position_y + self.offset[1]) # endpoints of the data range # used by linear selector but not linear region @@ -261,7 +287,7 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): self.data.value[:, 1].max() + padding, ) else: - offset = self.position_y + offset = self.offset[1] # y limits limits = (data[0, 1] + offset, data[-1, 1] + offset) @@ -272,7 +298,7 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): position_x = (data[:, 0].min() + data[:, 0].max()) / 2 # need x offset too for this - origin = (position_x + self.position_x, limits[0] - offset) + origin = (position_x + self.offset[0], limits[0] - offset) end_points = ( self.data.value[:, 0].min() - padding, diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index f20eba4a0..88082bced 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -35,7 +35,7 @@ class MoveInfo: # Selector base class class BaseSelector(Graphic): - feature_events = ("selection",) + features = {"selection"} def __init__( self, @@ -46,6 +46,7 @@ def __init__( arrow_keys_modifier: str = None, axis: str = None, name: str = None, + parent: Graphic = None, ): if edges is None: edges = tuple() @@ -95,6 +96,8 @@ def __init__( self._pygfx_event = None + self._parent = parent + Graphic.__init__(self, name=name) def get_selected_index(self): @@ -110,7 +113,7 @@ def get_selected_data(self): raise NotImplementedError def _get_source(self, graphic): - if self.parent is None and graphic is None: + if self._parent is None and graphic is None: raise AttributeError( "No Graphic to apply selector. " "You must either set a ``parent`` Graphic on the selector, or pass a graphic." @@ -120,7 +123,7 @@ def _get_source(self, graphic): if graphic is not None: source = graphic else: - source = self.parent + source = self._parent return source @@ -262,7 +265,7 @@ def _move_to_pointer(self, ev): """ Calculates delta just using current world object position and calls self._move_graphic(). """ - current_position: np.ndarray = self.position + current_position: np.ndarray = self.offset # middle mouse button clicks if ev.button != 3: diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 82e553f0a..9c7eb6f77 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -17,6 +17,42 @@ class LinearSelector(BaseSelector): + @property + def selection(self) -> float: + """ + The selected data index. Index of the data under the selector + not the x or y value of the data but the index of the x or y value + """ + if self._parent is not None: + return self.get_selected_index() + # TODO: if no parent graphic is set, this just returns world position + # but should we change it? + return self._selection.value + + @selection.setter + def selection(self, index: int): + graphic = self._parent + + if "Line" in graphic.__class__.__name__ or "Scatter" in graphic.__class__.__name__: + if self.axis == "x": + geo_positions = graphic.data.value[:, 0] + offset = graphic.offset[0] + elif self.axis == "y": + geo_positions = graphic.data.value[:, 1] + offset = graphic.offset[1] + + # we want to find the geometry position at the desired index + position = geo_positions[index] + + elif "Image" in graphic.__class__.__name__: + # 1:1 mapping between geometry position and index + position = index + + # new world position for the selector + # offset + new_index + world_pos = offset + position + self._selection.set_value(self, world_pos) + @property def limits(self) -> Tuple[float, float]: return self._limits @@ -79,17 +115,6 @@ def __init__( name: str, optional name of line slider - Features - -------- - - selection: :class:`.LinearSelectionFeature` - ``selection()`` returns the current selector position in world coordinates. - Use ``get_selected_index()`` to get the currently selected index in data - space. - Use ``selection.add_event_handler()`` to add callback functions that are - called when the LinearSelector selection changes. See feature class for - event pick_info table - """ if len(limits) != 2: raise ValueError("limits must be a tuple of 2 integers, i.e. (int, int)") @@ -144,8 +169,6 @@ def __init__( self._move_info: dict = None - self.parent = parent - self._block_ipywidget_call = False self._handled_widgets = list() @@ -158,19 +181,26 @@ def __init__( arrow_keys_modifier=arrow_keys_modifier, axis=axis, name=name, + parent=parent, ) self._set_world_object(world_object) - self.selection = LinearSelectionFeature( - self, axis=axis, value=selection, limits=self._limits + self._selection = LinearSelectionFeature( + axis=axis, value=selection, limits=self._limits ) - self.selection = selection + if self._parent is not None: + self.selection = selection + else: + self._selection.set_value(self, selection) + + # update any ipywidgets + self.add_event_handler("selection", self._update_ipywidgets) def _setup_ipywidget_slider(self, widget): # setup an ipywidget slider with bidirectional callbacks to this LinearSelector - value = self.selection() + value = self.selection if isinstance(widget, ipywidgets.IntSlider): value = int(value) @@ -180,16 +210,13 @@ def _setup_ipywidget_slider(self, widget): # user changes widget -> linear selection changes widget.observe(self._ipywidget_callback, "value") - # user changes linear selection -> widget changes - self.selection.add_event_handler(self._update_ipywidgets) - self._handled_widgets.append(widget) def _update_ipywidgets(self, ev): # update the ipywidget sliders when LinearSelector value changes self._block_ipywidget_call = True # prevent infinite recursion - value = ev.pick_info["new_data"] + value = ev.info["index"] # update all the handled slider widgets for widget in self._handled_widgets: if isinstance(widget, ipywidgets.IntSlider): @@ -200,7 +227,7 @@ def _update_ipywidgets(self, ev): self._block_ipywidget_call = False def _ipywidget_callback(self, change): - # update the LinearSelector if the ipywidget value changes + # update the LinearSelector when the ipywidget value changes if self._block_ipywidget_call or self._moving: return @@ -249,9 +276,9 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs): cls = getattr(ipywidgets, kind) - value = self.selection() + value = self.selection if "Int" in kind: - value = int(self.selection()) + value = int(self.selection) slider = cls( min=self.limits[0], @@ -335,7 +362,7 @@ def _get_selected_index(self, graphic): if "Line" in graphic.__class__.__name__: # we want to find the index of the geometry position that is closest to the slider's geometry position - find_value = self.selection() - offset + find_value = self._selection.value - offset # get closest data index to the world space position of the slider idx = np.searchsorted(geo_positions, find_value, side="left") @@ -354,7 +381,7 @@ def _get_selected_index(self, graphic): or "Image" in graphic.__class__.__name__ ): # indices map directly to grid geometry for image data buffer - index = self.selection() - offset + index = self._selection.value - offset return round(index) def _move_graphic(self, delta: np.ndarray): @@ -369,9 +396,9 @@ def _move_graphic(self, delta: np.ndarray): """ if self.axis == "x": - self.selection = self.selection() + delta[0] + self.selection = self._selection + delta[0] else: - self.selection = self.selection() + delta[1] + self.selection = self._selection + delta[1] def _fpl_cleanup(self): for widget in self._handled_widgets: diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 09c134800..a13d4adcd 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -16,6 +16,60 @@ class LinearRegionSelector(BaseSelector): + @property + def selection(self) -> tuple[int, int] | List[tuple[int, int]]: + """ + (min, max) of data value along selector's axis, in data space + """ + # TODO: This probably does not account for rotation since world.position + # does not account for rotation, we can do this later + if self._parent is not None: + # just subtract parent offset to map from world to data space + if self.axis == "x": + offset = self._parent.offset[0] + elif self.axis == "y": + offset = self._parent.offset[1] + + return self._selection.value.copy() - offset + + # + # indices = self.get_selected_indices() + # if isinstance(indices, np.ndarray): + # # this can be used directly to create a range object + # return indices[0], indices[-1] + 1 + # # if a collection is under the selector + # elif isinstance(indices, list): + # ranges = list() + # for ixs in indices: + # ranges.append((ixs[0], ixs[-1] + 1)) + # + # return ranges + + # TODO: if no parent graphic is set, this just returns world positions + # but should we change it? + return self._selection.value + + @selection.setter + def selection(self, selection: tuple[int, int]): + # set (xmin, xmax), or (ymin, ymax) of the selector in data space + graphic = self._parent + + start, stop = selection + + if isinstance(graphic, GraphicCollection): + pass + + if self.axis == "x": + offset = graphic.offset[0] + elif self.axis == "y": + offset = graphic.offset[1] + + # add the offset + start += offset + stop += offset + + self._selection.set_value(self, (start, stop)) + @property def limits(self) -> Tuple[float, float]: return self._limits @@ -32,19 +86,19 @@ def limits(self, values: Tuple[float, float]): self.selection._limits = self._limits def __init__( - self, - bounds: Tuple[int, int], - limits: Tuple[int, int], - size: int, - origin: Tuple[int, int], - axis: str = "x", - parent: Graphic = None, - resizable: bool = True, - fill_color=(0, 0, 0.35), - edge_color=(0.8, 0.8, 0), - edge_thickness: int = 3, - arrow_keys_modifier: str = "Shift", - name: str = None, + self, + selection: Tuple[int, int], + limits: Tuple[int, int], + size: tuple[float, float], + center: float, + axis: str = "x", + parent: Graphic = None, + resizable: bool = True, + fill_color=(0, 0, 0.35), + edge_color=(0.8, 0.8, 0), + edge_thickness: int = 3, + arrow_keys_modifier: str = "Shift", + name: str = None, ): """ Create a LinearRegionSelector graphic which can be moved only along either the x-axis or y-axis. @@ -60,18 +114,15 @@ def __init__( Parameters ---------- - bounds: (int, int) - the initial bounds of the linear selector + selection: (int, int) + (min, max) values of the "axis" under the selector limits: (int, int) - (min limit, max limit) for the selector + (min limit, max limit) of values on the axis size: int height or width of the selector - origin: (int, int) - initial position of the selector - axis: str, default "x" "x" | "y", axis for the selector @@ -108,43 +159,32 @@ def __init__( """ # lots of very close to zero values etc. so round them, otherwise things get weird - bounds = tuple(map(round, bounds)) - self._limits = tuple(map(round, limits)) - origin = tuple(map(round, origin)) + if not len(selection) == 2: + raise ValueError + + selection = np.asarray(selection) + + if not len(limits) == 2: + raise ValueError + + self._limits = np.asarray(limits) # TODO: sanity checks, we recommend users to add LinearSelection using the add_linear_selector() methods # TODO: so we can worry about the sanity checks later - # if axis == "x": - # if limits[0] != origin[0] != bounds[0]: - # raise ValueError( - # f"limits[0] != position[0] != bounds[0]\n" - # f"{limits[0]} != {origin[0]} != {bounds[0]}" - # ) - # - # elif axis == "y": - # # initial y-position is position[1] - # if limits[0] != origin[1] != bounds[0]: - # raise ValueError( - # f"limits[0] != position[1] != bounds[0]\n" - # f"{limits[0]} != {origin[1]} != {bounds[0]}" - # ) - - self.parent = parent - - # 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() + mesh_size = np.ptp(size) + if axis == "x": mesh = pygfx.Mesh( - pygfx.box_geometry(1, size, 1), + pygfx.box_geometry(1, mesh_size, 1), pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color), pick_write=True), ) elif axis == "y": mesh = pygfx.Mesh( - pygfx.box_geometry(size, 1, 1), + pygfx.box_geometry(mesh_size, 1, 1), pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color), pick_write=True), ) else: @@ -152,91 +192,77 @@ def __init__( # the fill of the selection self.fill = mesh - self.fill.world.position = (*origin, -2) + # no x, y offsets for linear region selector + # everything is done by setting the mesh data + # and line positions + self.fill.world.position = (0, 0, -2) group.add(self.fill) self._resizable = resizable if axis == "x": - # position data for the left edge line - left_line_data = np.array( + # just some data to initialize the edge lines + init_line_data = np.array( [ - [origin[0], (-size / 2) + origin[1], 0.5], - [origin[0], (size / 2) + origin[1], 0.5], + [0, size[0], 0], + [0, size[1], 0] ] ).astype(np.float32) - left_line = pygfx.Line( - pygfx.Geometry(positions=left_line_data), - pygfx.LineMaterial( - thickness=edge_thickness, color=edge_color, pick_write=True - ), - ) - - # position data for the right edge line - right_line_data = np.array( - [ - [bounds[1], (-size / 2) + origin[1], 0.5], - [bounds[1], (size / 2) + origin[1], 0.5], - ] - ).astype(np.float32) - - right_line = pygfx.Line( - pygfx.Geometry(positions=right_line_data), - pygfx.LineMaterial( - thickness=edge_thickness, color=edge_color, pick_write=True - ), - ) - - self.edges: Tuple[pygfx.Line, pygfx.Line] = (left_line, right_line) + if parent is not None: + parent_offset = parent.offset[0] + else: + parent_offset = 0 elif axis == "y": - # position data for the left edge line - bottom_line_data = np.array( + # just some line data to initialize y axis edge lines + init_line_data = np.array( [ - [(-size / 2) + origin[0], origin[1], 0.5], - [(size / 2) + origin[0], origin[1], 0.5], + [size[0], 0, 0], + [size[1], 0, 0], ] ).astype(np.float32) - bottom_line = pygfx.Line( - pygfx.Geometry(positions=bottom_line_data), - pygfx.LineMaterial( - thickness=edge_thickness, color=edge_color, pick_write=True - ), - ) - - # position data for the right edge line - top_line_data = np.array( - [ - [(-size / 2) + origin[0], bounds[1], 0.5], - [(size / 2) + origin[0], bounds[1], 0.5], - ] - ).astype(np.float32) - - top_line = pygfx.Line( - pygfx.Geometry(positions=top_line_data), - pygfx.LineMaterial( - thickness=edge_thickness, color=edge_color, pick_write=True - ), - ) - - self.edges: Tuple[pygfx.Line, pygfx.Line] = (bottom_line, top_line) + if parent is not None: + parent_offset = parent.offset[1] + else: + parent_offset = 0 else: raise ValueError("axis argument must be one of 'x' or 'y'") + line0 = pygfx.Line( + pygfx.Geometry(positions=init_line_data.copy()), # copy so the line buffer is isolated + pygfx.LineMaterial( + thickness=edge_thickness, color=edge_color, pick_write=True + ), + ) + line1 = pygfx.Line( + pygfx.Geometry(positions=init_line_data.copy()), # copy so the line buffer is isolated + pygfx.LineMaterial( + thickness=edge_thickness, color=edge_color, pick_write=True + ), + ) + + self.edges: Tuple[pygfx.Line, pygfx.Line] = (line0, line1) + # add the edge lines for edge in self.edges: - edge.world.z = -1 + edge.world.z = -0.5 group.add(edge) # set the initial bounds of the selector - self.selection = LinearRegionSelectionFeature( - self, bounds, axis=axis, limits=self._limits + # compensate for any offset from the parent graphic + # selection feature only works in world space, not data space + self._selection = LinearRegionSelectionFeature( + selection + parent_offset, + axis=axis, + limits=self._limits + parent_offset ) + print(f"sel value after construct: {selection}") + self._handled_widgets = list() self._block_ipywidget_call = False self._pygfx_event = None @@ -249,13 +275,25 @@ def __init__( arrow_keys_modifier=arrow_keys_modifier, axis=axis, name=name, + parent=parent ) self._set_world_object(group) + self.selection = selection + + print(f"sel value after set: {selection}") + + if self.axis == "x": + offset = (0, center, 0) + elif self.axis == "y": + offset = (center, 0, 0) + + self.offset = self.offset + offset + def get_selected_data( - self, graphic: Graphic = None - ) -> Union[np.ndarray, List[np.ndarray], None]: + self, graphic: Graphic = None + ) -> Union[np.ndarray, List[np.ndarray]]: """ Get the ``Graphic`` data bounded by the current selection. Returns a view of the full data array. @@ -289,33 +327,34 @@ def get_selected_data( data_selections: List[np.ndarray] = list() for i, g in enumerate(source.graphics): - if ixs[i].size == 0: - data_selections.append(None) - else: - s = slice(ixs[i][0], ixs[i][-1]) - data_selections.append(g.data.buffer.data[s]) + # if ixs[i].size == 0: + # data_selections.append(np.array([], dtype=np.float32)) + # else: + s = slice(ixs[i][0], ixs[i][-1]) + # slices n_datapoints dim + data_selections.append(g.data.buffer.data[s]) return source[:].data[s] - # just for one Line graphic else: - if ixs.size == 0: - return None + # just for one graphic + # if ixs.size == 0: + # return np.array([], dtype=np.float32) s = slice(ixs[0], ixs[-1]) + # slices n_datapoints dim return source.data.buffer.data[s] - if ( - "Heatmap" in source.__class__.__name__ - or "Image" in source.__class__.__name__ - ): + if "Image" in source.__class__.__name__: s = slice(ixs[0], ixs[-1]) + if self.axis == "x": - return source.data()[:, s] + return source.data.value[:, s] + elif self.axis == "y": - return source.data()[s] + return source.data.value[s] def get_selected_indices( - self, graphic: Graphic = None + self, graphic: Graphic = None ) -> Union[np.ndarray, List[np.ndarray]]: """ Returns the indices of the ``Graphic`` data bounded by the current selection. @@ -336,47 +375,52 @@ def get_selected_indices( data indices of the selection, list of np.ndarray if graphic is LineCollection """ + # we get the indices from the source graphic source = self._get_source(graphic) - # if the graphic position is not at (0, 0) then the bounds must be offset - offset = getattr(source, f"position_{self.selection.axis}") - offset_bounds = tuple(v - offset for v in self.selection()) - - # need them to be int to use as indices - offset_bounds = tuple(map(int, offset_bounds)) - - if self.selection.axis == "x": + # get the offset of the source graphic + if self.axis == "x": + source_offset = source.offset[0] dim = 0 - else: + elif self.axis == "y": + source_offset = source.offset[1] dim = 1 + # selector (min, max) in world space + bounds = self._selection.value + # subtract offset to get the (min, max) bounded region + # of the source graphic in world space + bounds = tuple(v - source_offset for v in bounds) + + # # need them to be int to use as indices + # offset_bounds = tuple(map(int, offset_bounds)) + if "Line" in source.__class__.__name__: - # now we need to map from graphic space to data space - # we can have more than 1 datapoint between two integer locations in the world space + # now we need to map from world space to data space + # gets indices corresponding to n_datapoints dim + # data space is [n_datapoints, xyz], so we return + # indices that can be used to slice `n_datapoints` if isinstance(source, GraphicCollection): ixs = list() for g in source.graphics: # map for each graphic in the collection g_ixs = np.where( - (g.data()[:, dim] >= offset_bounds[0]) - & (g.data()[:, dim] <= offset_bounds[1]) + (g.data.value[:, dim] >= bounds[0]) + & (g.data.value[:, dim] <= bounds[1]) )[0] ixs.append(g_ixs) else: # map this only this graphic ixs = np.where( - (source.data()[:, dim] >= offset_bounds[0]) - & (source.data()[:, dim] <= offset_bounds[1]) + (source.data.value[:, dim] >= bounds[0]) + & (source.data.value[:, dim] <= bounds[1]) )[0] return ixs - if ( - "Heatmap" in source.__class__.__name__ - or "Image" in source.__class__.__name__ - ): + if "Image" in source.__class__.__name__: # indices map directly to grid geometry for image data buffer - ixs = np.arange(*self.selection(), dtype=int) + ixs = np.arange(*bounds, dtype=int) return ixs def make_ipywidget_slider(self, kind: str = "IntRangeSlider", **kwargs): @@ -410,9 +454,9 @@ def make_ipywidget_slider(self, kind: str = "IntRangeSlider", **kwargs): cls = getattr(ipywidgets, kind) - value = self.selection() + value = self.selection if "Int" in kind: - value = tuple(map(int, self.selection())) + value = tuple(map(int, self.selection)) slider = cls( min=self.limits[0], @@ -438,7 +482,7 @@ def add_ipywidget_handler(self, widget, step: Union[int, float] = None): """ if not isinstance( - widget, (ipywidgets.IntRangeSlider, ipywidgets.FloatRangeSlider) + widget, (ipywidgets.IntRangeSlider, ipywidgets.FloatRangeSlider) ): raise TypeError( f"`widget` must be one of: ipywidgets.IntRangeSlider or ipywidgets.FloatRangeSlider\n" @@ -457,7 +501,7 @@ def add_ipywidget_handler(self, widget, step: Union[int, float] = None): def _setup_ipywidget_slider(self, widget): # setup an ipywidget slider with bidirectional callbacks to this LinearSelector - value = self.selection() + value = self.selection if isinstance(widget, ipywidgets.IntSlider): value = tuple(map(int, value)) @@ -503,18 +547,19 @@ def _set_slider_layout(self, *args): def _move_graphic(self, delta: np.ndarray): # add delta to current bounds to get new positions - if self.selection.axis == "x": + # print(delta) + if self.axis == "x": # min and max of current bounds, i.e. the edges - xmin, xmax = self.selection() + xmin, xmax = self._selection.value # new left bound position bound0_new = xmin + delta[0] # new right bound position bound1_new = xmax + delta[0] - else: + elif self.axis == "y": # min and max of current bounds, i.e. the edges - ymin, ymax = self.selection() + ymin, ymax = self._selection.value # new bottom bound position bound0_new = ymin + delta[1] @@ -524,8 +569,9 @@ def _move_graphic(self, delta: np.ndarray): # move entire selector if source was fill if self._move_info.source == self.fill: - # set the new bounds - self.selection = (bound0_new, bound1_new) + # set the new bounds, in WORLD space + # don't set property because that is in data space! + self._selection.set_value(self, (bound0_new, bound1_new)) return # if selector is not resizable do nothing @@ -535,10 +581,10 @@ def _move_graphic(self, delta: np.ndarray): # if resizable, move edges if self._move_info.source == self.edges[0]: # change only left or bottom bound - self.selection = (bound0_new, self.selection()[1]) + self._selection.set_value(self, (bound0_new, self._selection.value[1])) elif self._move_info.source == self.edges[1]: # change only right or top bound - self.selection = (self.selection()[0], bound1_new) + self._selection.set_value(self, (self._selection.value[0], bound1_new)) else: return From db910d34cc98d07341d3ec387bbb4d89d8fd719c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 25 May 2024 05:28:07 -0400 Subject: [PATCH 058/196] proper centering --- fastplotlib/graphics/line.py | 3 ++- fastplotlib/graphics/selectors/_linear_region.py | 16 +++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 7a46f5195..3cd1be729 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -232,7 +232,7 @@ def add_linear_region_selector( limits = axis_vals[0], axis_vals[-1] # width or height of selector - size = (magn_vals.min() - padding, magn_vals.max() + padding) + size = int(np.ptp(magn_vals) + padding) # center of selector along the other axis center = np.nanmean(magn_vals) @@ -243,6 +243,7 @@ def add_linear_region_selector( limits=limits, size=size, center=center, + axis=axis, parent=self, **kwargs, ) diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index a13d4adcd..03a2de10b 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -89,7 +89,7 @@ def __init__( self, selection: Tuple[int, int], limits: Tuple[int, int], - size: tuple[float, float], + size: int, center: float, axis: str = "x", parent: Graphic = None, @@ -174,17 +174,15 @@ def __init__( group = pygfx.Group() - mesh_size = np.ptp(size) - if axis == "x": mesh = pygfx.Mesh( - pygfx.box_geometry(1, mesh_size, 1), + pygfx.box_geometry(1, size, 1), pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color), pick_write=True), ) elif axis == "y": mesh = pygfx.Mesh( - pygfx.box_geometry(mesh_size, 1, 1), + pygfx.box_geometry(size, 1, 1), pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color), pick_write=True), ) else: @@ -205,8 +203,8 @@ def __init__( # just some data to initialize the edge lines init_line_data = np.array( [ - [0, size[0], 0], - [0, size[1], 0] + [0, -size / 2, 0], + [0, size / 2, 0] ] ).astype(np.float32) @@ -219,8 +217,8 @@ def __init__( # just some line data to initialize y axis edge lines init_line_data = np.array( [ - [size[0], 0, 0], - [size[1], 0, 0], + [-size / 2, 0, 0], + [size / 2, 0, 0], ] ).astype(np.float32) From a09386bf6d87c6231515d572ca442abf0fa0efeb Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 25 May 2024 20:48:46 -0400 Subject: [PATCH 059/196] much simpliified and better linear region selector --- .../graphics/_features/_selection_features.py | 29 ++-- .../graphics/selectors/_base_selector.py | 4 +- .../graphics/selectors/_linear_region.py | 129 +++++++----------- 3 files changed, 66 insertions(+), 96 deletions(-) diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index 9601468fd..35538c28c 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -1,3 +1,5 @@ +from typing import Sequence + import numpy as np from ...utils import mesh_masks @@ -73,9 +75,9 @@ def __init__( self._value = tuple(int(v) for v in value) @property - def value(self) -> np.ndarray[int]: + def value(self) -> np.ndarray[float]: """ - selection in world space, NOT data space + (min, max) of the selection, in data space """ return self._value @@ -84,7 +86,7 @@ def axis(self) -> str: """one of "x" | "y" """ return self._axis - def set_value(self, selector, value: tuple[float, float]): + def set_value(self, selector, value: Sequence[float]): """ Set start, stop range of selector @@ -93,26 +95,21 @@ def set_value(self, selector, value: tuple[float, float]): selector: LinearRegionSelector value: (float, float) - in world space, NOT data space + (min, max) values in data space """ - if not isinstance(value, tuple): + if not len(value) == 2: raise TypeError( - "Bounds must be a tuple in the form of `(min_bound, max_bound)`, " - "where `min_bound` and `max_bound` are numeric values." + "selection must be a array, tuple, list, or sequence in the form of `(min, max)`, " + "where `min` and `max` are numeric values." ) - # value = tuple(int(v) for v in value) - - # make sure bounds not exceeded - for v in value: - if not (self._limits[0] <= v <= self._limits[1]): - return + # convert to array, clip values if they are beyond the limits + value = np.asarray(value, dtype=np.float32).clip(*self._limits) # make sure `selector width >= 2`, left edge must not move past right edge! # or bottom edge must not move past top edge! - # has to be at least 2 otherwise can't join datapoints for lines - if not (value[1] - value[0]) >= 2: + if not (value[1] - value[0]) >= 0: return if self.axis == "x": @@ -141,7 +138,7 @@ def set_value(self, selector, value: tuple[float, float]): # change y position of the top edge line selector.edges[1].geometry.positions.data[:, 1] = value[1] - self._value = np.array(value) # (value[0], value[1]) + self._value = value # send changes to GPU selector.fill.geometry.positions.update_range() diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 88082bced..8c0556ce5 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -45,8 +45,8 @@ def __init__( hover_responsive: Tuple[WorldObject, ...] = None, arrow_keys_modifier: str = None, axis: str = None, - name: str = None, parent: Graphic = None, + **kwargs, ): if edges is None: edges = tuple() @@ -98,7 +98,7 @@ def __init__( self._parent = parent - Graphic.__init__(self, name=name) + Graphic.__init__(self, **kwargs) def get_selected_index(self): """Not implemented for this selector""" diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 03a2de10b..ee538ab5f 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -17,20 +17,20 @@ class LinearRegionSelector(BaseSelector): @property - def selection(self) -> tuple[int, int] | List[tuple[int, int]]: + def selection(self) -> Sequence[float] | List[Sequence[float]]: """ (min, max) of data value along selector's axis, in data space """ # TODO: This probably does not account for rotation since world.position # does not account for rotation, we can do this later - if self._parent is not None: - # just subtract parent offset to map from world to data space - if self.axis == "x": - offset = self._parent.offset[0] - elif self.axis == "y": - offset = self._parent.offset[1] + # if self._parent is not None: + # # just subtract parent offset to map from world to data space + # if self.axis == "x": + # offset = self._parent.offset[0] + # elif self.axis == "y": + # offset = self._parent.offset[1] - return self._selection.value.copy() - offset + return self._selection.value.copy() # # indices = self.get_selected_indices() @@ -47,28 +47,28 @@ def selection(self) -> tuple[int, int] | List[tuple[int, int]]: # TODO: if no parent graphic is set, this just returns world positions # but should we change it? - return self._selection.value + # return self._selection.value @selection.setter - def selection(self, selection: tuple[int, int]): + def selection(self, selection: Sequence[float]): # set (xmin, xmax), or (ymin, ymax) of the selector in data space graphic = self._parent - start, stop = selection + # start, stop = selection if isinstance(graphic, GraphicCollection): pass - if self.axis == "x": - offset = graphic.offset[0] - elif self.axis == "y": - offset = graphic.offset[1] + # if self.axis == "x": + # offset = graphic.offset[0] + # elif self.axis == "y": + # offset = graphic.offset[1] + # + # # add the offset + # start += offset + # stop += offset - # add the offset - start += offset - stop += offset - - self._selection.set_value(self, (start, stop)) + self._selection.set_value(self, selection) @property def limits(self) -> Tuple[float, float]: @@ -104,13 +104,9 @@ def __init__( Create a LinearRegionSelector graphic which can be moved only along either the x-axis or y-axis. Allows sub-selecting data from a ``Graphic`` or from multiple Graphics. - bounds[0], limits[0], and position[0] must be identical. - - Holding the right mouse button while dragging an edge will force the entire region selector to move. This is - a when using transparent fill areas due to ``pygfx`` picking limitations. - - **Note:** Events get very weird if the values of bounds, limits and origin are close to zero. If you need - a linear selector with small data, we recommend scaling the data and then using the selector. + Assumes that the data under the selector is a function of the axis on which the selector moves + along. Example: if the selector is along the x-axis, then there must be only one y-value for each + x-value, otherwise functions such as ``get_selected_indices()`` do not make sense. Parameters ---------- @@ -208,11 +204,6 @@ def __init__( ] ).astype(np.float32) - if parent is not None: - parent_offset = parent.offset[0] - else: - parent_offset = 0 - elif axis == "y": # just some line data to initialize y axis edge lines init_line_data = np.array( @@ -222,11 +213,6 @@ def __init__( ] ).astype(np.float32) - if parent is not None: - parent_offset = parent.offset[1] - else: - parent_offset = 0 - else: raise ValueError("axis argument must be one of 'x' or 'y'") @@ -250,17 +236,20 @@ def __init__( edge.world.z = -0.5 group.add(edge) + if axis == "x": + offset = (parent.offset[0], center, 0) + elif axis == "y": + offset = (center, parent.offset[1], 0) + # set the initial bounds of the selector # compensate for any offset from the parent graphic # selection feature only works in world space, not data space self._selection = LinearRegionSelectionFeature( - selection + parent_offset, + selection, axis=axis, - limits=self._limits + parent_offset + limits=self._limits ) - print(f"sel value after construct: {selection}") - self._handled_widgets = list() self._block_ipywidget_call = False self._pygfx_event = None @@ -272,23 +261,15 @@ def __init__( hover_responsive=self.edges, arrow_keys_modifier=arrow_keys_modifier, axis=axis, + parent=parent, name=name, - parent=parent + offset=offset, ) self._set_world_object(group) self.selection = selection - print(f"sel value after set: {selection}") - - if self.axis == "x": - offset = (0, center, 0) - elif self.axis == "y": - offset = (center, 0, 0) - - self.offset = self.offset + offset - def get_selected_data( self, graphic: Graphic = None ) -> Union[np.ndarray, List[np.ndarray]]: @@ -544,45 +525,37 @@ def _set_slider_layout(self, *args): widget.layout = ipywidgets.Layout(width=f"{w}px") def _move_graphic(self, delta: np.ndarray): - # add delta to current bounds to get new positions - # print(delta) + # add delta to current min, max to get new positions if self.axis == "x": - # min and max of current bounds, i.e. the edges - xmin, xmax = self._selection.value - - # new left bound position - bound0_new = xmin + delta[0] + # add x value + new_min, new_max = self.selection + delta[0] - # new right bound position - bound1_new = xmax + delta[0] elif self.axis == "y": - # min and max of current bounds, i.e. the edges - ymin, ymax = self._selection.value - - # new bottom bound position - bound0_new = ymin + delta[1] + # add y value + new_min, new_max = self.selection + delta[1] - # new top bound position - bound1_new = ymax + delta[1] - - # move entire selector if source was fill + # move entire selector if event source was fill if self._move_info.source == self.fill: - # set the new bounds, in WORLD space - # don't set property because that is in data space! - self._selection.set_value(self, (bound0_new, bound1_new)) + # prevent weird shrinkage of selector if one edge is already at the limit + if self.selection[0] == self.limits[0] and new_min < self.limits[0]: + return + if self.selection[1] == self.limits[1] and new_max > self.limits[1]: + return + + # move entire selector + self._selection.set_value(self, (new_min, new_max)) return - # if selector is not resizable do nothing + # if selector is not resizable return if not self._resizable: return - # if resizable, move edges + # 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]: # change only left or bottom bound - self._selection.set_value(self, (bound0_new, self._selection.value[1])) + self._selection.set_value(self, (new_min, self._selection.value[1])) elif self._move_info.source == self.edges[1]: # change only right or top bound - self._selection.set_value(self, (self._selection.value[0], bound1_new)) - else: - return + self._selection.set_value(self, (self.selection[0], new_max)) From 49c2108c66c2b0d557bc7f6ccd94670c2af77f16 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 25 May 2024 22:08:49 -0400 Subject: [PATCH 060/196] linear region selector works well on x axis with events and data selection --- .../graphics/_features/_selection_features.py | 8 +- fastplotlib/graphics/line.py | 13 +- .../graphics/selectors/_base_selector.py | 6 +- .../graphics/selectors/_linear_region.py | 157 +++++++----------- 4 files changed, 71 insertions(+), 113 deletions(-) diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index 35538c28c..00f4e5aa7 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -147,11 +147,13 @@ def set_value(self, selector, value: Sequence[float]): selector.edges[1].geometry.positions.update_range() # send event - if len(self._event_handlers) > 0: + if len(self._event_handlers) < 1: return - # event = FeatureEvent("selection", {"indices": selector.get_selected_indices()}) - # self._call_event_handlers(event) + event = FeatureEvent("selection", {"value": self.value}) + event.get_selected_indices = selector.get_selected_indices + event.get_selected_data = selector.get_selected_data + self._call_event_handlers(event) # TODO: user's selector event handlers can call event.graphic.get_selected_indices() to get the data index, # and event.graphic.get_selected_data() to get the data under the selection # this is probably a good idea so that the data isn't sliced until it's actually necessary diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 3cd1be729..74ced44ef 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -182,7 +182,7 @@ def add_linear_selector( return weakref.proxy(selector) def add_linear_region_selector( - self, padding: float = 100.0, axis="x", **kwargs + self, padding: float = 0.0, axis="x", **kwargs ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, @@ -203,15 +203,6 @@ def add_linear_region_selector( """ - # ( - # bounds_init, - # limits, - # size, - # origin, - # axis, - # end_points, - # ) = self._get_linear_selector_init_args(padding, **kwargs) - n_datapoints = self.data.value.shape[0] value_25p = int(n_datapoints / 4) @@ -232,7 +223,7 @@ def add_linear_region_selector( limits = axis_vals[0], axis_vals[-1] # width or height of selector - size = int(np.ptp(magn_vals) + padding) + size = int(np.ptp(magn_vals) * 1.5 + padding) # center of selector along the other axis center = np.nanmean(magn_vals) diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 8c0556ce5..408acf465 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -37,6 +37,10 @@ class MoveInfo: class BaseSelector(Graphic): features = {"selection"} + @property + def axis(self) -> str: + return self._axis + def __init__( self, edges: Tuple[Line, ...] = None, @@ -72,7 +76,7 @@ def __init__( for wo in self._hover_responsive: self._original_colors[wo] = wo.material.color - self.axis = axis + self._axis = axis # current delta in world coordinates self.delta: np.ndarray = None diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index ee538ab5f..e09023076 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -16,35 +16,21 @@ class LinearRegionSelector(BaseSelector): + @property + def parent(self) -> Graphic | None: + """graphic that the selector is associated with""" + return self._parent + @property def selection(self) -> Sequence[float] | List[Sequence[float]]: """ - (min, max) of data value along selector's axis, in data space + (min, max) of data value along selector's axis """ # TODO: This probably does not account for rotation since world.position # does not account for rotation, we can do this later - # if self._parent is not None: - # # just subtract parent offset to map from world to data space - # if self.axis == "x": - # offset = self._parent.offset[0] - # elif self.axis == "y": - # offset = self._parent.offset[1] return self._selection.value.copy() - # - # indices = self.get_selected_indices() - # if isinstance(indices, np.ndarray): - # # this can be used directly to create a range object - # return indices[0], indices[-1] + 1 - # # if a collection is under the selector - # elif isinstance(indices, list): - # ranges = list() - # for ixs in indices: - # ranges.append((ixs[0], ixs[-1] + 1)) - # - # return ranges - # TODO: if no parent graphic is set, this just returns world positions # but should we change it? # return self._selection.value @@ -54,20 +40,9 @@ def selection(self, selection: Sequence[float]): # set (xmin, xmax), or (ymin, ymax) of the selector in data space graphic = self._parent - # start, stop = selection - if isinstance(graphic, GraphicCollection): pass - # if self.axis == "x": - # offset = graphic.offset[0] - # elif self.axis == "y": - # offset = graphic.offset[1] - # - # # add the offset - # start += offset - # stop += offset - self._selection.set_value(self, selection) @property @@ -95,18 +70,18 @@ def __init__( parent: Graphic = None, resizable: bool = True, fill_color=(0, 0, 0.35), - edge_color=(0.8, 0.8, 0), - edge_thickness: int = 3, + edge_color=(0.8, 0.6, 0), + edge_thickness: float = 8, arrow_keys_modifier: str = "Shift", name: str = None, ): """ Create a LinearRegionSelector graphic which can be moved only along either the x-axis or y-axis. - Allows sub-selecting data from a ``Graphic`` or from multiple Graphics. + Allows sub-selecting data from a parent ``Graphic`` or from multiple Graphics. Assumes that the data under the selector is a function of the axis on which the selector moves along. Example: if the selector is along the x-axis, then there must be only one y-value for each - x-value, otherwise functions such as ``get_selected_indices()`` do not make sense. + x-value, otherwise functions such as ``get_selected_data()`` do not make sense. Parameters ---------- @@ -114,16 +89,19 @@ def __init__( (min, max) values of the "axis" under the selector limits: (int, int) - (min limit, max limit) of values on the axis + (min limit, max limit) within which the selector can move size: int height or width of the selector + center: float + center offset of the selector, by default the data mean + axis: str, default "x" - "x" | "y", axis for the selector + "x" | "y", axis the selected can move on parent: Graphic, default ``None`` - associate this selector with a parent Graphic + associate this selector with a parent Graphic from which to fetch data or indices resizable: bool if ``True``, the edges can be dragged to resize the width of the linear selection @@ -134,6 +112,9 @@ def __init__( 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`` @@ -141,17 +122,6 @@ def __init__( name: str name for this selector graphic - Features - -------- - - selection: :class:`.LinearRegionSelectionFeature` - ``selection()`` returns the current selector bounds in world coordinates. - Use ``get_selected_indices()`` to return the selected indices in data - space, and ``get_selected_data()`` to return the selected data. - Use ``selection.add_event_handler()`` to add callback functions that are - called when the LinearSelector selection changes. See feature class for - event pick_info table. - """ # lots of very close to zero values etc. so round them, otherwise things get weird @@ -236,6 +206,7 @@ def __init__( edge.world.z = -0.5 group.add(edge) + # TODO: if parent offset changes, we should set the selector offset too if axis == "x": offset = (parent.offset[0], center, 0) elif axis == "y": @@ -275,7 +246,8 @@ def get_selected_data( ) -> Union[np.ndarray, List[np.ndarray]]: """ Get the ``Graphic`` data bounded by the current selection. - Returns a view of the full data array. + 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. @@ -286,15 +258,16 @@ def get_selected_data( Parameters ---------- - graphic: Graphic, optional + graphic: Graphic, optional, default ``None`` if provided, returns the data selection from this graphic instead of the graphic set as ``parent`` Returns ------- - np.ndarray, List[np.ndarray], or None + np.ndarray or List[np.ndarray] view or list of views of the full array, returns ``None`` if selection is empty """ + source = self._get_source(graphic) ixs = self.get_selected_indices(source) @@ -306,42 +279,42 @@ def get_selected_data( data_selections: List[np.ndarray] = list() for i, g in enumerate(source.graphics): - # if ixs[i].size == 0: - # data_selections.append(np.array([], dtype=np.float32)) - # else: - s = slice(ixs[i][0], ixs[i][-1]) - # slices n_datapoints dim - data_selections.append(g.data.buffer.data[s]) - - return source[:].data[s] + if ixs[i].size == 0: + data_selections.append(np.array([], dtype=np.float32).reshape(0, 3)) + else: + s = slice(ixs[i][0], ixs[i][-1] + 1) # add 1 because these are direct indices + # slices n_datapoints dim + data_selections.append(g.data[s]) + + # return source[:].data[s] else: - # just for one graphic - # if ixs.size == 0: - # return np.array([], dtype=np.float32) + if ixs.size == 0: + # empty selection + return np.array([], dtype=np.float32).reshape(0, 3) - s = slice(ixs[0], ixs[-1]) + s = slice(ixs[0], ixs[-1] + 1) # add 1 to end because these are direct indices # slices n_datapoints dim - return source.data.buffer.data[s] + # slice with min, max is faster than using all the indices + return source.data[s] if "Image" in source.__class__.__name__: - s = slice(ixs[0], ixs[-1]) + s = slice(ixs[0], ixs[-1] + 1) if self.axis == "x": - return source.data.value[:, s] + # slice columns + return source.data[:, s] elif self.axis == "y": - return source.data.value[s] + # slice rows + return source.data[s] def get_selected_indices( self, graphic: Graphic = None ) -> Union[np.ndarray, List[np.ndarray]]: """ Returns the indices of the ``Graphic`` data bounded by the current selection. - This is useful because the ``bounds`` min and max are not necessarily the same - as the Line Geometry positions x-vals or y-vals. For example, if if you used a - np.linspace(0, 100, 1000) for xvals in your line, then you will have 1,000 - x-positions. If the selection ``bounds`` are set to ``(0, 10)``, the returned - indices would be ``(0, 100)``. + + These are the data indices along the selector's "axis" which correspond to the data under the selector. Parameters ---------- @@ -351,7 +324,7 @@ def get_selected_indices( Returns ------- Union[np.ndarray, List[np.ndarray]] - data indices of the selection, list of np.ndarray if graphic is LineCollection + data indices of the selection, list of np.ndarray if graphic is a collection """ # we get the indices from the source graphic @@ -359,48 +332,34 @@ def get_selected_indices( # get the offset of the source graphic if self.axis == "x": - source_offset = source.offset[0] dim = 0 elif self.axis == "y": - source_offset = source.offset[1] dim = 1 - # selector (min, max) in world space - bounds = self._selection.value - # subtract offset to get the (min, max) bounded region - # of the source graphic in world space - bounds = tuple(v - source_offset for v in bounds) - - # # need them to be int to use as indices - # offset_bounds = tuple(map(int, offset_bounds)) + # selector (min, max) data values along axis + bounds = self.selection - if "Line" in source.__class__.__name__: - # now we need to map from world space to data space + if "Line" in source.__class__.__name__ or "Scatter" in source.__class__.__name__: # gets indices corresponding to n_datapoints dim - # data space is [n_datapoints, xyz], so we return + # data is [n_datapoints, xyz], so we return # indices that can be used to slice `n_datapoints` if isinstance(source, GraphicCollection): ixs = list() for g in source.graphics: - # map for each graphic in the collection - g_ixs = np.where( - (g.data.value[:, dim] >= bounds[0]) - & (g.data.value[:, dim] <= bounds[1]) - )[0] + # indices for each graphic in the collection + data = g.data.value[:, dim] + g_ixs = np.where((data >= bounds[0]) & (data <= bounds[1]))[0] ixs.append(g_ixs) else: # map this only this graphic - ixs = np.where( - (source.data.value[:, dim] >= bounds[0]) - & (source.data.value[:, dim] <= bounds[1]) - )[0] + data = source.data.value[:, dim] + ixs = np.where((data >= bounds[0]) & (data <= bounds[1]))[0] return ixs if "Image" in source.__class__.__name__: # indices map directly to grid geometry for image data buffer - ixs = np.arange(*bounds, dtype=int) - return ixs + return np.arange(*bounds, dtype=int) def make_ipywidget_slider(self, kind: str = "IntRangeSlider", **kwargs): """ @@ -538,8 +497,10 @@ def _move_graphic(self, delta: np.ndarray): if self._move_info.source == self.fill: # prevent weird shrinkage of selector if one edge is already at the limit if self.selection[0] == self.limits[0] and new_min < self.limits[0]: + # self._move_end(None) # TODO: cancel further movement to prevent weird asynchronization with pointer return if self.selection[1] == self.limits[1] and new_max > self.limits[1]: + # self._move_end(None) return # move entire selector From 1ec0f4002338ee78ac02026eae24094efbb398c3 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 May 2024 00:15:56 -0400 Subject: [PATCH 061/196] vertex cmap fix, delete synchronizer --- fastplotlib/graphics/_features/_base.py | 4 +- .../graphics/_features/_positions_graphics.py | 2 +- fastplotlib/graphics/selectors/_sync.py | 90 ------------------- 3 files changed, 3 insertions(+), 93 deletions(-) delete mode 100644 fastplotlib/graphics/selectors/_sync.py diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 2761bf994..45254ad91 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -157,7 +157,7 @@ def __init__( **kwargs ): super().__init__() - if isolated_buffer: + if isolated_buffer and not isinstance(data, pygfx.Resource): # useful if data is read-only, example: memmaps bdata = np.zeros(data.shape, dtype=data.dtype) bdata[:] = data[:] @@ -165,7 +165,7 @@ def __init__( # user's input array is used as the buffer bdata = data - if isinstance(data, pygfx.Buffer): + if isinstance(data, pygfx.Resource): # already a buffer, probably used for # managing another BufferManager, example: VertexCmap manages VertexColors self._buffer = data diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index a90e36806..c83fdd7e8 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -296,7 +296,7 @@ class VertexCmap(BufferManager): """ def __init__(self, vertex_colors: VertexColors, cmap_name: str | None, cmap_values: np.ndarray | None): - super().__init__(data=vertex_colors) + super().__init__(data=vertex_colors.buffer) self._vertex_colors = vertex_colors self._cmap_name = cmap_name diff --git a/fastplotlib/graphics/selectors/_sync.py b/fastplotlib/graphics/selectors/_sync.py deleted file mode 100644 index ce903aab8..000000000 --- a/fastplotlib/graphics/selectors/_sync.py +++ /dev/null @@ -1,90 +0,0 @@ -from . import LinearSelector -from typing import * - - -class Synchronizer: - def __init__( - self, *selectors: LinearSelector, key_bind: Union[str, None] = "Shift" - ): - """ - Synchronize the movement of `Selectors`. Selectors will move in sync only when the selected `"key_bind"` is - used during the mouse movement event. Valid key binds are: ``"Control"``, ``"Shift"`` and ``"Alt"``. - If ``key_bind`` is ``None`` then the selectors will always be synchronized. - - Parameters - ---------- - selectors - selectors to synchronize - - key_bind: str, default ``"Shift"`` - one of ``"Control"``, ``"Shift"`` and ``"Alt"`` or ``None`` - """ - self._selectors = list() - self.key_bind = key_bind - - for s in selectors: - self.add(s) - - self.block_event = False - - self.enabled: bool = True - - @property - def selectors(self): - """Selectors managed by the Synchronizer""" - return self._selectors - - def add(self, selector): - """add a selector""" - selector.selection.add_event_handler(self._handle_event) - self._selectors.append(selector) - - def remove(self, selector): - """remove a selector""" - selector.selection.remove_event_handler(self._handle_event) - self._selectors.remove(selector) - - def clear(self): - for i in range(len(self.selectors)): - self.remove(self.selectors[0]) - - def _handle_event(self, ev): - if self.block_event: - # because infinite recursion - return - - if not self.enabled: - return - - self.block_event = True - - source = ev.pick_info["graphic"] - delta = ev.pick_info["delta"] - pygfx_ev = ev.pick_info["pygfx_event"] - - # only moves when modifier is used - if pygfx_ev is None: - self.block_event = False - return - - if self.key_bind is not None: - if self.key_bind not in pygfx_ev.modifiers: - self.block_event = False - return - - if delta is not None: - self._move_selectors(source, delta) - - self.block_event = False - - def _move_selectors(self, source, delta): - for s in self.selectors: - # must use == and not is to compare Graphics because they are weakref proxies! - if s == source: - # if it's the source, since it has already moved - continue - - s._move_graphic(delta) - - def __del__(self): - self.clear() From 5022753361c0ce07f84861baa99abc61799f9726 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 May 2024 00:17:01 -0400 Subject: [PATCH 062/196] linear selector works --- .../graphics/_features/_selection_features.py | 65 +++++++++--- fastplotlib/graphics/line.py | 42 +++++--- .../graphics/selectors/_base_selector.py | 2 - fastplotlib/graphics/selectors/_linear.py | 98 ++++++++----------- .../graphics/selectors/_linear_region.py | 16 +-- 5 files changed, 128 insertions(+), 95 deletions(-) diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index 00f4e5aa7..0bf0d1d55 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -7,9 +7,23 @@ class LinearSelectionFeature(GraphicFeature): - # A bit much to have a class for this but this allows it to integrate with the fastplotlib callback system """ - Manages the linear selection and callbacks + **additional event attributes:** + + +--------------------+----------+------------------------------------+ + | attribute | type | description | + +====================+==========+====================================+ + | get_selected_index | callable | returns indices under the selector | + +--------------------+----------+------------------------------------+ + + **info dict:** + + +----------+------------+-------------------------------+ + | dict key | value type | value description | + +==========+============+===============================+ + | value | np.ndarray | new x or y value of selection | + +----------+------------+-------------------------------+ + """ def __init__(self, axis: str, value: float, limits: tuple[float, float]): @@ -36,33 +50,52 @@ def __init__(self, axis: str, value: float, limits: tuple[float, float]): @property def value(self) -> float: """ - selection in world space, NOT data space + selection, data x or y value """ - # TODO: Not sure if we should make this public since it's in world space, not data space - # need to decide if we give a value based on the selector's parent graphic, if there is one return self._value def set_value(self, selector, value: float): - if not (self._limits[0] <= value <= self._limits[1]): - return - - offset = list(selector.offset) + # clip value between limits + value = np.clip(value, self._limits[0], self._limits[1]) + # set position if self._axis == "x": - offset[0] = value - else: - offset[1] = value + dim = 0 + elif self._axis == "y": + dim = 1 - selector.offset = offset + for edge in selector._edges: + edge.geometry.positions.data[:, dim] = value + edge.geometry.positions.update_range() self._value = value - event = FeatureEvent("selection", {"index": selector.get_selected_index()}) + + event = FeatureEvent("selection", {"value": value}) + event.get_selected_index = selector.get_selected_index + self._call_event_handlers(event) class LinearRegionSelectionFeature(GraphicFeature): """ - Feature for a linearly bounding region + **additional event attributes:** + + +----------------------+----------+------------------------------------+ + | attribute | type | description | + +======================+==========+====================================+ + | get_selected_indices | callable | returns indices under the selector | + +----------------------+----------+------------------------------------+ + | get_selected_data | callable | returns data under the selector | + +----------------------+----------+------------------------------------+ + + **info dict:** + + +----------+------------+-----------------------------+ + | dict key | value type | value description | + +==========+============+=============================+ + | value | np.ndarray | new [min, max] of selection | + +----------+------------+-----------------------------+ + """ def __init__( @@ -151,8 +184,10 @@ def set_value(self, selector, value: Sequence[float]): return event = FeatureEvent("selection", {"value": self.value}) + event.get_selected_indices = selector.get_selected_indices event.get_selected_data = selector.get_selected_data + self._call_event_handlers(event) # TODO: user's selector event handlers can call event.graphic.get_selected_indices() to get the data index, # and event.graphic.get_selected_data() to get the data under the selection diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 74ced44ef..629b2cad6 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -129,14 +129,14 @@ def __init__( self.position_z = z_position def add_linear_selector( - self, selection: int = None, padding: float = 50, **kwargs + self, selection: float = None, padding: float = 0., axis: str = "x",**kwargs ) -> LinearSelector: """ Adds a linear selector. Parameters ---------- - selection: int + selection: float initial position of the selector padding: float @@ -151,38 +151,52 @@ def add_linear_selector( """ - ( - bounds_init, - limits, - size, - origin, - axis, - end_points, - ) = self._get_linear_selector_init_args(padding, **kwargs) + 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] if selection is None: - selection = limits[0] + selection = axis_vals[0] + limits = axis_vals[0], axis_vals[-1] - if selection < limits[0] or selection > limits[1]: + if not limits[0] <= selection <= limits[1]: raise ValueError( f"the passed selection: {selection} is beyond the limits: {limits}" ) + # 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) + selector = LinearSelector( selection=selection, limits=limits, - end_points=end_points, + 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., self.offset[-1] + 1) return weakref.proxy(selector) def add_linear_region_selector( - self, padding: float = 0.0, axis="x", **kwargs + self, padding: float = 0., axis: str = "x", **kwargs ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 408acf465..672b54cd1 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -355,8 +355,6 @@ def _key_down(self, ev): if ev.key not in key_bind_direction.keys(): return - # print(ev.key) - self._key_move_value = ev.key def _key_up(self, ev): diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 9c7eb6f77..b1082f5aa 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -17,41 +17,25 @@ class LinearSelector(BaseSelector): + @property + def parent(self) -> Graphic: + return self._parent + @property def selection(self) -> float: """ - The selected data index. Index of the data under the selector - not the x or y value of the data but the index of the x or y value + x or y value of selector's current position """ - if self._parent is not None: - return self.get_selected_index() - # TODO: if no parent graphic is set, this just returns world position - # but should we change it? return self._selection.value @selection.setter - def selection(self, index: int): + def selection(self, value: int): graphic = self._parent - if "Line" in graphic.__class__.__name__ or "Scatter" in graphic.__class__.__name__: - if self.axis == "x": - geo_positions = graphic.data.value[:, 0] - offset = graphic.offset[0] - elif self.axis == "y": - geo_positions = graphic.data.value[:, 1] - offset = graphic.offset[1] - - # we want to find the geometry position at the desired index - position = geo_positions[index] - - elif "Image" in graphic.__class__.__name__: - # 1:1 mapping between geometry position and index - position = index + if isinstance(graphic, GraphicCollection): + pass - # new world position for the selector - # offset + new_index - world_pos = offset + position - self._selection.set_value(self, world_pos) + self._selection.set_value(self, value) @property def limits(self) -> Tuple[float, float]: @@ -71,14 +55,15 @@ def limits(self, values: Tuple[float, float]): # TODO: make `selection` arg in graphics data space not world space def __init__( self, - selection: int, - limits: Tuple[int, int], + selection: float, + limits: Sequence[float], + size: float, + center: float, axis: str = "x", parent: Graphic = None, - end_points: Tuple[int, int] = None, - arrow_keys_modifier: str = "Shift", + color: str | tuple = "w", thickness: float = 2.5, - color: Any = "w", + arrow_keys_modifier: str = "Shift", name: str = None, ): """ @@ -119,19 +104,19 @@ def __init__( if len(limits) != 2: raise ValueError("limits must be a tuple of 2 integers, i.e. (int, int)") - self._limits = tuple(map(round, limits)) + self._limits = np.asarray(limits) - selection = round(selection) + end_points = [-size / 2, size / 2] if axis == "x": - xs = np.zeros(2) + xs = np.array([selection, selection]) ys = np.array(end_points) zs = np.zeros(2) line_data = np.column_stack([xs, ys, zs]) elif axis == "y": xs = np.array(end_points) - ys = np.zeros(2) + ys = np.array([selection, selection]) zs = np.zeros(2) line_data = np.column_stack([xs, ys, zs]) @@ -173,6 +158,11 @@ def __init__( self._handled_widgets = list() + if axis == "x": + offset = (parent.offset[0], center, 0) + elif axis == "y": + offset = (center, parent.offset[1], 0) + # init base selector BaseSelector.__init__( self, @@ -180,8 +170,9 @@ def __init__( hover_responsive=(line_inner, self.line_outer), arrow_keys_modifier=arrow_keys_modifier, axis=axis, - name=name, parent=parent, + name=name, + offset=offset ) self._set_world_object(world_object) @@ -196,7 +187,7 @@ def __init__( self._selection.set_value(self, selection) # update any ipywidgets - self.add_event_handler("selection", self._update_ipywidgets) + self.add_event_handler(self._update_ipywidgets, "selection") def _setup_ipywidget_slider(self, widget): # setup an ipywidget slider with bidirectional callbacks to this LinearSelector @@ -216,7 +207,7 @@ def _update_ipywidgets(self, ev): # update the ipywidget sliders when LinearSelector value changes self._block_ipywidget_call = True # prevent infinite recursion - value = ev.info["index"] + value = ev.info["value"] # update all the handled slider widgets for widget in self._handled_widgets: if isinstance(widget, ipywidgets.IntSlider): @@ -354,34 +345,29 @@ def get_selected_index(self, graphic: Graphic = None) -> Union[int, List[int]]: def _get_selected_index(self, graphic): # the array to search for the closest value along that axis if self.axis == "x": - geo_positions = graphic.data()[:, 0] - offset = getattr(graphic, f"position_{self.axis}") - else: - geo_positions = graphic.data()[:, 1] - offset = getattr(graphic, f"position_{self.axis}") + data = graphic.data[:, 0] + elif self.axis == "y": + data = graphic.data[:, 1] - if "Line" in graphic.__class__.__name__: - # we want to find the index of the geometry position that is closest to the slider's geometry position - find_value = self._selection.value - offset + if "Line" in graphic.__class__.__name__ or "Scatter" in graphic.__class__.__name__: + # we want to find the index of the data closest to the slider position + find_value = self.selection # get closest data index to the world space position of the slider - idx = np.searchsorted(geo_positions, find_value, side="left") + idx = np.searchsorted(data, find_value, side="left") if idx > 0 and ( - idx == len(geo_positions) - or math.fabs(find_value - geo_positions[idx - 1]) - < math.fabs(find_value - geo_positions[idx]) + idx == len(data) + or math.fabs(find_value - data[idx - 1]) + < math.fabs(find_value - data[idx]) ): return round(idx - 1) else: return round(idx) - if ( - "Heatmap" in graphic.__class__.__name__ - or "Image" in graphic.__class__.__name__ - ): + if "Image" in graphic.__class__.__name__: # indices map directly to grid geometry for image data buffer - index = self._selection.value - offset + index = self.selection return round(index) def _move_graphic(self, delta: np.ndarray): @@ -396,9 +382,9 @@ def _move_graphic(self, delta: np.ndarray): """ if self.axis == "x": - self.selection = self._selection + delta[0] + self.selection = self.selection + delta[0] else: - self.selection = self._selection + delta[1] + self.selection = self.selection + delta[1] def _fpl_cleanup(self): for widget in self._handled_widgets: diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index e09023076..c6c40fa88 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -62,8 +62,8 @@ def limits(self, values: Tuple[float, float]): def __init__( self, - selection: Tuple[int, int], - limits: Tuple[int, int], + selection: Sequence[float], + limits: Sequence[float], size: int, center: float, axis: str = "x", @@ -85,10 +85,10 @@ def __init__( Parameters ---------- - selection: (int, int) - (min, max) values of the "axis" under the selector + selection: (float, float) + initial (min, max) x or y values - limits: (int, int) + limits: (float, float) (min limit, max limit) within which the selector can move size: int @@ -347,12 +347,12 @@ def get_selected_indices( ixs = list() for g in source.graphics: # indices for each graphic in the collection - data = g.data.value[:, dim] + data = g.data[:, dim] g_ixs = np.where((data >= bounds[0]) & (data <= bounds[1]))[0] ixs.append(g_ixs) else: # map this only this graphic - data = source.data.value[:, dim] + data = source.data[:, dim] ixs = np.where((data >= bounds[0]) & (data <= bounds[1]))[0] return ixs @@ -450,7 +450,7 @@ def _setup_ipywidget_slider(self, widget): widget.observe(self._ipywidget_callback, "value") # user changes linear selection -> widget changes - self.selection.add_event_handler(self._update_ipywidgets) + self.selection.add_event_handler(self._update_ipywidgets, "selection") self._plot_area.renderer.add_event_handler(self._set_slider_layout, "resize") From f295356676ad86a4120c9372dda0263baf19bbbe Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 May 2024 00:17:08 -0400 Subject: [PATCH 063/196] cleanup --- fastplotlib/graphics/selectors/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/fastplotlib/graphics/selectors/__init__.py b/fastplotlib/graphics/selectors/__init__.py index 1fb0c453e..113772173 100644 --- a/fastplotlib/graphics/selectors/__init__.py +++ b/fastplotlib/graphics/selectors/__init__.py @@ -2,11 +2,8 @@ from ._linear_region import LinearRegionSelector from ._polygon import PolygonSelector -from ._sync import Synchronizer - __all__ = [ "LinearSelector", "LinearRegionSelector", "PolygonSelector", - "Synchronizer", ] From 4d4d636885c22222100721ecee47f8dc37ea4528 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 May 2024 00:17:23 -0400 Subject: [PATCH 064/196] update graphic methods mixin --- fastplotlib/layouts/_graphic_methods_mixin.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index 9f82cfed5..6765c33be 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -178,7 +178,7 @@ def add_image( def add_line_collection( self, data: List[numpy.ndarray], - z_offset: Union[Iterable[float], float] = None, + z_offset: Union[Iterable[float | int], float, int] = None, thickness: Union[float, Iterable[float]] = 2.0, colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", alpha: float = 1.0, @@ -200,8 +200,8 @@ def add_line_collection( if elements are 2D, interpreted as [y_vals, n_lines] z_offset: Iterable of float or float, optional - | if ``float``, single offset will be used for all lines - | if ``list`` of ``float``, each value will apply to the individual lines + | if ``float`` | ``int``, single offset will be used for all lines + | if ``list`` of ``float`` | ``int``, each value will apply to the individual lines thickness: float or Iterable of float, default 2.0 | if ``float``, single thickness will be used for all lines @@ -268,11 +268,13 @@ def add_line( data: Any, thickness: float = 2.0, colors: Union[str, numpy.ndarray, Iterable] = "w", + uniform_colors: bool = False, alpha: float = 1.0, cmap: str = None, cmap_values: Union[numpy.ndarray, Iterable] = None, z_position: float = None, collection_index: int = None, + isolated_buffer: bool = True, *args, **kwargs ) -> LineGraphic: @@ -314,6 +316,7 @@ def add_line( Features -------- + **data**: :class:`.ImageDataFeature` Manages the line [x, y, z] positions data buffer, allows regular and fancy indexing. @@ -336,11 +339,13 @@ def add_line( data, thickness, colors, + uniform_colors, alpha, cmap, cmap_values, z_position, collection_index, + isolated_buffer, *args, **kwargs ) @@ -439,13 +444,15 @@ def add_line_stack( def add_scatter( self, - data: numpy.ndarray, - sizes: Union[float, numpy.ndarray, Iterable[float]] = 1, - colors: Union[str, numpy.ndarray, Iterable[str]] = "w", + data: Any, + colors: str | numpy.ndarray | tuple[float] | list[float] | list[str] = "w", + uniform_colors: bool = False, alpha: float = 1.0, cmap: str = None, - cmap_values: Union[numpy.ndarray, List] = None, - z_position: float = 0.0, + cmap_values: numpy.ndarray = None, + isolated_buffer: bool = True, + sizes: Union[float, numpy.ndarray, Iterable[float]] = 1, + uniform_sizes: bool = False, *args, **kwargs ) -> ScatterGraphic: @@ -504,12 +511,14 @@ def add_scatter( return self._create_graphic( ScatterGraphic, data, - sizes, colors, + uniform_colors, alpha, cmap, cmap_values, - z_position, + isolated_buffer, + sizes, + uniform_sizes, *args, **kwargs ) From 1d6adc6413e861b4a4ddc1f22fbe4c754b584a30 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 May 2024 03:35:21 -0400 Subject: [PATCH 065/196] update selector example nbs, still WIP --- .../notebooks/linear_region_selector.ipynb | 57 ++++++++++--------- examples/notebooks/linear_selector.ipynb | 28 +++++---- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/examples/notebooks/linear_region_selector.ipynb b/examples/notebooks/linear_region_selector.ipynb index 2ba40ed54..cbe845f71 100644 --- a/examples/notebooks/linear_region_selector.ipynb +++ b/examples/notebooks/linear_region_selector.ipynb @@ -17,7 +17,7 @@ "source": [ "import fastplotlib as fpl\n", "import numpy as np\n", - "from ipywidgets import IntRangeSlider, FloatRangeSlider, VBox\n", + "# from ipywidgets import IntRangeSlider, FloatRangeSlider, VBox\n", "\n", "fig = fpl.Figure((2, 2))\n", "\n", @@ -25,11 +25,12 @@ "zoomed_prealloc = 1_000\n", "\n", "# data to plot\n", - "xs = np.linspace(0, 100, 1_000)\n", - "sine = np.sin(xs) * 20\n", + "xs = np.linspace(0, 10* np.pi, 1_000)\n", + "sine = np.sin(xs)\n", + "sine += 100\n", "\n", "# make sine along x axis\n", - "sine_graphic_x = fig[0, 0].add_line(sine)\n", + "sine_graphic_x = fig[0, 0].add_line(np.column_stack([xs, sine]), offset=(10, 0, 0))\n", "\n", "# just something that looks different for line along y-axis\n", "sine_y = sine\n", @@ -47,7 +48,7 @@ "ls_y = sine_graphic_y.add_linear_region_selector(axis=\"y\")\n", "\n", "# preallocate array for storing zoomed in data\n", - "zoomed_init = np.column_stack([np.arange(zoomed_prealloc), np.random.rand(zoomed_prealloc)])\n", + "zoomed_init = np.column_stack([np.arange(zoomed_prealloc), np.zeros(zoomed_prealloc)])\n", "\n", "# make line graphics for displaying zoomed data\n", "zoomed_x = fig[1, 0].add_line(zoomed_init)\n", @@ -62,54 +63,54 @@ " # interpolate to preallocated size\n", " return np.interp(x, xp, fp=subdata[:, axis]) # use the y-values\n", "\n", - "\n", + "@ls_x.add_event_handler(\"selection\")\n", "def set_zoom_x(ev):\n", " \"\"\"sets zoomed x selector data\"\"\"\n", - " selected_data = ev.pick_info[\"selected_data\"]\n", - " zoomed_x.data = interpolate(selected_data, axis=1) # use the y-values\n", + " # get the selected data\n", + " selected_data = ev.get_selected_data()\n", + " if selected_data.size == 0:\n", + " # no data selected\n", + " zoomed_x.data[:, 1] = 0\n", + "\n", + " # set the y-values\n", + " zoomed_x.data[:, 1] = interpolate(selected_data, axis=1)\n", " fig[1, 0].auto_scale()\n", "\n", "\n", "def set_zoom_y(ev):\n", - " \"\"\"sets zoomed y selector data\"\"\"\n", - " selected_data = ev.pick_info[\"selected_data\"]\n", - " zoomed_y.data = -interpolate(selected_data, axis=0) # use the x-values\n", + " \"\"\"sets zoomed x selector data\"\"\"\n", + " # get the selected data\n", + " selected_data = ev.get_selected_data()\n", + " if selected_data.size == 0:\n", + " # no data selected\n", + " zoomed_y.data[:, 0] = 0\n", + "\n", + " # set the x-values\n", + " zoomed_y.data[:, 0] = -interpolate(selected_data, axis=1)\n", " fig[1, 1].auto_scale()\n", "\n", "\n", - "# update zoomed plots when bounds change\n", - "ls_x.selection.add_event_handler(set_zoom_x)\n", - "ls_y.selection.add_event_handler(set_zoom_y)\n", - "\n", - "fig.show()" - ] - }, - { - "cell_type": "markdown", - "id": "0bad4a35-f860-4f85-9061-920154ab682b", - "metadata": {}, - "source": [ - "### On the x-axis we have a 1-1 mapping from the data that we have passed and the line geometry positions. So the `bounds` min max corresponds directly to the data indices." + "fig.show(maintain_aspect=False)" ] }, { "cell_type": "code", "execution_count": null, - "id": "2c96a3ff-c2e7-4683-8097-8491e97dd6d3", + "id": "2f29e913-c4f8-44a6-8692-eb14436849a5", "metadata": {}, "outputs": [], "source": [ - "ls_x.selection()" + "sine_graphic_x.data[:, 1].ptp()" ] }, { "cell_type": "code", "execution_count": null, - "id": "3ec71e3f-291c-43c6-a954-0a082ba5981c", + "id": "1947a477-5dd2-4df9-aecd-6967c6ab45fe", "metadata": {}, "outputs": [], "source": [ - "ls_x.get_selected_indices()" + "np.clip(-0.1, 0, 10)" ] }, { diff --git a/examples/notebooks/linear_selector.ipynb b/examples/notebooks/linear_selector.ipynb index e9c8e664a..ee590f20b 100644 --- a/examples/notebooks/linear_selector.ipynb +++ b/examples/notebooks/linear_selector.ipynb @@ -5,7 +5,7 @@ "id": "a06e1fd9-47df-42a3-a76c-19e23d7b89fd", "metadata": {}, "source": [ - "## `LinearSelector`, draggable selector that can optionally associated with an ipywidget." + "## `LinearSelector`, draggable selector that can also be linked to an ipywidget slider" ] }, { @@ -16,7 +16,6 @@ "outputs": [], "source": [ "import fastplotlib as fpl\n", - "from fastplotlib.graphics.selectors import Synchronizer\n", "\n", "import numpy as np\n", "from ipywidgets import VBox, IntSlider, FloatSlider\n", @@ -35,16 +34,14 @@ "selector2 = sine_graphic.add_linear_selector(20)\n", "selector3 = sine_graphic.add_linear_selector(40)\n", "\n", - "ss = Synchronizer(selector, selector2, selector3)\n", - "\n", + "# one of the selectors will change the line colors when it moves\n", + "@selector.add_event_handler(\"selection\")\n", "def set_color_at_index(ev):\n", " # changes the color at the index where the slider is\n", - " ix = ev.pick_info[\"selected_index\"]\n", - " g = ev.pick_info[\"graphic\"].parent\n", + " ix = ev.get_selected_index()\n", + " g = ev.graphic.parent\n", " g.colors[ix] = \"green\"\n", "\n", - "selector.selection.add_event_handler(set_color_at_index)\n", - "\n", "# fastplotlib LineSelector can make an ipywidget slider and return it :D \n", "ipywidget_slider = selector.make_ipywidget_slider()\n", "ipywidget_slider.description = \"slider1\"\n", @@ -57,7 +54,15 @@ "selector3.add_ipywidget_handler(ipywidget_slider3, step=0.1)\n", "\n", "fig[0, 0].auto_scale()\n", - "fig.show(add_widgets=[ipywidget_slider])" + "VBox([fig.show(), ipywidget_slider, ipywidget_slider2, ipywidget_slider3])" + ] + }, + { + "cell_type": "markdown", + "id": "d83caca6-e9b6-45df-b93c-0dfe0498d20e", + "metadata": {}, + "source": [ + "Double click the first selctor, and then use `Shift` + Right/Left Arrow Key to move it!" ] }, { @@ -67,13 +72,16 @@ "metadata": {}, "outputs": [], "source": [ + "# this controls the step-size of arrow key movements\n", "selector.step = 0.1" ] }, { "cell_type": "markdown", "id": "3b0f448f-bbe4-4b87-98e3-093f561c216c", - "metadata": {}, + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, "source": [ "### Drag linear selectors with the mouse, hold \"Shift\" to synchronize movement of all the selectors" ] From 284a1dbf2068eda49bf96c37b12340703f33c7a8 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 28 May 2024 23:53:02 -0400 Subject: [PATCH 066/196] type annotation in setter --- fastplotlib/graphics/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index bbb2051f5..3ed02c4af 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -384,7 +384,7 @@ def colors(self) -> VertexColors | pygfx.Color: return self._colors.value @colors.setter - def colors(self, value): + def colors(self, value: str | np.ndarray | tuple[float] | list[float] | list[str]): if isinstance(self._colors, VertexColors): self._colors[:] = value From b4fe9577785568e7637bd974d74d54b11fffb4bc Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 31 May 2024 16:00:43 -0400 Subject: [PATCH 067/196] add notes to tests comments --- tests/utils.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index df991095a..8aa474b1f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -14,62 +14,62 @@ def generate_slice_indices(kind: int): indices = [2] case 1: - # everything + # everything, [:] s = slice(None, None, None) indices = list(range(10)) case 2: - # positive continuous range + # positive continuous range, [1:5] s = slice(1, 5, None) indices = [1, 2, 3, 4] case 3: - # positive stepped range + # positive stepped range, [2:8:2] s = slice(2, 8, 2) indices = [2, 4, 6] case 4: - # negative continuous range + # negative continuous range, [-5:] s = slice(-5, None, None) indices = [5, 6, 7, 8, 9] case 5: - # negative backwards + # negative backwards, [-5::-1] s = slice(-5, None, -1) indices = [5, 4, 3, 2, 1, 0] case 5: - # negative backwards stepped + # negative backwards stepped, [-5::-2] s = slice(-5, None, -2) indices = [5, 3, 1] case 6: - # negative stepped forward + # negative stepped forward[-5::2] s = slice(-5, None, 2) indices = [5, 7, 9] case 7: - # both negative + # both negative, [-8:-2] s = slice(-8, -2, None) indices = [2, 3, 4, 5, 6, 7] case 8: - # both negative and stepped + # both negative and stepped, [-8:2:2] s = slice(-8, -2, 2) indices = [2, 4, 6] case 9: - # positive, negative, negative + # positive, negative, negative, [8:-9:-2] s = slice(8, -9, -2) indices = [8, 6, 4, 2] case 10: - # only stepped forward + # only stepped forward, [::2] s = slice(None, None, 2) indices = [0, 2, 4, 6, 8] case 11: - # only stepped backward + # only stepped backward, [::-3] s = slice(None, None, -3) indices = [9, 6, 3, 0] From e6b91336c77bcf76c79c4dee9089d6d39aa91818 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 3 Jun 2024 21:50:56 -0400 Subject: [PATCH 068/196] refactor image stuff --- fastplotlib/graphics/_features/__init__.py | 9 +- fastplotlib/graphics/_features/_base.py | 10 +- fastplotlib/graphics/_features/_data.py | 115 -------- fastplotlib/graphics/_features/_image.py | 151 ++++++++--- fastplotlib/graphics/image.py | 297 +++++++-------------- 5 files changed, 219 insertions(+), 363 deletions(-) delete mode 100644 fastplotlib/graphics/_features/_data.py diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index ace671805..b2b07fa04 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -1,5 +1,5 @@ from ._positions_graphics import VertexColors, UniformColor, UniformSizes, Thickness, VertexPositions, PointsSizesFeature, VertexCmap -from ._image import ImageData, ImageCmap, ImageVmin, ImageVmax +from ._image import TextureArray, ImageCmap, ImageVmin, ImageVmax, ImageInterpolation, ImageCmapInterpolation, WGPU_MAX_TEXTURE_SIZE from ._base import ( GraphicFeature, BufferManager, @@ -8,10 +8,3 @@ ) from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature from ._common import Name, Offset, Rotation, Visible, Deleted - - -class HeatmapDataFeature: - pass - -class HeatmapCmapFeature: - pass diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 45254ad91..ebf7dbf15 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -9,6 +9,9 @@ import pygfx +WGPU_MAX_TEXTURE_SIZE = 8192 + + supported_dtypes = [ np.uint8, np.uint16, @@ -141,9 +144,6 @@ def _call_event_handlers(self, event_data: FeatureEvent): with log_exception(f"Error during handling {self.__class__.__name__} event"): func(event_data) - def __repr__(self) -> str: - raise NotImplementedError - class BufferManager(GraphicFeature): """Smaller wrapper for pygfx.Buffer""" @@ -151,7 +151,7 @@ class BufferManager(GraphicFeature): def __init__( self, data: NDArray | pygfx.Buffer, - buffer_type: Literal["buffer", "texture"] = "buffer", + buffer_type: Literal["buffer", "texture", "texture-array"] = "buffer", isolated_buffer: bool = True, texture_dim: int = 2, **kwargs @@ -300,7 +300,7 @@ def _emit_event(self, type: str, key, value): } event = FeatureEvent(type, info=event_info) - super()._call_event_handlers(event) + self._call_event_handlers(event) def __repr__(self): return f"{self.__class__.__name__} buffer data:\n" \ diff --git a/fastplotlib/graphics/_features/_data.py b/fastplotlib/graphics/_features/_data.py deleted file mode 100644 index d0f4bd4a4..000000000 --- a/fastplotlib/graphics/_features/_data.py +++ /dev/null @@ -1,115 +0,0 @@ -# -# class ImageDataFeature(GraphicFeatureIndexable): -# """ -# Access to the Texture buffer shown in an ImageGraphic. -# """ -# -# def __init__(self, parent, data: Any): -# if data.ndim not in (2, 3): -# raise ValueError( -# "`data.ndim` must be 2 or 3, ImageGraphic data shape must be " -# "``[x_dim, y_dim]`` or ``[x_dim, y_dim, rgb]``" -# ) -# -# super().__init__(parent, data) -# -# @property -# def buffer(self) -> pygfx.Texture: -# """Texture buffer for the image data""" -# return self._parent.world_object.geometry.grid -# -# def update_gpu(self): -# """Update the GPU with the buffer""" -# self._update_range(None) -# -# def __call__(self, *args, **kwargs): -# return self.buffer.data -# -# def __getitem__(self, item): -# return self.buffer.data[item] -# -# def __setitem__(self, key, value): -# # make sure float32 -# value = to_gpu_supported_dtype(value) -# -# self.buffer.data[key] = value -# self._update_range(key) -# -# # avoid creating dicts constantly if there are no events to handle -# if len(self._event_handlers) > 0: -# self._feature_changed(key, value) -# -# def _update_range(self, key): -# self.buffer.update_range((0, 0, 0), size=self.buffer.size) -# -# def _feature_changed(self, key, new_data): -# if key is not None: -# key = cleanup_slice(key, self._upper_bound) -# if isinstance(key, int): -# indices = [key] -# elif isinstance(key, slice): -# indices = range(key.start, key.stop, key.step) -# elif key is None: -# indices = None -# -# pick_info = { -# "index": indices, -# "world_object": self._parent.world_object, -# "new_data": new_data, -# } -# -# event_data = FeatureEvent(type="data", pick_info=pick_info) -# -# self._call_event_handlers(event_data) -# -# def __repr__(self) -> str: -# s = f"ImageDataFeature for {self._parent}, call `.data()` to get values" -# return s -# -# -# class HeatmapDataFeature(ImageDataFeature): -# @property -# def buffer(self) -> List[pygfx.Texture]: -# """list of Texture buffer for the image data""" -# return [img.geometry.grid for img in self._parent.world_object.children] -# -# def __getitem__(self, item): -# return self._data[item] -# -# def __call__(self, *args, **kwargs): -# return self._data -# -# def __setitem__(self, key, value): -# # make sure supported type, not float64 etc. -# value = to_gpu_supported_dtype(value) -# -# self._data[key] = value -# self._update_range(key) -# -# # avoid creating dicts constantly if there are no events to handle -# if len(self._event_handlers) > 0: -# self._feature_changed(key, value) -# -# def _update_range(self, key): -# for buffer in self.buffer: -# buffer.update_range((0, 0, 0), size=buffer.size) -# -# def _feature_changed(self, key, new_data): -# if key is not None: -# key = cleanup_slice(key, self._upper_bound) -# if isinstance(key, int): -# indices = [key] -# elif isinstance(key, slice): -# indices = range(key.start, key.stop, key.step) -# elif key is None: -# indices = None -# -# pick_info = { -# "index": indices, -# "world_object": self._parent.world_object, -# "new_data": new_data, -# } -# -# event_data = FeatureEvent(type="data", pick_info=pick_info) -# -# self._call_event_handlers(event_data) diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py index c32f14bd1..55bb5a745 100644 --- a/fastplotlib/graphics/_features/_image.py +++ b/fastplotlib/graphics/_features/_image.py @@ -1,23 +1,80 @@ +from math import ceil + import numpy as np +from numpy.typing import NDArray import pygfx -from ._base import GraphicFeature, BufferManager, FeatureEvent +from ._base import GraphicFeature, FeatureEvent, WGPU_MAX_TEXTURE_SIZE from ...utils import ( make_colors, get_cmap_texture, ) +# manages an array of 8192x8192 Textures representing chunks of an image +class TextureArray(GraphicFeature): -class ImageData(BufferManager): def __init__(self, data, isolated_buffer: bool = True): + super().__init__() + data = self._fix_data(data) - super().__init__(data, buffer_type="texture", isolated_buffer=isolated_buffer) + + if isolated_buffer: + # useful if data is read-only, example: memmaps + self._value = np.zeros(data.shape, dtype=data.dtype) + self.value[:] = data[:] + else: + # user's input array is used as the buffer + self._value = data + + # indices for each Texture + self._row_indices = np.arange(0, ceil(self.value.shape[0] / WGPU_MAX_TEXTURE_SIZE) * WGPU_MAX_TEXTURE_SIZE, WGPU_MAX_TEXTURE_SIZE) + self._col_indices = np.arange(0, ceil(self.value.shape[1] / WGPU_MAX_TEXTURE_SIZE) * WGPU_MAX_TEXTURE_SIZE, WGPU_MAX_TEXTURE_SIZE) + + # buffer will be an array of textures + self._buffer: np.ndarray[pygfx.Texture] = np.empty(shape=(self.row_indices.size, self.col_indices.size), dtype=object) + + # max index + row_max = self.value.shape[0] - 1 + col_max = self.value.shape[1] - 1 + + for (buffer_row, row_ix), (buffer_col, col_ix) in zip(enumerate(self.row_indices), enumerate(self.col_indices)): + # stop index for this chunk + row_stop = min(row_max, row_ix + WGPU_MAX_TEXTURE_SIZE) + col_stop = min(col_max, col_ix + WGPU_MAX_TEXTURE_SIZE) + + # make texture from slice + texture = pygfx.Texture( + self.value[row_ix:row_stop, col_ix:col_stop], dim=2 + ) + + self.buffer[buffer_row, buffer_col] = texture + + self._shared: int = 0 @property - def buffer(self) -> pygfx.Texture: + def value(self) -> NDArray: + return self._data + + def set_value(self, graphic, value): + self[:] = value + + @property + def buffer(self) -> np.ndarray[pygfx.Texture]: return self._buffer + @property + def row_indices(self) -> np.ndarray: + return self._row_indices + + @property + def col_indices(self) -> np.ndarray: + return self._row_indices + + @property + def shared(self) -> int: + return self._shared + def _fix_data(self, data): if data.ndim not in (2, 3): raise ValueError( @@ -28,34 +85,17 @@ def _fix_data(self, data): # let's just cast to float32 always return data.astype(np.float32) - def __setitem__(self, key: int | slice | np.ndarray[int | bool] | tuple[slice | np.ndarray[int | bool]], value): - # offset and size should be (width, height, depth), i.e. (columns, rows, depth) - # offset and size for depth should always be 0, 1 for 2D images - if isinstance(key, tuple): - # multiple dims sliced - if any([k is Ellipsis for k in key]): - # let's worry about ellipsis later - raise TypeError("ellipses not supported for indexing buffers") - if len(key) in (2, 3): - dim_os = list() # hold offset and size for each dim - for dim, k in enumerate(key[:2]): # we only need width and height - dim_os.append(self._parse_offset_size(k, self.value.shape[dim])) - - # offset and size for each dim into individual offset and size tuple - # note that this is flipped since we need (width, height) from (rows, cols) - offset = (*tuple(os[1] for os in dim_os), 0) - size = (*tuple(os[1] for os in dim_os), 0) - else: - raise IndexError + def __getitem__(self, item): + return self.value[item] - else: - # only first dim (rows) indexed - row_offset, row_size = self._parse_offset_size(key, self.value.shape[0]) - offset = (0, row_offset, 0) - size = (self.value.shape[1], row_size, 1) + def __setitem__(self, key, value): + self.value[key] = value + + for texture in self.buffer.ravel(): + texture.update_range((0, 0, 0), texture.size) - self.buffer.update_range(offset, size) - self._emit_event("data", key, value) + event = FeatureEvent("data", info={"key": key, "value": value}) + self._call_event_handlers(event) class ImageVmin(GraphicFeature): @@ -115,3 +155,54 @@ def set_value(self, graphic, value: str): self._value = value event = FeatureEvent(type="cmap", info={"value": value}) self._call_event_handlers(event) + + +class ImageInterpolation(GraphicFeature): + """Image interpolation method""" + def __init__(self, value: str): + self._validate(value) + self._value = value + super().__init__() + + def _validate(self, value): + if value not in ["nearest", "linear"]: + raise ValueError("`interpolation` must be one of 'nearest' or 'linear'") + + @property + def value(self) -> str: + return self._value + + def set_value(self, graphic, value: str): + self._validate(value) + + graphic.world_object.material.interpolation = value + + self._value = value + event = FeatureEvent(type="interpolation", info={"value": value}) + self._call_event_handlers(event) + + +class ImageCmapInterpolation(GraphicFeature): + """Image cmap interpolation method""" + + def __init__(self, value: str): + self._validate(value) + self._value = value + super().__init__() + + def _validate(self, value): + if value not in ["nearest", "linear"]: + raise ValueError("`cmap_interpolation` must be one of 'nearest' or 'linear'") + + @property + def value(self) -> str: + return self._value + + def set_value(self, graphic, value: str): + self._validate(value) + + graphic.world_object.material.map_interpolation = value + + self._value = value + event = FeatureEvent(type="interpolation", info={"value": value}) + self._call_event_handlers(event) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index ef05631fc..19b1677c8 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -1,9 +1,8 @@ from typing import * -from math import ceil -from itertools import product import weakref import numpy as np +from numpy.typing import NDArray import pygfx @@ -11,13 +10,13 @@ from ._base import Graphic, Interaction from .selectors import LinearSelector, LinearRegionSelector from ._features import ( - ImageData, + TextureArray, ImageCmap, ImageVmin, ImageVmax, - HeatmapDataFeature, - HeatmapCmapFeature, - to_gpu_supported_dtype, + ImageInterpolation, + ImageCmapInterpolation, + WGPU_MAX_TEXTURE_SIZE ) @@ -113,7 +112,7 @@ def add_linear_region_selector( # create selector selector = LinearRegionSelector( - bounds=bounds_init, + selection=bounds_init, limits=limits, size=size, origin=origin, @@ -197,11 +196,55 @@ def _add_plot_area_hook(self, plot_area): self._plot_area = plot_area +class _ImageTile(pygfx.Image): + """ + Similar to pygfx.Image, only difference is that it contains a few properties to keep track of + row chunk index, column chunk index + """ + def __init__(self, geometry, material, row_chunk_ix: int, col_chunk_ix: int, **kwargs): + super().__init__(geometry, material, **kwargs) + + self._row_chunk_index = row_chunk_ix + self._col_chunk_index = col_chunk_ix + + def _wgpu_get_pick_info(self, pick_value): + pick_info = super()._wgpu_get_pick_info(pick_value) + + row_start_ix = WGPU_MAX_TEXTURE_SIZE * self.row_chunk_index + col_start_ix = WGPU_MAX_TEXTURE_SIZE * self.col_chunk_index + + # adjust w.r.t. chunk + x, y = pick_info["index"] + x += col_start_ix + y += row_start_ix + pick_info["index"] = (x, y) + + xp, yp = pick_info["pixel_coord"] + xp += col_start_ix + yp += row_start_ix + pick_info["pixel_coord"] = (xp, yp) + + # add row chunk and col chunk index to pick_info dict + return { + **pick_info, + "row_chunk_index": self.row_chunk_index, + "col_chunk_index": self.col_chunk_index, + } + + @property + def row_chunk_index(self) -> int: + return self._row_chunk_index + + @property + def col_chunk_index(self) -> int: + return self._col_chunk_index + + class ImageGraphic(Graphic, Interaction, _AddSelectorsMixin): features = {"data", "cmap", "vmin", "vmax"} @property - def data(self) -> ImageData: + def data(self) -> NDArray: """Get or set the image data""" return self._data @@ -236,142 +279,23 @@ def vmax(self) -> float: def vmax(self, value: float): self._vmax.set_value(self, value) - def __init__( - self, - data: Any, - vmin: int = None, - vmax: int = None, - cmap: str = "plasma", - filter: str = "nearest", - isolated_buffer: bool = True, - *args, - **kwargs, - ): - """ - Create an Image Graphic - - Parameters - ---------- - data: array-like - array-like, usually numpy.ndarray, must support ``memoryview()`` - Tensorflow Tensors also work **probably**, but not thoroughly tested - | shape must be ``[x_dim, y_dim]`` or ``[x_dim, y_dim, rgb]`` - - vmin: int, optional - minimum value for color scaling, calculated from data if not provided - - vmax: int, optional - maximum value for color scaling, calculated from data if not provided - - cmap: str, optional, default "plasma" - colormap to use to display the image data, ignored if data is RGB - - filter: str, optional, default "nearest" - interpolation filter, one of "nearest" or "linear" - - isolated_buffer: bool, default True - If True, initialize a buffer with the same shape as the input data and then - set the data, useful if the data arrays are ready-only such as memmaps. - If False, the input array is itself used as the buffer. - - args: - additional arguments passed to Graphic - - kwargs: - additional keyword arguments passed to Graphic - - Features - -------- - - **data**: :class:`.ImageDataFeature` - Manages the data buffer displayed in the ImageGraphic - - **cmap**: :class:`.ImageCmapFeature` - Manages the colormap - - **present**: :class:`.PresentFeature` - Control the presence of the Graphic in the scene - - """ - - super().__init__(*args, **kwargs) - self._data = ImageData(data, isolated_buffer=isolated_buffer) - self._cmap = ImageCmap(cmap) - - if (vmin is None) or (vmax is None): - vmin, vmax = quick_min_max(data) - - self._vmin = ImageVmin(vmin) - self._vmax = ImageVmax(vmax) - - clim = (self.vmin, self.vmax) - - # make grid geometry from image data Texture - geometry = pygfx.Geometry(grid=self._data.buffer) - - if self._data.value.ndim > 2: - # if data is RGB or RGBA - material = pygfx.ImageBasicMaterial( - clim=clim, map_interpolation=filter, pick_write=True - ) - else: - # if data is just 2D without color information, use colormap LUT - material = pygfx.ImageBasicMaterial( - clim=clim, - map=self._cmap.texture, - map_interpolation=filter, - pick_write=True, - ) - - world_object = pygfx.Image(geometry, material) - - self._set_world_object(world_object) - - def reset_vmin_vmax(self): - self.vmin, self.vmax = quick_min_max(self._data.value) - - def set_feature(self, feature: str, new_data: Any, indices: Any): - pass - - def reset_feature(self, feature: str): - pass - - -class _ImageTile(pygfx.Image): - """ - Similar to pygfx.Image, only difference is that it contains a few properties to keep track of - row chunk index, column chunk index - """ - - def _wgpu_get_pick_info(self, pick_value): - pick_info = super()._wgpu_get_pick_info(pick_value) - - # add row chunk and col chunk index to pick_info dict - return { - **pick_info, - "row_chunk_index": self.row_chunk_index, - "col_chunk_index": self.col_chunk_index, - } - @property - def row_chunk_index(self) -> int: - return self._row_chunk_index + def interpolation(self) -> str: + """image data interpolation method""" + return self._interpolation.value - @row_chunk_index.setter - def row_chunk_index(self, index: int): - self._row_chunk_index = index + @interpolation.setter + def interpolation(self, value: str): + self._interpolation.set_value(self, value) @property - def col_chunk_index(self) -> int: - return self._col_chunk_index + def cmap_interpolation(self) -> str: + """cmap interpolation method""" + return self._cmap_interpolation.value - @col_chunk_index.setter - def col_chunk_index(self, index: int): - self._col_chunk_index = index - - -class HeatmapGraphic(Graphic, Interaction, _AddSelectorsMixin): - feature_events = {"data", "cmap", "present"} + @cmap_interpolation.setter + def cmap_interpolation(self, value: str): + self._cmap_interpolation.set_value(self, value) def __init__( self, @@ -379,8 +303,8 @@ def __init__( vmin: int = None, vmax: int = None, cmap: str = "plasma", - filter: str = "nearest", - chunk_size: int = 8192, + interpolation: str = "nearest", + cmap_interpolation: str = "linear", isolated_buffer: bool = True, *args, **kwargs, @@ -392,7 +316,6 @@ def __init__( ---------- data: array-like array-like, usually numpy.ndarray, must support ``memoryview()`` - Tensorflow Tensors also work **probably**, but not thoroughly tested | shape must be ``[x_dim, y_dim]`` vmin: int, optional @@ -404,11 +327,11 @@ def __init__( cmap: str, optional, default "plasma" colormap to use to display the data - filter: str, optional, default "nearest" + interpolation: str, optional, default "nearest" interpolation filter, one of "nearest" or "linear" - chunk_size: int, default 8192, max 8192 - chunk size for each tile used to make up the heatmap texture + cmap_interpolation: str, optional, default "linear" + colormap interpolation method, one of "nearest" or "linear" isolated_buffer: bool, default True If True, initialize a buffer with the same shape as the input data and then @@ -437,77 +360,41 @@ def __init__( super().__init__(*args, **kwargs) - if chunk_size > 8192: - raise ValueError("Maximum chunk size is 8192") - - data = to_gpu_supported_dtype(data) - - # TODO: we need to organize and do this better - if isolated_buffer: - # initialize a buffer with the same shape as the input data - # we do not directly use the input data array as the buffer - # because if the input array is a read-only type, such as - # numpy memmaps, we would not be able to change the image data - buffer_init = np.zeros(shape=data.shape, dtype=data.dtype) - else: - buffer_init = data + world_object = pygfx.Group() - row_chunks = range(ceil(data.shape[0] / chunk_size)) - col_chunks = range(ceil(data.shape[1] / chunk_size)) + self._data = TextureArray(data, isolated_buffer=isolated_buffer) - chunks = list(product(row_chunks, col_chunks)) - # chunks is the index position of each chunk + if (vmin is None) or (vmax is None): + vmin, vmax = quick_min_max(data) - start_ixs = [list(map(lambda c: c * chunk_size, chunk)) for chunk in chunks] - stop_ixs = [list(map(lambda c: c + chunk_size, chunk)) for chunk in start_ixs] + self._vmin = ImageVmin(vmin) + self._vmax = ImageVmax(vmax) - world_object = pygfx.Group() - self._set_world_object(world_object) + self._cmap = ImageCmap(cmap) - if (vmin is None) or (vmax is None): - vmin, vmax = quick_min_max(data) + self._interpolation = ImageInterpolation(interpolation) + self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation) - self.cmap = HeatmapCmapFeature(self, cmap) self._material = pygfx.ImageBasicMaterial( clim=(vmin, vmax), - map=self.cmap(), - map_interpolation=filter, + map=self._cmap.texture, + interpolatio=self._interpolation.value, + map_interpolation=self._cmap_interpolation.value, pick_write=True, ) - for start, stop, chunk in zip(start_ixs, stop_ixs, chunks): - row_start, col_start = start - row_stop, col_stop = stop - - # x and y positions of the Tile in world space coordinates - y_pos, x_pos = row_start, col_start - - texture = pygfx.Texture( - buffer_init[row_start:row_stop, col_start:col_stop], dim=2 - ) - geometry = pygfx.Geometry(grid=texture) - # material = pygfx.ImageBasicMaterial(clim=(0, 1), map=self.cmap()) - - img = _ImageTile(geometry, self._material) - - # row and column chunk index for this Tile - img.row_chunk_index = chunk[0] - img.col_chunk_index = chunk[1] - - img.world.x = x_pos - img.world.y = y_pos - - self.world_object.add(img) + for row_ix in range(self._data.row_indices.size): + for col_ix in range(self._data.col_indices.size): + img = _ImageTile( + geometry=pygfx.Geometry(grid=self._data.buffer[row_ix, col_ix]), + material=self._material, + row_chunk_ix=row_ix, + col_chunk_ix=col_ix + ) - self.data = HeatmapDataFeature(self, buffer_init) - # TODO: we need to organize and do this better - if isolated_buffer: - # if the buffer was initialized with zeros - # set it with the actual data - self.data = data + img.world.x = self._data.row_indices[row_ix] + img.world.y = self._data.row_indices[col_ix] - def set_feature(self, feature: str, new_data: Any, indices: Any): - pass + world_object.add(img) - def reset_feature(self, feature: str): - pass + self._set_world_object(world_object) \ No newline at end of file From 820772d224f4129e3feb4c6077d5431c1092424c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 3 Jun 2024 22:19:23 -0400 Subject: [PATCH 069/196] image selector tool --- fastplotlib/graphics/image.py | 323 +++++++++++++++------------------- 1 file changed, 143 insertions(+), 180 deletions(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 19b1677c8..0831123ed 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -1,13 +1,12 @@ from typing import * import weakref -import numpy as np from numpy.typing import NDArray import pygfx from ..utils import quick_min_max -from ._base import Graphic, Interaction +from ._base import Graphic from .selectors import LinearSelector, LinearRegionSelector from ._features import ( TextureArray, @@ -20,182 +19,6 @@ ) -class _AddSelectorsMixin: - def add_linear_selector( - self, selection: int = None, padding: float = None, **kwargs - ) -> LinearSelector: - """ - Adds a :class:`.LinearSelector`. - - Parameters - ---------- - selection: int, optional - initial position of the selector - - padding: float, optional - pad the length of the selector - - kwargs: - passed to :class:`.LinearSelector` - - Returns - ------- - LinearSelector - - """ - - # default padding is 15% the height or width of the image - if "axis" in kwargs.keys(): - axis = kwargs["axis"] - else: - axis = "x" - - ( - bounds_init, - limits, - size, - origin, - axis, - end_points, - ) = self._get_linear_selector_init_args(padding, **kwargs) - - if selection is None: - selection = limits[0] - - if selection < limits[0] or selection > limits[1]: - raise ValueError( - f"the passed selection: {selection} is beyond the limits: {limits}" - ) - - selector = LinearSelector( - selection=selection, - limits=limits, - end_points=end_points, - parent=weakref.proxy(self), - **kwargs, - ) - - self._plot_area.add_graphic(selector, center=False) - selector.position_z = self.position_z + 1 - - return weakref.proxy(selector) - - def add_linear_region_selector( - self, padding: float = None, **kwargs - ) -> LinearRegionSelector: - """ - Add a :class:`.LinearRegionSelector`. - - Parameters - ---------- - padding: float, optional - Extends the linear selector along the y-axis to make it easier to interact with. - - kwargs: optional - passed to ``LinearRegionSelector`` - - Returns - ------- - LinearRegionSelector - linear selection graphic - - """ - - ( - bounds_init, - limits, - size, - origin, - axis, - end_points, - ) = self._get_linear_selector_init_args(padding, **kwargs) - - # create selector - selector = LinearRegionSelector( - selection=bounds_init, - limits=limits, - size=size, - origin=origin, - parent=weakref.proxy(self), - fill_color=(0, 0, 0.35, 0.2), - **kwargs, - ) - - self._plot_area.add_graphic(selector, center=False) - # so that it is above this graphic - selector.position_z = self.position_z + 3 - - # 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 weakref.proxy(selector) - - # TODO: this method is a bit of a mess, can refactor later - def _get_linear_selector_init_args(self, padding: float, **kwargs): - # computes initial bounds, limits, size and origin of linear selectors - data = self.data() - - if "axis" in kwargs.keys(): - axis = kwargs["axis"] - else: - axis = "x" - - if padding is None: - if axis == "x": - # based on number of rows - padding = int(data.shape[0] * 0.15) - elif axis == "y": - # based on number of columns - padding = int(data.shape[1] * 0.15) - - if axis == "x": - offset = self.position_x - # x limits, number of columns - limits = (offset, data.shape[1] - 1) - - # size is number of rows + padding - # used by LinearRegionSelector but not LinearSelector - size = data.shape[0] + padding - - # initial position of the selector - # center row - position_y = data.shape[0] / 2 - - # need y offset too for this - origin = (limits[0] - offset, position_y + self.position_y) - - # endpoints of the data range - # used by linear selector but not linear region - # padding, n_rows + padding - end_points = (0 - padding, data.shape[0] + padding) - else: - offset = self.position_y - # y limits - limits = (offset, data.shape[0] - 1) - - # width + padding - # used by LinearRegionSelector but not LinearSelector - size = data.shape[1] + padding - - # initial position of the selector - position_x = data.shape[1] / 2 - - # need x offset too for this - origin = (position_x + self.position_x, limits[0] - offset) - - # endpoints of the data range - # used by linear selector but not linear region - end_points = (0 - padding, data.shape[1] + padding) - - # initial bounds are 20% of the limits range - # used by LinearRegionSelector but not LinearSelector - bounds_init = (limits[0], int(np.ptp(limits) * 0.2) + offset) - - return bounds_init, limits, size, origin, axis, end_points - - def _add_plot_area_hook(self, plot_area): - self._plot_area = plot_area - - class _ImageTile(pygfx.Image): """ Similar to pygfx.Image, only difference is that it contains a few properties to keep track of @@ -240,7 +63,7 @@ def col_chunk_index(self) -> int: return self._col_chunk_index -class ImageGraphic(Graphic, Interaction, _AddSelectorsMixin): +class ImageGraphic(Graphic): features = {"data", "cmap", "vmin", "vmax"} @property @@ -397,4 +220,144 @@ def __init__( world_object.add(img) - self._set_world_object(world_object) \ No newline at end of file + self._set_world_object(world_object) + + def add_linear_selector( + self, selection: int = None, axis: str = "x", padding: float = None, **kwargs + ) -> LinearSelector: + """ + Adds a :class:`.LinearSelector`. + + Parameters + ---------- + selection: int, optional + initial position of the selector + + padding: float, optional + pad the length of the selector + + kwargs: + passed to :class:`.LinearSelector` + + Returns + ------- + LinearSelector + + """ + + if axis == "x": + size = self._data.value.shape[0] + center = size / 2 + limits = (0, self._data.value.shape[1]) + elif axis == "y": + size = self._data.value.shape[1] + center = size / 2 + limits = (0, self._data.value.shape[0]) + else: + raise ValueError( + "`axis` must be one of 'x' | 'y'" + ) + + # default padding is 25% the height or width of the image + if padding is None: + size *= 1.25 + else: + size += padding + + if selection is None: + selection = limits[0] + + if selection < limits[0] or selection > limits[1]: + raise ValueError( + f"the passed selection: {selection} is beyond the limits: {limits}" + ) + + selector = LinearSelector( + selection=selection, + limits=limits, + size=size, + center=center, + axis=axis, + parent=weakref.proxy(self), + **kwargs, + ) + + self._plot_area.add_graphic(selector, center=False) + + # place selector above this graphic + selector.offset = selector.offset + (0., 0., self.offset[-1] + 1) + + return weakref.proxy(selector) + + def add_linear_region_selector( + self, selection: tuple[float, float] = None, axis: str = "x", padding: float = 0., fill_color=(0, 0, 0.35, 0.2), **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) + initial (min, max) of the selection + + axis: "x" | "y" + axis the selector can move along + + padding: float, default 100.0 + Extends the linear selector along the perpendicular axis to make it easier to interact with. + + kwargs + passed to ``LinearRegionSelector`` + + Returns + ------- + LinearRegionSelector + linear selection graphic + + """ + + if axis == "x": + size = self._data.value.shape[0] + center = size / 2 + limits = (0, self._data.value.shape[1]) + elif axis == "y": + size = self._data.value.shape[1] + center = size / 2 + limits = (0, self._data.value.shape[0]) + else: + raise ValueError( + "`axis` must be one of 'x' | 'y'" + ) + + # default padding is 25% the height or width of the image + if padding is None: + size *= 1.25 + else: + size += padding + + if selection is None: + selection = limits[0], int(limits[1] * 0.25) + + if padding is None: + size *= 1.25 + + else: + size += padding + + selector = LinearRegionSelector( + selection=selection, + limits=limits, + size=size, + center=center, + axis=axis, + parent=weakref.proxy(self), + **kwargs, + ) + + self._plot_area.add_graphic(selector, center=False) + + # place above this graphic + selector.offset = selector.offset + (0., 0., self.offset[-1] + 1) + + return weakref.proxy(selector) From 254f0ba05096fe684030173ffdc07a88ef46bd66 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 3 Jun 2024 22:21:31 -0400 Subject: [PATCH 070/196] return selectors as proxies --- fastplotlib/graphics/line.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 629b2cad6..f97f95b1f 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -184,7 +184,7 @@ def add_linear_selector( size=size, center=center, axis=axis, - parent=self, + parent=weakref.proxy(self), **kwargs, ) @@ -249,7 +249,7 @@ def add_linear_region_selector( size=size, center=center, axis=axis, - parent=self, + parent=weakref.proxy(self), **kwargs, ) @@ -316,9 +316,6 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): return bounds_init, limits, size, origin, axis, end_points - def _fpl_add_plot_area_hook(self, plot_area): - self._plot_area = plot_area - def set_feature(self, feature: str, new_data: Any, indices: Any = None): if not hasattr(self, "_previous_data"): self._previous_data = dict() From 2bf33fade95a5af84a1f7628d22bf7bc17bfbb6e Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 3 Jun 2024 22:41:07 -0400 Subject: [PATCH 071/196] image stuff works --- fastplotlib/graphics/__init__.py | 12 +-- fastplotlib/graphics/_features/_image.py | 21 ++--- fastplotlib/graphics/image.py | 11 ++- fastplotlib/graphics/selectors/__init__.py | 6 -- fastplotlib/layouts/_graphic_methods_mixin.py | 90 ++----------------- 5 files changed, 29 insertions(+), 111 deletions(-) diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py index 2a008015e..bb3cd8854 100644 --- a/fastplotlib/graphics/__init__.py +++ b/fastplotlib/graphics/__init__.py @@ -1,15 +1,5 @@ from .line import LineGraphic from .scatter import ScatterGraphic -from .image import ImageGraphic, HeatmapGraphic +from .image import ImageGraphic from .text import TextGraphic from .line_collection import LineCollection, LineStack - -__all__ = [ - "ImageGraphic", - "ScatterGraphic", - "LineGraphic", - "HeatmapGraphic", - "LineCollection", - "LineStack", - "TextGraphic", -] diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py index 55bb5a745..1c71b8d4a 100644 --- a/fastplotlib/graphics/_features/_image.py +++ b/fastplotlib/graphics/_features/_image.py @@ -54,7 +54,7 @@ def __init__(self, data, isolated_buffer: bool = True): @property def value(self) -> NDArray: - return self._data + return self._value def set_value(self, graphic, value): self[:] = value @@ -109,8 +109,8 @@ def value(self) -> float: return self._value def set_value(self, graphic, value: float): - vmax = graphic.world_object.material.clim[1] - graphic.world_object.material.clim = (value, vmax) + vmax = graphic._material.clim[1] + graphic._material.clim = (value, vmax) self._value = value event = FeatureEvent(type="vmin", info={"value": value}) @@ -128,8 +128,8 @@ def value(self) -> float: return self._value def set_value(self, graphic, value: float): - vmin = graphic.world_object.material.clim[0] - graphic.world_object.material.clim = (vmin, value) + vmin = graphic._material.clim[0] + graphic._material.clim = (vmin, value) self._value = value event = FeatureEvent(type="vmax", info={"value": value}) @@ -149,8 +149,8 @@ def value(self) -> str: def set_value(self, graphic, value: str): new_colors = make_colors(256, value) - graphic.world_object.material.map.data[:] = new_colors - graphic.world_object.material.map.data.update_range((0, 0, 0), size=(256, 1, 1)) + graphic._material.map.data[:] = new_colors + graphic._material.map.update_range((0, 0, 0), size=(256, 1, 1)) self._value = value event = FeatureEvent(type="cmap", info={"value": value}) @@ -175,7 +175,7 @@ def value(self) -> str: def set_value(self, graphic, value: str): self._validate(value) - graphic.world_object.material.interpolation = value + graphic._material.interpolation = value self._value = value event = FeatureEvent(type="interpolation", info={"value": value}) @@ -201,8 +201,9 @@ def value(self) -> str: def set_value(self, graphic, value: str): self._validate(value) - graphic.world_object.material.map_interpolation = value + # common material for all image tiles + graphic._material.map_interpolation = value self._value = value - event = FeatureEvent(type="interpolation", info={"value": value}) + event = FeatureEvent(type="cmap_interpolation", info={"value": value}) self._call_event_handlers(event) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 0831123ed..f3603dee1 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -200,8 +200,8 @@ def __init__( self._material = pygfx.ImageBasicMaterial( clim=(vmin, vmax), - map=self._cmap.texture, - interpolatio=self._interpolation.value, + map=self._cmap.texture if self._data.value.ndim == 2 else None, # RGB vs. grayscale + interpolation=self._interpolation.value, map_interpolation=self._cmap_interpolation.value, pick_write=True, ) @@ -222,6 +222,11 @@ def __init__( self._set_world_object(world_object) + def reset_vmin_vmax(self): + vmin, vmax = quick_min_max(self._data.value) + self.vmin = vmin + self.vmax = vmax + def add_linear_selector( self, selection: int = None, axis: str = "x", padding: float = None, **kwargs ) -> LinearSelector: @@ -290,7 +295,7 @@ def add_linear_selector( return weakref.proxy(selector) def add_linear_region_selector( - self, selection: tuple[float, float] = None, axis: str = "x", padding: float = 0., fill_color=(0, 0, 0.35, 0.2), **kwargs, + self, selection: tuple[float, float] = None, axis: str = "x", padding: float = 0., fill_color = (0, 0, 0.35, 0.2), **kwargs, ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, diff --git a/fastplotlib/graphics/selectors/__init__.py b/fastplotlib/graphics/selectors/__init__.py index 113772173..6f081448e 100644 --- a/fastplotlib/graphics/selectors/__init__.py +++ b/fastplotlib/graphics/selectors/__init__.py @@ -1,9 +1,3 @@ from ._linear import LinearSelector from ._linear_region import LinearRegionSelector from ._polygon import PolygonSelector - -__all__ = [ - "LinearSelector", - "LinearRegionSelector", - "PolygonSelector", -] diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index 6765c33be..00bdd5e85 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -28,18 +28,18 @@ def _create_graphic(self, graphic_class, *args, **kwargs) -> Graphic: # only return a proxy to the real graphic return weakref.proxy(graphic) - def add_heatmap( + def add_image( self, data: Any, vmin: int = None, vmax: int = None, cmap: str = "plasma", - filter: str = "nearest", - chunk_size: int = 8192, + interpolation: str = "nearest", + cmap_interpolation: str = "linear", isolated_buffer: bool = True, *args, **kwargs - ) -> HeatmapGraphic: + ) -> ImageGraphic: """ Create an Image Graphic @@ -48,7 +48,6 @@ def add_heatmap( ---------- data: array-like array-like, usually numpy.ndarray, must support ``memoryview()`` - Tensorflow Tensors also work **probably**, but not thoroughly tested | shape must be ``[x_dim, y_dim]`` vmin: int, optional @@ -60,11 +59,11 @@ def add_heatmap( cmap: str, optional, default "plasma" colormap to use to display the data - filter: str, optional, default "nearest" + interpolation: str, optional, default "nearest" interpolation filter, one of "nearest" or "linear" - chunk_size: int, default 8192, max 8192 - chunk size for each tile used to make up the heatmap texture + cmap_interpolation: str, optional, default "linear" + colormap interpolation method, one of "nearest" or "linear" isolated_buffer: bool, default True If True, initialize a buffer with the same shape as the input data and then @@ -90,78 +89,6 @@ def add_heatmap( Control the presence of the Graphic in the scene - """ - return self._create_graphic( - HeatmapGraphic, - data, - vmin, - vmax, - cmap, - filter, - chunk_size, - isolated_buffer, - *args, - **kwargs - ) - - def add_image( - self, - data: Any, - vmin: int = None, - vmax: int = None, - cmap: str = "plasma", - filter: str = "nearest", - isolated_buffer: bool = True, - *args, - **kwargs - ) -> ImageGraphic: - """ - - Create an Image Graphic - - Parameters - ---------- - data: array-like - array-like, usually numpy.ndarray, must support ``memoryview()`` - Tensorflow Tensors also work **probably**, but not thoroughly tested - | shape must be ``[x_dim, y_dim]`` or ``[x_dim, y_dim, rgb]`` - - vmin: int, optional - minimum value for color scaling, calculated from data if not provided - - vmax: int, optional - maximum value for color scaling, calculated from data if not provided - - cmap: str, optional, default "plasma" - colormap to use to display the image data, ignored if data is RGB - - filter: str, optional, default "nearest" - interpolation filter, one of "nearest" or "linear" - - isolated_buffer: bool, default True - If True, initialize a buffer with the same shape as the input data and then - set the data, useful if the data arrays are ready-only such as memmaps. - If False, the input array is itself used as the buffer. - - args: - additional arguments passed to Graphic - - kwargs: - additional keyword arguments passed to Graphic - - Features - -------- - - **data**: :class:`.ImageDataFeature` - Manages the data buffer displayed in the ImageGraphic - - **cmap**: :class:`.ImageCmapFeature` - Manages the colormap - - **present**: :class:`.PresentFeature` - Control the presence of the Graphic in the scene - - """ return self._create_graphic( ImageGraphic, @@ -169,7 +96,8 @@ def add_image( vmin, vmax, cmap, - filter, + interpolation, + cmap_interpolation, isolated_buffer, *args, **kwargs From 67404caef6b961f838e2b378cf065b55ce14440f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 3 Jun 2024 23:00:15 -0400 Subject: [PATCH 072/196] fix offsets adding graphics, fix positions_graphic cmap bug, quickstart runs :D --- .../graphics/_features/_positions_graphics.py | 5 +++-- fastplotlib/layouts/_plot_area.py | 14 ++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index c83fdd7e8..4ccf639a6 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -305,8 +305,9 @@ def __init__(self, vertex_colors: VertexColors, cmap_name: str | None, cmap_valu if self._cmap_name is not None: if not isinstance(self._cmap_name, str): raise TypeError - if not isinstance(self._cmap_values, np.ndarray): - raise TypeError + if self._cmap_values is not None: + if not isinstance(self._cmap_values, np.ndarray): + raise TypeError n_datapoints = vertex_colors.value.shape[0] diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 6ff07a748..4d8900971 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -469,14 +469,14 @@ def add_graphic(self, graphic: Graphic, center: bool = True): if self.camera.fov == 0: # for orthographic positions stack objects along the z-axis # for perspective projections we assume the user wants full 3D control - graphic.position_z = len(self) + graphic.offset = (*graphic.offset[:-1], len(self)) def insert_graphic( self, graphic: Graphic, center: bool = True, index: int = 0, - z_position: int = None, + auto_offset: int = None, ): """ Insert graphic into scene at given position ``index`` in stored graphics. @@ -493,8 +493,8 @@ def insert_graphic( index: int, default 0 Index to insert graphic. - z_position: int, default None - z axis position to place Graphic. If ``None``, uses value of `index` argument + auto_offset: bool, default True + If True and using an orthographic projection, sets z-axis offset of graphic to `index` """ if index > len(self._graphics): @@ -511,10 +511,8 @@ def insert_graphic( if self.camera.fov == 0: # for orthographic positions stack objects along the z-axis # for perspective projections we assume the user wants full 3D control - if z_position is None: - graphic.position_z = index - else: - graphic.position_z = z_position + if auto_offset: + graphic.offset = (*graphic.offset[:-1], index) def _add_or_insert_graphic( self, From b1b297d8c0ec523a7ba2718b73a2d37cabfd4859 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 4 Jun 2024 01:41:33 -0400 Subject: [PATCH 073/196] fix add_graphic args and mixin --- .../graphics/_features/_positions_graphics.py | 38 +------------ fastplotlib/graphics/image.py | 6 +-- fastplotlib/graphics/line.py | 10 ---- fastplotlib/graphics/scatter.py | 5 -- fastplotlib/graphics/text.py | 3 +- fastplotlib/layouts/_graphic_methods_mixin.py | 54 ++++--------------- scripts/generate_add_graphic_methods.py | 2 +- 7 files changed, 13 insertions(+), 105 deletions(-) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index 4ccf639a6..c6a96b709 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -373,40 +373,4 @@ def values(self, values: np.ndarray | list[float | int], indices: slice | list | self._vertex_colors[indices] = colors - self._emit_event("cmap.name", indices, values) - -# -# class HeatmapCmapFeature(ImageCmapFeature): -# """ -# Colormap for :class:`HeatmapGraphic` -# -# Same event pick info as :class:`ImageCmapFeature` -# """ -# -# def _set(self, cmap_name: str): -# # in heatmap we use one material for all ImageTiles -# self._parent._material.map.data[:] = make_colors(256, cmap_name) -# self._parent._material.map.update_range((0, 0, 0), size=(256, 1, 1)) -# self._name = cmap_name -# -# self._feature_changed(key=None, new_data=self.name) -# -# @property -# def vmin(self) -> float: -# """Minimum contrast limit.""" -# return self._parent._material.clim[0] -# -# @vmin.setter -# def vmin(self, value: float): -# """Minimum contrast limit.""" -# self._parent._material.clim = (value, self._parent._material.clim[1]) -# -# @property -# def vmax(self) -> float: -# """Maximum contrast limit.""" -# return self._parent._material.clim[1] -# -# @vmax.setter -# def vmax(self, value: float): -# """Maximum contrast limit.""" -# self._parent._material.clim = (self._parent._material.clim[0], value) + self._emit_event("cmap.values", indices, values) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index f3603dee1..ea43ee42f 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -129,7 +129,6 @@ def __init__( interpolation: str = "nearest", cmap_interpolation: str = "linear", isolated_buffer: bool = True, - *args, **kwargs, ): """ @@ -161,9 +160,6 @@ def __init__( set the data, useful if the data arrays are ready-only such as memmaps. If False, the input array is itself used as the buffer. - args: - additional arguments passed to Graphic - kwargs: additional keyword arguments passed to Graphic @@ -181,7 +177,7 @@ def __init__( """ - super().__init__(*args, **kwargs) + super().__init__(**kwargs) world_object = pygfx.Group() diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index f97f95b1f..b0df8f7ce 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -31,10 +31,7 @@ def __init__( alpha: float = 1.0, cmap: str = None, cmap_values: np.ndarray | Iterable = None, - z_position: float = None, - collection_index: int = None, isolated_buffer: bool = True, - *args, **kwargs, ): """ @@ -65,9 +62,6 @@ def __init__( z_position: float, optional z-axis position for placing the graphic - args - passed to Graphic - kwargs passed to Graphic @@ -100,7 +94,6 @@ def __init__( cmap=cmap, cmap_values=cmap_values, isolated_buffer=isolated_buffer, - *args, **kwargs ) @@ -125,9 +118,6 @@ def __init__( self._set_world_object(world_object) - if z_position is not None: - self.position_z = z_position - def add_linear_selector( self, selection: float = None, padding: float = 0., axis: str = "x",**kwargs ) -> LinearSelector: diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index f3afcd31a..a935b8092 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -39,7 +39,6 @@ def __init__( isolated_buffer: bool = True, sizes: float | np.ndarray | Iterable[float] = 1, uniform_sizes: bool = False, - *args, **kwargs, ): """ @@ -70,9 +69,6 @@ def __init__( z_position: float, optional z-axis position for placing the graphic - args - passed to Graphic - kwargs passed to Graphic @@ -101,7 +97,6 @@ def __init__( cmap=cmap, cmap_values=cmap_values, isolated_buffer=isolated_buffer, - *args, **kwargs ) diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index 49b4ac4be..27d49eece 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -16,7 +16,6 @@ def __init__( outline_thickness=0, screen_space: bool = True, anchor: str = "middle-center", - *args, **kwargs, ): """ @@ -55,7 +54,7 @@ def __init__( * Vertical values: "top", "middle", "baseline", "bottom" * Horizontal values: "left", "center", "right" """ - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self._text = text diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index 00bdd5e85..d523bc668 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -37,7 +37,6 @@ def add_image( interpolation: str = "nearest", cmap_interpolation: str = "linear", isolated_buffer: bool = True, - *args, **kwargs ) -> ImageGraphic: """ @@ -70,9 +69,6 @@ def add_image( set the data, useful if the data arrays are ready-only such as memmaps. If False, the input array is itself used as the buffer. - args: - additional arguments passed to Graphic - kwargs: additional keyword arguments passed to Graphic @@ -99,22 +95,21 @@ def add_image( interpolation, cmap_interpolation, isolated_buffer, - *args, **kwargs ) def add_line_collection( self, data: List[numpy.ndarray], - z_offset: Union[Iterable[float | int], float, int] = None, - thickness: Union[float, Iterable[float]] = 2.0, - colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", + thickness: Union[float, Sequence[float]] = 2.0, + colors: Union[str, Sequence[str], numpy.ndarray, Sequence[numpy.ndarray]] = "w", + uniform_colors: bool = False, alpha: float = 1.0, - cmap: Union[Iterable[str], str] = None, + cmap: Union[Sequence[str], str] = None, cmap_values: Union[numpy.ndarray, List] = None, name: str = None, - metadata: Union[Iterable[Any], numpy.ndarray] = None, - *args, + metadata: Union[Sequence[Any], numpy.ndarray] = None, + isolated_buffer: bool = True, **kwargs ) -> LineCollection: """ @@ -127,10 +122,6 @@ def add_line_collection( 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_offset: Iterable of float or float, optional - | if ``float`` | ``int``, single offset will be used for all lines - | if ``list`` of ``float`` | ``int``, each value will apply to the individual lines - 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 @@ -161,11 +152,8 @@ def add_line_collection( metadata associated with this collection, this is for the user to manage. ``len(metadata)`` must be same as ``len(data)`` - args - passed to GraphicCollection - kwargs - passed to GraphicCollection + passed to Graphic Features -------- @@ -179,15 +167,15 @@ def add_line_collection( return self._create_graphic( LineCollection, data, - z_offset, thickness, colors, + uniform_colors, alpha, cmap, cmap_values, name, metadata, - *args, + isolated_buffer, **kwargs ) @@ -200,10 +188,7 @@ def add_line( alpha: float = 1.0, cmap: str = None, cmap_values: Union[numpy.ndarray, Iterable] = None, - z_position: float = None, - collection_index: int = None, isolated_buffer: bool = True, - *args, **kwargs ) -> LineGraphic: """ @@ -235,9 +220,6 @@ def add_line( z_position: float, optional z-axis position for placing the graphic - args - passed to Graphic - kwargs passed to Graphic @@ -271,17 +253,13 @@ def add_line( alpha, cmap, cmap_values, - z_position, - collection_index, isolated_buffer, - *args, **kwargs ) def add_line_stack( self, data: List[numpy.ndarray], - z_offset: 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, @@ -291,7 +269,6 @@ def add_line_stack( metadata: Union[Iterable[Any], numpy.ndarray] = None, separation: float = 10.0, separation_axis: str = "y", - *args, **kwargs ) -> LineStack: """ @@ -304,10 +281,6 @@ def add_line_stack( 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_offset: Iterable of float or float, optional - | if ``float``, single offset will be used for all lines - | if ``list`` of ``float``, each value will apply to the individual lines - 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 @@ -356,7 +329,6 @@ def add_line_stack( return self._create_graphic( LineStack, data, - z_offset, thickness, colors, alpha, @@ -366,7 +338,6 @@ def add_line_stack( metadata, separation, separation_axis, - *args, **kwargs ) @@ -381,7 +352,6 @@ def add_scatter( isolated_buffer: bool = True, sizes: Union[float, numpy.ndarray, Iterable[float]] = 1, uniform_sizes: bool = False, - *args, **kwargs ) -> ScatterGraphic: """ @@ -413,9 +383,6 @@ def add_scatter( z_position: float, optional z-axis position for placing the graphic - args - passed to Graphic - kwargs passed to Graphic @@ -447,7 +414,6 @@ def add_scatter( isolated_buffer, sizes, uniform_sizes, - *args, **kwargs ) @@ -461,7 +427,6 @@ def add_text( outline_thickness=0, screen_space: bool = True, anchor: str = "middle-center", - *args, **kwargs ) -> TextGraphic: """ @@ -512,6 +477,5 @@ def add_text( outline_thickness, screen_space, anchor, - *args, **kwargs ) diff --git a/scripts/generate_add_graphic_methods.py b/scripts/generate_add_graphic_methods.py index 2a480d884..3f45d9007 100644 --- a/scripts/generate_add_graphic_methods.py +++ b/scripts/generate_add_graphic_methods.py @@ -69,7 +69,7 @@ def generate_add_graphics_methods(): f.write(f" {class_name.__init__.__doc__}\n") f.write(' """\n') f.write( - f" return self._create_graphic({class_name.__name__}, {s}*args, **kwargs)\n\n" + f" return self._create_graphic({class_name.__name__}, {s} **kwargs)\n\n" ) f.close() From 74f842893f8b5c63fc905fa0456c3fc011bb0115 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 4 Jun 2024 01:43:14 -0400 Subject: [PATCH 074/196] simpler graphic collection stuff --- fastplotlib/graphics/_base.py | 194 ++++++++---------------- fastplotlib/graphics/line_collection.py | 121 +++++++++------ 2 files changed, 138 insertions(+), 177 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 3ed02c4af..c06f5681f 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -104,7 +104,6 @@ def __init__( offset: np.ndarray | list | tuple = (0., 0., 0.), rotation: np.ndarray | list | tuple = (0., 0., 0., 1.), metadata: Any = None, - collection_index: int = None, ): """ @@ -113,6 +112,12 @@ def __init__( name: str, optional name this graphic to use it as a key to access from the plot + offset: (float, float, float), default (0., 0., 0.) + (x, y, z) vector to offset this graphic from the origin + + rotation: (float, float, float, float), default (0, 0, 0, 1) + rotation quaternion + metadata: Any, optional metadata attached to this Graphic, this is for the user to manage @@ -121,7 +126,6 @@ def __init__( raise TypeError("Graphic `name` must be of type ") self.metadata = metadata - self.collection_index = collection_index self.registered_callbacks = dict() # store hex id str of Graphic instance mem location @@ -138,7 +142,7 @@ def __init__( # all the common features self._name = Name(name) self._deleted = Deleted(False) - self._rotation = Rotation(rotation) # set later when world object is set + self._rotation = Rotation(rotation) self._offset = Offset(offset) self._visible = Visible(True) @@ -165,11 +169,6 @@ def detach_feature(self, feature: str): def attach_feature(self, feature: BufferManager): raise NotImplementedError - @property - def children(self) -> list[pygfx.WorldObject]: - """Return the children of the WorldObject.""" - return self.world_object.children - @property def event_handlers(self) -> list[tuple[str, callable, ...]]: """ @@ -729,11 +728,52 @@ class PreviouslyModifiedData: COLLECTION_GRAPHICS: dict[HexStr, Graphic] = dict() +class CollectionIndexer: + """Collection Indexer""" + + def __init__( + self, + selection: np.ndarray[Graphic], + ): + """ + + Parameters + ---------- + + selection: np.ndarray of Graphics + array of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` + + """ + + self._selection = selection + + @property + def graphics(self) -> np.ndarray[Graphic]: + """Returns an array of the selected graphics. Always returns a proxy to the Graphic""" + return tuple(self._selection) + + def __getitem__(self, item): + return self.graphics[item] + + def __len__(self): + return len(self._selection) + + def __repr__(self): + return ( + f"{self.__class__.__name__} @ {hex(id(self))}\n" + f"Selection of <{len(self._selection)}> {self._selection[0].__class__.__name__}" + ) + + class GraphicCollection(Graphic): """Graphic Collection base class""" + child_type: type + _indexer: type def __init__(self, name: str = None): super().__init__(name) + + # list of mem locations of the graphics self._graphics: list[str] = list() self._graphics_changed: bool = True @@ -752,7 +792,7 @@ def graphics(self) -> np.ndarray[Graphic]: return self._graphics_array - def add_graphic(self, graphic: Graphic, reset_index: False): + def add_graphic(self, graphic: Graphic): """ Add a graphic to the collection. @@ -761,15 +801,12 @@ def add_graphic(self, graphic: Graphic, reset_index: False): graphic: Graphic graphic to add, must be a real ``Graphic`` not a proxy - reset_index: bool, default ``False`` - reset the collection index - """ - if not type(graphic).__name__ == self.child_type: + if not type(graphic) == self.child_type: raise TypeError( - f"Can only add graphics of the same type to a collection, " - f"You can only add {self.child_type} to a {self.__class__.__name__}, " + f"Can only add graphics of the same type to a collection.\n" + f"You can only add {self.child_type.__name__} to a {self.__class__.__name__}, " f"you are trying to add a {graphic.__class__.__name__}." ) @@ -778,41 +815,32 @@ def add_graphic(self, graphic: Graphic, reset_index: False): self._graphics.append(addr) - if reset_index: - self._reset_index() - elif graphic.collection_index is None: - graphic.collection_index = len(self) - self.world_object.add(graphic.world_object) self._graphics_changed = True - def remove_graphic(self, graphic: Graphic, reset_index: True): + def remove_graphic(self, graphic: Graphic): """ Remove a graphic from the collection. + Note: Only removes the graphic from the collection. Does not remove + the graphic from the scene, and does not delete the graphic. + Parameters ---------- graphic: Graphic graphic to remove - reset_index: bool, default ``False`` - reset the collection index - """ self._graphics.remove(graphic._fpl_address) - if reset_index: - self._reset_index() - self.world_object.remove(graphic.world_object) self._graphics_changed = True - def __getitem__(self, key): - return CollectionIndexer( - parent=self, + def __getitem__(self, key) -> CollectionIndexer: + return self._indexer( selection=self.graphics[key], ) @@ -824,10 +852,6 @@ def __del__(self): super().__del__() - def _reset_index(self): - for new_index, graphic in enumerate(self._graphics): - graphic.collection_index = new_index - def __len__(self): return len(self._graphics) @@ -836,70 +860,10 @@ def __repr__(self): return f"{rval}\nCollection of <{len(self._graphics)}> Graphics" -class CollectionIndexer: - """Collection Indexer""" - - def __init__( - self, - parent: GraphicCollection, - selection: list[Graphic], - ): - """ - - Parameters - ---------- - parent: GraphicCollection - the GraphicCollection object that is being indexed - - selection: list of Graphics - a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` - - """ - - self._parent = weakref.proxy(parent) - self._selection = selection - - # we use parent.graphics[0] instead of selection[0] - # because the selection can be empty - for attr_name in self._parent.graphics[0].__dict__.keys(): - attr = getattr(self._parent.graphics[0], attr_name) - if isinstance(attr, GraphicFeature): - collection_feature = CollectionFeature( - self._selection, feature=attr_name - ) - collection_feature.__doc__ = ( - f"indexable <{attr_name}> feature for collection" - ) - setattr(self, attr_name, collection_feature) - - @property - def graphics(self) -> np.ndarray[Graphic]: - """Returns an array of the selected graphics. Always returns a proxy to the Graphic""" - return tuple(self._selection) - - def __setattr__(self, key, value): - if hasattr(self, key): - attr = getattr(self, key) - if isinstance(attr, CollectionFeature): - attr._set(value) - return - - super().__setattr__(key, value) - - def __len__(self): - return len(self._selection) - - def __repr__(self): - return ( - f"{self.__class__.__name__} @ {hex(id(self))}\n" - f"Selection of <{len(self._selection)}> {self._selection[0].__class__.__name__}" - ) - - class CollectionFeature: """Collection Feature""" - def __init__(self, selection: list[Graphic], feature: str): + def __init__(self, selection: np.ndarray[Graphic], feature: str): """ selection: list of Graphics a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` @@ -912,50 +876,14 @@ def __init__(self, selection: list[Graphic], feature: str): self._selection = selection self._feature = feature - self._feature_instances: list[GraphicFeature] = list() - - if len(self._selection) > 0: - for graphic in self._selection: - fi = getattr(graphic, self._feature) - self._feature_instances.append(fi) - - if isinstance(fi, GraphicFeatureIndexable): - self._indexable = True - else: - self._indexable = False - else: # it's an empty selection so it doesn't really matter - self._indexable = False - - def _set(self, value): - self[:] = value + self._feature_instances = [getattr(g, feature) for g in self._selection] def __getitem__(self, item): - # only for indexable graphic features return [fi[item] for fi in self._feature_instances] def __setitem__(self, key, value): - if self._indexable: - for fi in self._feature_instances: - fi[key] = value - - else: - for fi in self._feature_instances: - fi._set(value) - - def add_event_handler(self, handler: callable): - """Adds an event handler to each of the selected Graphics from the parent GraphicCollection""" - for fi in self._feature_instances: - fi.add_event_handler(handler) - - def remove_event_handler(self, handler: callable): - """Removes an event handler from each of the selected Graphics of the parent GraphicCollection""" - for fi in self._feature_instances: - fi.remove_event_handler(handler) - - def block_events(self, b: bool): - """Blocks event handling from occurring.""" for fi in self._feature_instances: - fi.block_events(b) + fi[key] = value def __repr__(self): return f"Collection feature for: <{self._feature}>" diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index da74cc54e..5941e7a9a 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -7,27 +7,84 @@ import pygfx from ..utils import parse_cmap_values -from ._base import Interaction, PreviouslyModifiedData, GraphicCollection -from ._features import GraphicFeature +from ._base import Interaction, PreviouslyModifiedData, GraphicCollection, CollectionIndexer, CollectionFeature +from ._features import GraphicFeature, VertexColors, VertexPositions from .line import LineGraphic from .selectors import LinearRegionSelector, LinearSelector +class LineSelection(CollectionIndexer): + """A sub-selection of a line-collection""" + @property + def name(self) -> np.ndarray[str | None]: + return np.asarray([g.name for g in self.graphics]) + + @name.setter + def name(self, values: np.ndarray[str] | list[str]): + if not len(values) == len(self): + raise IndexError + + for g, v in zip(self.graphics, values): + g.name = v + + @property + def colors(self) -> CollectionFeature: + 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 + self.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 + + elif len(values) == 4: + # assume RGBA + self.colors[:] = values + + else: + # assume individual colors for each + for g, v in zip(self.graphics, values): + g.colors = v + + @property + def data(self) -> CollectionFeature: + return CollectionFeature(self.graphics, "data") + + @data.setter + def data(self, values): + self.data[:] = values + + def add_event_handler(self): + pass + + class LineCollection(GraphicCollection, Interaction): - child_type = LineGraphic.__name__ + child_type = LineGraphic + _indexer = LineSelection def __init__( self, data: List[np.ndarray], - z_offset: Iterable[float | int] | float | int = None, - thickness: float | Iterable[float] = 2.0, - colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", + 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: Iterable[str] | str = None, + cmap: Sequence[str] | str = None, cmap_values: np.ndarray | List = None, name: str = None, - metadata: Iterable[Any] | np.ndarray = None, - *args, + metadata: Sequence[Any] | np.ndarray = None, + isolated_buffer: bool = True, **kwargs, ): """ @@ -39,10 +96,6 @@ def __init__( 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_offset: Iterable of float or float, optional - | if ``float`` | ``int``, single offset will be used for all lines - | if ``list`` of ``float`` | ``int``, each value will apply to the individual lines - 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 @@ -73,11 +126,8 @@ def __init__( metadata associated with this collection, this is for the user to manage. ``len(metadata)`` must be same as ``len(data)`` - args - passed to GraphicCollection - kwargs - passed to GraphicCollection + passed to Graphic Features -------- @@ -90,12 +140,6 @@ def __init__( super().__init__(name) - if not isinstance(z_offset, (float, int)) and z_offset is not None: - if len(data) != len(z_offset): - raise ValueError( - "z_position must be a single float or an iterable with same length as data" - ) - if not isinstance(thickness, (float, int)): if len(thickness) != len(data): raise ValueError( @@ -178,11 +222,6 @@ def __init__( self._set_world_object(pygfx.Group()) for i, d in enumerate(data): - if isinstance(z_offset, list): - _z = z_offset[i] - else: - _z = z_offset - if isinstance(thickness, list): _s = thickness[i] else: @@ -208,13 +247,14 @@ def __init__( data=d, thickness=_s, colors=_c, - z_position=_z, + uniform_colors=uniform_colors, cmap=_cmap, - collection_index=i, metadata=_m, + isolated_buffer=isolated_buffer, + **kwargs ) - self.add_graphic(lg, reset_index=False) + self.add_graphic(lg) @property def cmap(self) -> str: @@ -330,7 +370,7 @@ def add_linear_region_selector( ) = self._get_linear_selector_init_args(padding, **kwargs) selector = LinearRegionSelector( - bounds=bounds, + selection=bounds, limits=limits, size=size, origin=origin, @@ -478,7 +518,6 @@ class LineStack(LineCollection): def __init__( self, data: List[np.ndarray], - z_offset: Iterable[float] | float = None, thickness: float | Iterable[float] = 2.0, colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", alpha: float = 1.0, @@ -488,7 +527,6 @@ def __init__( metadata: Iterable[Any] | np.ndarray = None, separation: float = 10.0, separation_axis: str = "y", - *args, **kwargs, ): """ @@ -500,10 +538,6 @@ def __init__( 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_offset: Iterable of float or float, optional - | if ``float``, single offset will be used for all lines - | if ``list`` of ``float``, each value will apply to the individual lines - 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 @@ -550,27 +584,26 @@ def __init__( """ super().__init__( data=data, - z_offset=z_offset, thickness=thickness, colors=colors, alpha=alpha, cmap=cmap, cmap_values=cmap_values, - metadata=metadata, name=name, - *args, + metadata=metadata, **kwargs, ) axis_zero = 0 for i, line in enumerate(self.graphics): if separation_axis == "x": - line.position_x = axis_zero + line.offset = (axis_zero, *line.offset[1:]) + elif separation_axis == "y": - line.position_y = axis_zero + line.offset = (line.offset[0], axis_zero, line.offset[2]) axis_zero = ( - axis_zero + line.data()[:, axes[separation_axis]].max() + separation + axis_zero + line.data.value[:, axes[separation_axis]].max() + separation ) self.separation = separation From 6eb5a1a8b49b96c424361327e4709f3607136950 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 4 Jun 2024 03:13:24 -0400 Subject: [PATCH 075/196] more line collection --- fastplotlib/graphics/_base.py | 110 ++++++++++++++++++++++++ fastplotlib/graphics/line_collection.py | 47 ++++++---- 2 files changed, 141 insertions(+), 16 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index c06f5681f..f4ab6b7eb 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -730,10 +730,51 @@ class PreviouslyModifiedData: class CollectionIndexer: """Collection Indexer""" + @property + def name(self) -> np.ndarray[str | None]: + return np.asarray([g.name for g in self.graphics]) + + @name.setter + def name(self, values: np.ndarray[str] | list[str]): + self._set_feature("name", values) + + @property + def offset(self) -> np.ndarray: + return np.stack([g.offset for g in self.graphics]) + + @offset.setter + def offset(self, values: np.ndarray | list[np.ndarray]): + self._set_feature("offset", values) + + @property + def rotation(self) -> np.ndarray: + return np.stack([g.rotation for g in self.graphics]) + + @rotation.setter + def rotation(self, values: np.ndarray | list[np.ndarray]): + self._set_feature("rotation", values) + + @property + def visible(self) -> np.ndarray[bool]: + return np.asarray([g.visible for g in self.graphics]) + + @visible.setter + def visible(self, values: np.ndarray[bool] | list[bool]): + self._set_feature("visible", values) + + # TODO: how to work with deleted feature in a collection + + def _set_feature(self, feature, values): + if not len(values) == len(self): + raise IndexError + + for g, v in zip(self.graphics, values): + setattr(g, feature, v) def __init__( self, selection: np.ndarray[Graphic], + features: set[str] ): """ @@ -746,12 +787,75 @@ def __init__( """ self._selection = selection + self._features = features @property def graphics(self) -> np.ndarray[Graphic]: """Returns an array of the selected graphics. Always returns a proxy to the Graphic""" return tuple(self._selection) + def add_event_handler(self, *args): + """ + Register an event handler. + + Parameters + ---------- + callback: callable, the first argument + Event handler, must accept a single event argument + *types: list of strings + A list of event types, ex: "click", "data", "colors", "pointer_down" + + For the available renderer event types, see + https://jupyter-rfb.readthedocs.io/en/stable/events.html + + All feature support events, i.e. ``graphic.features`` will give a set of + all features that are evented + + Can also be used as a decorator. + + Example + ------- + + .. code-block:: py + + def my_handler(event): + print(event) + + graphic.add_event_handler(my_handler, "pointer_up", "pointer_down") + + Decorator usage example: + + .. code-block:: py + + @graphic.add_event_handler("click") + def my_handler(event): + print(event) + """ + + decorating = not callable(args[0]) + callback = None if decorating else args[0] + types = args if decorating else args[1:] + + if not all(t in set(PYGFX_EVENTS).union(self._features) for t in types): + raise KeyError( + f"event types must be strings for a valid event from the following:\n" + f"{PYGFX_EVENTS + list(self._features)}" + ) + + def decorator(_callback): + for g in self.graphics: + g.add_event_handler(_callback, types) + return _callback + + if decorating: + return decorator + + return decorator(callback) + + def remove_event_handler(self, callback, *types): + for g in self.graphics: + g.remove_event_handler(callback, *types) + def __getitem__(self, item): return self.graphics[item] @@ -839,6 +943,12 @@ def remove_graphic(self, graphic: Graphic): self._graphics_changed = True + def add_event_handler(self, *args): + raise NotImplementedError("Slice graphic collection to add event handlers") + + def remove_event_handler(self, callback, *types): + raise NotImplementedError("Slice graphic collection to remove event handlers") + def __getitem__(self, key) -> CollectionIndexer: return self._indexer( selection=self.graphics[key], diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 5941e7a9a..6ab416efc 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -8,25 +8,12 @@ from ..utils import parse_cmap_values from ._base import Interaction, PreviouslyModifiedData, GraphicCollection, CollectionIndexer, CollectionFeature -from ._features import GraphicFeature, VertexColors, VertexPositions +from ._features import GraphicFeature from .line import LineGraphic from .selectors import LinearRegionSelector, LinearSelector class LineSelection(CollectionIndexer): - """A sub-selection of a line-collection""" - @property - def name(self) -> np.ndarray[str | None]: - return np.asarray([g.name for g in self.graphics]) - - @name.setter - def name(self, values: np.ndarray[str] | list[str]): - if not len(values) == len(self): - raise IndexError - - for g, v in zip(self.graphics, values): - g.name = v - @property def colors(self) -> CollectionFeature: return CollectionFeature(self.graphics, "colors") @@ -48,6 +35,13 @@ def colors(self, values: str | np.ndarray | tuple[float] | list[float] | list[st return + if isinstance(values, np.ndarray): + if values.ndim == 2: + # assume individual colors for each + for g, v in zip(self.graphics, values): + g.colors = v + return + elif len(values) == 4: # assume RGBA self.colors[:] = values @@ -65,8 +59,28 @@ def data(self) -> CollectionFeature: def data(self, values): self.data[:] = values - def add_event_handler(self): - pass + @property + def cmap(self) -> CollectionFeature: + return CollectionFeature(self.graphics, "cmap") + + @cmap.setter + def cmap(self, name: str): + colors = parse_cmap_values( + n_colors=len(self), cmap_name=name + ) + self.colors = colors + + @property + def thickness(self) -> np.ndarray: + return np.asarray([g.thickness for g in self.graphics]) + + @thickness.setter + def thickness(self, values: np.ndarray | list[float]): + if not len(values) == len(self): + raise IndexError + + for g, v in zip(self.graphics, values): + g.thickness = v class LineCollection(GraphicCollection, Interaction): @@ -308,6 +322,7 @@ def add_linear_selector( LinearSelector """ + # TODO: Use bbox to get size and center for selectors! ( bounds, From f543e4aca320f15d55cfcf808ff724a458917914 Mon Sep 17 00:00:00 2001 From: Caitlin Date: Tue, 4 Jun 2024 09:31:37 -0400 Subject: [PATCH 076/196] remove old events system --- fastplotlib/graphics/_base.py | 163 ------------------------ fastplotlib/graphics/line.py | 4 +- fastplotlib/graphics/line_collection.py | 4 +- 3 files changed, 4 insertions(+), 167 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index f4ab6b7eb..aff4b90e5 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -519,129 +519,6 @@ def attach_feature(self, feature: VertexPositions | VertexColors | PointsSizesFe self._sizes._shared += 1 self.world_object.geometry.sizes = self._sizes.buffer - -class Interaction(ABC): - """Mixin class that makes graphics interactive""" - - @abstractmethod - def set_feature(self, feature: str, new_data: Any, indices: Any): - pass - - @abstractmethod - def reset_feature(self, feature: str): - pass - - def link( - self, - event_type: str, - target: Any, - feature: str, - new_data: Any, - callback: callable = None, - bidirectional: bool = False, - ): - """ - Link this graphic to another graphic upon an ``event_type`` to change the ``feature`` - of a ``target`` graphic. - - Parameters - ---------- - event_type: str - can be a pygfx event ("key_down", "key_up","pointer_down", "pointer_move", "pointer_up", - "pointer_enter", "pointer_leave", "click", "double_click", "wheel", "close", "resize") - or appropriate feature event (ex. colors, data, etc.) associated with the graphic (can use - ``graphic_instance.feature_events`` to get a tuple of the valid feature events for the - graphic) - - target: Any - graphic to be linked to - - feature: str - feature (ex. colors, data, etc.) of the target graphic that will change following - the event - - new_data: Any - appropriate data that will be changed in the feature of the target graphic after - the event occurs - - callback: callable, optional - user-specified callable that will handle event, - the callable must take the following four arguments - | ''source'' - this graphic instance - | ''target'' - the graphic to be changed following the event - | ''event'' - the ''pygfx event'' or ''feature event'' that occurs - | ''new_data'' - the appropriate data of the ''target'' that will be changed - - bidirectional: bool, default False - if True, the target graphic is also linked back to this graphic instance using the - same arguments - - For example: - .. code-block::python - - Returns - ------- - None - - """ - if event_type in PYGFX_EVENTS: - self.world_object.add_event_handler(self._event_handler, event_type) - - # make sure event is valid - elif event_type in self.feature_events: - if isinstance(self, GraphicCollection): - feature_instance = getattr(self[:], event_type) - else: - feature_instance = getattr(self, event_type) - - feature_instance.add_event_handler(self._event_handler) - - else: - raise ValueError( - f"Invalid event, valid events are: {PYGFX_EVENTS + self.feature_events}" - ) - - # make sure target feature is valid - if feature is not None: - if feature not in target.feature_events: - raise ValueError( - f"Invalid feature for target, valid features are: {target.feature_events}" - ) - - if event_type not in self.registered_callbacks.keys(): - self.registered_callbacks[event_type] = list() - - callback_data = CallbackData( - target=target, - feature=feature, - new_data=new_data, - callback_function=callback, - ) - - for existing_callback_data in self.registered_callbacks[event_type]: - if existing_callback_data == callback_data: - warn( - "linkage already exists for given event, target, and data, skipping" - ) - return - - self.registered_callbacks[event_type].append(callback_data) - - if bidirectional: - if event_type in PYGFX_EVENTS: - warn("cannot use bidirectional link for pygfx events") - return - - target.link( - event_type=event_type, - target=self, - feature=feature, - new_data=new_data, - callback=callback, - bidirectional=False, # else infinite recursion, otherwise target will call - # this instance .link(), and then it will happen again etc. - ) - def _event_handler(self, event): """Handles the event after it occurs when two graphic have been linked together.""" if event.type in self.registered_callbacks.keys(): @@ -684,46 +561,6 @@ def _event_handler(self, event): ) -@dataclass -class CallbackData: - """Class for keeping track of the info necessary for interactivity after event occurs.""" - - target: Any - feature: str - new_data: Any - callback_function: callable = None - - def __eq__(self, other): - if not isinstance(other, CallbackData): - raise TypeError("Can only compare against other types") - - if other.target is not self.target: - return False - - if not other.feature == self.feature: - return False - - if not other.new_data == self.new_data: - return False - - if (self.callback_function is None) and (other.callback_function is None): - return True - - if other.callback_function is self.callback_function: - return True - - else: - return False - - -@dataclass -class PreviouslyModifiedData: - """Class for keeping track of previously modified data at indices""" - - data: Any - indices: Any - - # Dict that holds all collection graphics in one python instance COLLECTION_GRAPHICS: dict[HexStr, Graphic] = dict() diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index b0df8f7ce..640a880f2 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -5,12 +5,12 @@ import pygfx -from ._base import PositionsGraphic, Interaction, PreviouslyModifiedData +from ._base import PositionsGraphic from .selectors import LinearRegionSelector, LinearSelector from ._features import Thickness -class LineGraphic(PositionsGraphic, Interaction): +class LineGraphic(PositionsGraphic): features = {"data", "colors", "cmap", "thickness"} @property diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 6ab416efc..9403bcd08 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -7,7 +7,7 @@ import pygfx from ..utils import parse_cmap_values -from ._base import Interaction, PreviouslyModifiedData, GraphicCollection, CollectionIndexer, CollectionFeature +from ._base import GraphicCollection, CollectionIndexer, CollectionFeature from ._features import GraphicFeature from .line import LineGraphic from .selectors import LinearRegionSelector, LinearSelector @@ -83,7 +83,7 @@ def thickness(self, values: np.ndarray | list[float]): g.thickness = v -class LineCollection(GraphicCollection, Interaction): +class LineCollection(GraphicCollection): child_type = LineGraphic _indexer = LineSelection From aa1e974fa7eb41090fec3cf88a078c618c4fe158 Mon Sep 17 00:00:00 2001 From: Caitlin Date: Tue, 4 Jun 2024 11:04:54 -0400 Subject: [PATCH 077/196] fix some examples --- examples/desktop/image/image_rgbvminvmax.py | 4 ++-- examples/desktop/image/image_vminvmax.py | 4 ++-- examples/desktop/image/image_widget.py | 2 +- examples/desktop/line/line_cmap.py | 2 +- examples/desktop/line/line_colorslice.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/desktop/image/image_rgbvminvmax.py b/examples/desktop/image/image_rgbvminvmax.py index 9725c038a..56114e1e3 100644 --- a/examples/desktop/image/image_rgbvminvmax.py +++ b/examples/desktop/image/image_rgbvminvmax.py @@ -23,8 +23,8 @@ fig[0, 0].auto_scale() -image_graphic.cmap.vmin = 0.5 -image_graphic.cmap.vmax = 0.75 +image_graphic.vmin = 0.5 +image_graphic.vmax = 0.75 if __name__ == "__main__": diff --git a/examples/desktop/image/image_vminvmax.py b/examples/desktop/image/image_vminvmax.py index 3c8607aef..d24d1f18c 100644 --- a/examples/desktop/image/image_vminvmax.py +++ b/examples/desktop/image/image_vminvmax.py @@ -23,8 +23,8 @@ fig[0, 0].auto_scale() -image_graphic.cmap.vmin = 0.5 -image_graphic.cmap.vmax = 0.75 +image_graphic.vmin = 0.5 +image_graphic.vmax = 0.75 if __name__ == "__main__": diff --git a/examples/desktop/image/image_widget.py b/examples/desktop/image/image_widget.py index 80aafe0b1..ddfc7c68d 100644 --- a/examples/desktop/image/image_widget.py +++ b/examples/desktop/image/image_widget.py @@ -10,7 +10,7 @@ a = iio.imread("imageio:camera.png") -iw = fpl.ImageWidget(data=a, cmap="viridis") +iw = fpl.ImageWidget(data=a, cmap="viridis", histogram_widget=False) iw.show() diff --git a/examples/desktop/line/line_cmap.py b/examples/desktop/line/line_cmap.py index 7d8e1e7d6..0bdc78aaf 100644 --- a/examples/desktop/line/line_cmap.py +++ b/examples/desktop/line/line_cmap.py @@ -35,7 +35,7 @@ data=cosine, thickness=10, cmap="tab10", - cmap_values=cmap_values + cmap_values=np.array(cmap_values) ) fig.show() diff --git a/examples/desktop/line/line_colorslice.py b/examples/desktop/line/line_colorslice.py index 4df666531..25a6329ae 100644 --- a/examples/desktop/line/line_colorslice.py +++ b/examples/desktop/line/line_colorslice.py @@ -53,8 +53,8 @@ key = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 67, 19]) sinc_graphic.colors[key] = "Red" -key2 = np.array([True, False, True, False, True, True, True, True]) -cosine_graphic.colors[key2] = "Green" +#key2 = np.array([True, False, True, False, True, True, True, True]) +#cosine_graphic.colors[key2] = "Green" fig.canvas.set_logical_size(800, 800) From aa324df1df246b41d402c6a292e14d01be579731 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 4 Jun 2024 13:08:19 -0400 Subject: [PATCH 078/196] remove lingering older interaction stuff --- fastplotlib/graphics/line.py | 32 -------------- fastplotlib/graphics/line_collection.py | 55 ------------------------- 2 files changed, 87 deletions(-) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 640a880f2..887d1dcc5 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -305,35 +305,3 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): bounds_init = (limits[0], int(np.ptp(limits) * 0.2) + offset) return bounds_init, limits, size, origin, axis, end_points - - def set_feature(self, feature: str, new_data: Any, indices: Any = None): - if not hasattr(self, "_previous_data"): - self._previous_data = dict() - elif hasattr(self, "_previous_data"): - self.reset_feature(feature) - - feature_instance = getattr(self, feature) - if indices is not None: - previous = feature_instance[indices].copy() - feature_instance[indices] = new_data - else: - previous = feature_instance._data.copy() - feature_instance._set(new_data) - if feature in self._previous_data.keys(): - self._previous_data[feature].data = previous - self._previous_data[feature].indices = indices - else: - self._previous_data[feature] = PreviouslyModifiedData( - data=previous, indices=indices - ) - - def reset_feature(self, feature: str): - if feature not in self._previous_data.keys(): - return - - prev_ixs = self._previous_data[feature].indices - feature_instance = getattr(self, feature) - if prev_ixs is not None: - feature_instance[prev_ixs] = self._previous_data[feature].data - else: - feature_instance._set(self._previous_data[feature].data) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 9403bcd08..d896fa789 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -470,61 +470,6 @@ def _get_linear_selector_init_args(self, padding, **kwargs): return bounds, limits, size, origin, axis, end_points - def _fpl_add_plot_area_hook(self, plot_area): - self._plot_area = plot_area - - def set_feature(self, feature: str, new_data: Any, indices: Any): - # if single value force to be an array of size 1 - if isinstance(indices, (np.integer, int)): - indices = np.array([indices]) - if not hasattr(self, "_previous_data"): - self._previous_data = dict() - elif hasattr(self, "_previous_data"): - if feature in self._previous_data.keys(): - # for now assume same index won't be changed with diff data - # I can't think of a usecase where we'd have to check the data too - # so unless there is a bug we keep it like this - if self._previous_data[feature].indices == indices: - return # nothing to change, and this allows bidirectional linking without infinite recursion - - self.reset_feature(feature) - - # coll_feature = getattr(self[indices], feature) - - data = list() - - for graphic in self.graphics[indices]: - feature_instance: GraphicFeature = getattr(graphic, feature) - data.append(feature_instance()) - - # later we can think about multi-index events - previous_data = deepcopy(data[0]) - - if feature in self._previous_data.keys(): - self._previous_data[feature].data = previous_data - self._previous_data[feature].indices = indices - else: - self._previous_data[feature] = PreviouslyModifiedData( - data=previous_data, indices=indices - ) - - # finally set the new data - # this MUST occur after setting the previous data attribute to prevent recursion - # since calling `feature._set()` triggers all the feature callbacks - feature_instance._set(new_data) - - def reset_feature(self, feature: str): - if feature not in self._previous_data.keys(): - return - - # implemented for a single index at moment - prev_ixs = self._previous_data[feature].indices - coll_feature = getattr(self[prev_ixs], feature) - - coll_feature.block_events(True) - coll_feature._set(self._previous_data[feature].data) - coll_feature.block_events(False) - axes = {"x": 0, "y": 1, "z": 2} From 00ba47f99943e2190d60d73638e7303bb0d9c864 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 4 Jun 2024 14:09:32 -0400 Subject: [PATCH 079/196] cleanup --- fastplotlib/graphics/line_collection.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index d896fa789..4e8373ca1 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -1,5 +1,4 @@ from typing import * -from copy import deepcopy import weakref import numpy as np @@ -8,7 +7,6 @@ from ..utils import parse_cmap_values from ._base import GraphicCollection, CollectionIndexer, CollectionFeature -from ._features import GraphicFeature from .line import LineGraphic from .selectors import LinearRegionSelector, LinearSelector From 233dab7daf20c93486d640e0787c6cc08d48bb91 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 4 Jun 2024 14:18:40 -0400 Subject: [PATCH 080/196] fill color arg --- fastplotlib/graphics/image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index ea43ee42f..fa2480d4d 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -352,6 +352,7 @@ def add_linear_region_selector( size=size, center=center, axis=axis, + fill_color=fill_color, parent=weakref.proxy(self), **kwargs, ) From 4da827c5efc355b12a5b4603fbe24b90305e0fc4 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 4 Jun 2024 14:19:38 -0400 Subject: [PATCH 081/196] black --- fastplotlib/graphics/_base.py | 88 +++++++++++-------- fastplotlib/graphics/_features/__init__.py | 20 ++++- fastplotlib/graphics/_features/_base.py | 34 ++++--- fastplotlib/graphics/_features/_common.py | 9 +- fastplotlib/graphics/_features/_image.py | 34 +++++-- .../graphics/_features/_positions_graphics.py | 68 ++++++++++---- .../graphics/_features/_selection_features.py | 4 +- fastplotlib/graphics/_features/_sizes.py | 2 - fastplotlib/graphics/image.py | 32 ++++--- fastplotlib/graphics/line.py | 27 +++--- fastplotlib/graphics/line_collection.py | 6 +- fastplotlib/graphics/scatter.py | 2 +- fastplotlib/graphics/selectors/_linear.py | 7 +- .../graphics/selectors/_linear_region.py | 70 ++++++++------- fastplotlib/layouts/_figure.py | 1 - fastplotlib/utils/gui.py | 1 - fastplotlib/widgets/image.py | 1 - 17 files changed, 258 insertions(+), 148 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index aff4b90e5..ad272d8d1 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -12,7 +12,20 @@ import pygfx -from ._features import GraphicFeature, BufferManager, Deleted, VertexPositions, VertexColors, VertexCmap, PointsSizesFeature, Name, Offset, Rotation, Visible, UniformColor +from ._features import ( + GraphicFeature, + BufferManager, + Deleted, + VertexPositions, + VertexColors, + VertexCmap, + PointsSizesFeature, + Name, + Offset, + Rotation, + Visible, + UniformColor, +) HexStr: TypeAlias = str @@ -95,14 +108,21 @@ def __init_subclass__(cls, **kwargs): ) # set of all features - cls.features = {*cls.features, "name", "offset", "rotation", "visible", "deleted"} + cls.features = { + *cls.features, + "name", + "offset", + "rotation", + "visible", + "deleted", + } super().__init_subclass__(**kwargs) def __init__( self, name: str = None, - offset: np.ndarray | list | tuple = (0., 0., 0.), - rotation: np.ndarray | list | tuple = (0., 0., 0., 1.), + offset: np.ndarray | list | tuple = (0.0, 0.0, 0.0), + rotation: np.ndarray | list | tuple = (0.0, 0.0, 0.0, 1.0), metadata: Any = None, ): """ @@ -220,7 +240,9 @@ def my_handler(event): types = args if decorating else args[1:] def decorator(_callback): - _callback_injector = partial(self._handle_event, _callback) # adds graphic instance as attribute + _callback_injector = partial( + self._handle_event, _callback + ) # adds graphic instance as attribute for t in types: # add to our record @@ -264,7 +286,9 @@ def remove_event_handler(self, callback, *types): self._event_handler_wrappers[t].remove(wrapper_map) break else: - raise KeyError(f"event type: {t} with callback: {callback} is not registered") + raise KeyError( + f"event type: {t} with callback: {callback} is not registered" + ) self._event_handlers[t].remove(callback) # remove callback wrapper from world object if pygfx event @@ -403,16 +427,16 @@ def cmap(self, name: str): self._cmap[:] = name def __init__( - self, - data: Any, - colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", - uniform_colors: bool = False, - alpha: float = 1.0, - cmap: str | VertexCmap = None, - cmap_values: np.ndarray = None, - isolated_buffer: bool = True, - *args, - **kwargs, + self, + data: Any, + colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", + uniform_colors: bool = False, + alpha: float = 1.0, + cmap: str | VertexCmap = None, + cmap_values: np.ndarray = None, + isolated_buffer: bool = True, + *args, + **kwargs, ): if isinstance(data, VertexPositions): self._data = data @@ -422,9 +446,7 @@ def __init__( if cmap is not None: # if a cmap is specified it overrides colors argument if uniform_colors: - raise TypeError( - "Cannot use cmap if uniform_colors=True" - ) + raise TypeError("Cannot use cmap if uniform_colors=True") if isinstance(cmap, str): # make colors from cmap @@ -437,9 +459,7 @@ def __init__( self._colors = VertexColors("w", n_colors=self._data.value.shape[0]) # make cmap using vertex colors buffer self._cmap = VertexCmap( - self._colors, - cmap_name=cmap, - cmap_values=cmap_values + self._colors, cmap_name=cmap, cmap_values=cmap_values ) elif isinstance(cmap, VertexCmap): # use existing cmap instance @@ -454,11 +474,7 @@ def __init__( self._colors = colors self._colors._shared += 1 # blank colormap instance - self._cmap = VertexCmap( - self._colors, - cmap_name=None, - cmap_values=None - ) + self._cmap = VertexCmap(self._colors, cmap_name=None, cmap_values=None) else: if uniform_colors: self._colors = UniformColor(colors) @@ -469,8 +485,10 @@ def __init__( n_colors=self._data.value.shape[0], alpha=alpha, ) - self._cmap = VertexCmap(self._colors, cmap_name=None, cmap_values=None) - + self._cmap = VertexCmap( + self._colors, cmap_name=None, cmap_values=None + ) + super().__init__(*args, **kwargs) def detach_feature(self, feature: str): @@ -496,7 +514,9 @@ def detach_feature(self, feature: str): self.world_object.geometry.positions = self._sizes.buffer self._sizes._shared -= 1 - def attach_feature(self, feature: VertexPositions | VertexColors | PointsSizesFeature): + def attach_feature( + self, feature: VertexPositions | VertexColors | PointsSizesFeature + ): if isinstance(feature, VertexPositions): # TODO: check if this causes a memory leak self._data._shared -= 1 @@ -567,6 +587,7 @@ def _event_handler(self, event): class CollectionIndexer: """Collection Indexer""" + @property def name(self) -> np.ndarray[str | None]: return np.asarray([g.name for g in self.graphics]) @@ -608,11 +629,7 @@ def _set_feature(self, feature, values): for g, v in zip(self.graphics, values): setattr(g, feature, v) - def __init__( - self, - selection: np.ndarray[Graphic], - features: set[str] - ): + def __init__(self, selection: np.ndarray[Graphic], features: set[str]): """ Parameters @@ -708,6 +725,7 @@ def __repr__(self): class GraphicCollection(Graphic): """Graphic Collection base class""" + child_type: type _indexer: type diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index b2b07fa04..40d9e181f 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -1,5 +1,21 @@ -from ._positions_graphics import VertexColors, UniformColor, UniformSizes, Thickness, VertexPositions, PointsSizesFeature, VertexCmap -from ._image import TextureArray, ImageCmap, ImageVmin, ImageVmax, ImageInterpolation, ImageCmapInterpolation, WGPU_MAX_TEXTURE_SIZE +from ._positions_graphics import ( + VertexColors, + UniformColor, + UniformSizes, + Thickness, + VertexPositions, + PointsSizesFeature, + VertexCmap, +) +from ._image import ( + TextureArray, + ImageCmap, + ImageVmin, + ImageVmax, + ImageInterpolation, + ImageCmapInterpolation, + WGPU_MAX_TEXTURE_SIZE, +) from ._base import ( GraphicFeature, BufferManager, diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index ebf7dbf15..0429b05e1 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -141,7 +141,9 @@ def _call_event_handlers(self, event_data: FeatureEvent): return for func in self._event_handlers: - with log_exception(f"Error during handling {self.__class__.__name__} event"): + with log_exception( + f"Error during handling {self.__class__.__name__} event" + ): func(event_data) @@ -149,12 +151,12 @@ class BufferManager(GraphicFeature): """Smaller wrapper for pygfx.Buffer""" def __init__( - self, - data: NDArray | pygfx.Buffer, - buffer_type: Literal["buffer", "texture", "texture-array"] = "buffer", - isolated_buffer: bool = True, - texture_dim: int = 2, - **kwargs + self, + data: NDArray | pygfx.Buffer, + buffer_type: Literal["buffer", "texture", "texture-array"] = "buffer", + isolated_buffer: bool = True, + texture_dim: int = 2, + **kwargs, ): super().__init__() if isolated_buffer and not isinstance(data, pygfx.Resource): @@ -205,7 +207,11 @@ def __getitem__(self, item): def __setitem__(self, key, value): raise NotImplementedError - def _parse_offset_size(self, key: int | slice | np.ndarray[int | bool] | list[bool | int], upper_bound: int): + def _parse_offset_size( + self, + key: int | slice | np.ndarray[int | bool] | list[bool | int], + upper_bound: int, + ): """ parse offset and size for one dimension """ @@ -272,7 +278,14 @@ def _parse_offset_size(self, key: int | slice | np.ndarray[int | bool] | list[bo return offset, size - def _update_range(self, key: int | slice | np.ndarray[int | bool] | list[bool | int] | tuple[slice, ...]): + def _update_range( + self, + key: int + | slice + | np.ndarray[int | bool] + | list[bool | int] + | tuple[slice, ...], + ): """ Uses key from slicing to determine the offset and size of the buffer to mark for upload to the GPU @@ -303,5 +316,4 @@ def _emit_event(self, type: str, key, value): self._call_event_handlers(event) def __repr__(self): - return f"{self.__class__.__name__} buffer data:\n" \ - f"{self.value.__repr__()}" + return f"{self.__class__.__name__} buffer data:\n" f"{self.value.__repr__()}" diff --git a/fastplotlib/graphics/_features/_common.py b/fastplotlib/graphics/_features/_common.py index bd2604386..fe32a485f 100644 --- a/fastplotlib/graphics/_features/_common.py +++ b/fastplotlib/graphics/_features/_common.py @@ -5,6 +5,7 @@ class Name(GraphicFeature): """Graphic name""" + def __init__(self, value: str): self._value = value super().__init__() @@ -28,6 +29,7 @@ def set_value(self, graphic, value: str): class Offset(GraphicFeature): """Offset position of the graphic, [x, y, z]""" + def __init__(self, value: np.ndarray | list | tuple): self._validate(value) self._value = np.array(value) @@ -55,6 +57,7 @@ def set_value(self, graphic, value: np.ndarray | list | tuple): class Rotation(GraphicFeature): """Graphic rotation quaternion""" + def __init__(self, value: np.ndarray | list | tuple): self._validate(value) self._value = np.array(value) @@ -63,7 +66,9 @@ def __init__(self, value: np.ndarray | list | tuple): def _validate(self, value): if not len(value) == 4: - raise ValueError("rotation quaternion must be a list, tuple, or array of 4 float values") + raise ValueError( + "rotation quaternion must be a list, tuple, or array of 4 float values" + ) @property def value(self) -> np.ndarray: @@ -82,6 +87,7 @@ def set_value(self, graphic, value: np.ndarray | list | tuple): class Visible(GraphicFeature): """Access or change the visibility.""" + def __init__(self, value: bool): self._value = value super().__init__() @@ -102,6 +108,7 @@ class Deleted(GraphicFeature): """ Used when a graphic is deleted, triggers events that can be useful to indicate this graphic has been deleted """ + def __init__(self, value: bool): self._value = value super().__init__() diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py index 1c71b8d4a..a70f551e0 100644 --- a/fastplotlib/graphics/_features/_image.py +++ b/fastplotlib/graphics/_features/_image.py @@ -11,9 +11,9 @@ get_cmap_texture, ) + # manages an array of 8192x8192 Textures representing chunks of an image class TextureArray(GraphicFeature): - def __init__(self, data, isolated_buffer: bool = True): super().__init__() @@ -28,25 +28,35 @@ def __init__(self, data, isolated_buffer: bool = True): self._value = data # indices for each Texture - self._row_indices = np.arange(0, ceil(self.value.shape[0] / WGPU_MAX_TEXTURE_SIZE) * WGPU_MAX_TEXTURE_SIZE, WGPU_MAX_TEXTURE_SIZE) - self._col_indices = np.arange(0, ceil(self.value.shape[1] / WGPU_MAX_TEXTURE_SIZE) * WGPU_MAX_TEXTURE_SIZE, WGPU_MAX_TEXTURE_SIZE) + self._row_indices = np.arange( + 0, + ceil(self.value.shape[0] / WGPU_MAX_TEXTURE_SIZE) * WGPU_MAX_TEXTURE_SIZE, + WGPU_MAX_TEXTURE_SIZE, + ) + self._col_indices = np.arange( + 0, + ceil(self.value.shape[1] / WGPU_MAX_TEXTURE_SIZE) * WGPU_MAX_TEXTURE_SIZE, + WGPU_MAX_TEXTURE_SIZE, + ) # buffer will be an array of textures - self._buffer: np.ndarray[pygfx.Texture] = np.empty(shape=(self.row_indices.size, self.col_indices.size), dtype=object) + self._buffer: np.ndarray[pygfx.Texture] = np.empty( + shape=(self.row_indices.size, self.col_indices.size), dtype=object + ) # max index row_max = self.value.shape[0] - 1 col_max = self.value.shape[1] - 1 - for (buffer_row, row_ix), (buffer_col, col_ix) in zip(enumerate(self.row_indices), enumerate(self.col_indices)): + for (buffer_row, row_ix), (buffer_col, col_ix) in zip( + enumerate(self.row_indices), enumerate(self.col_indices) + ): # stop index for this chunk row_stop = min(row_max, row_ix + WGPU_MAX_TEXTURE_SIZE) col_stop = min(col_max, col_ix + WGPU_MAX_TEXTURE_SIZE) # make texture from slice - texture = pygfx.Texture( - self.value[row_ix:row_stop, col_ix:col_stop], dim=2 - ) + texture = pygfx.Texture(self.value[row_ix:row_stop, col_ix:col_stop], dim=2) self.buffer[buffer_row, buffer_col] = texture @@ -100,6 +110,7 @@ def __setitem__(self, key, value): class ImageVmin(GraphicFeature): """lower contrast limit""" + def __init__(self, value: float): self._value = value super().__init__() @@ -119,6 +130,7 @@ def set_value(self, graphic, value: float): class ImageVmax(GraphicFeature): """upper contrast limit""" + def __init__(self, value: float): self._value = value super().__init__() @@ -138,6 +150,7 @@ def set_value(self, graphic, value: float): class ImageCmap(GraphicFeature): """colormap for texture""" + def __init__(self, value: str): self._value = value self.texture = get_cmap_texture(value) @@ -159,6 +172,7 @@ def set_value(self, graphic, value: str): class ImageInterpolation(GraphicFeature): """Image interpolation method""" + def __init__(self, value: str): self._validate(value) self._value = value @@ -192,7 +206,9 @@ def __init__(self, value: str): def _validate(self, value): if value not in ["nearest", "linear"]: - raise ValueError("`cmap_interpolation` must be one of 'nearest' or 'linear'") + raise ValueError( + "`cmap_interpolation` must be one of 'nearest' or 'linear'" + ) @property def value(self) -> str: diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index c6a96b709..79f750b5d 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -55,13 +55,13 @@ def __init__( """ data = parse_colors(colors, n_colors, alpha) - + super().__init__(data=data, isolated_buffer=isolated_buffer) def __setitem__( - self, - key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], - user_value: str | np.ndarray | tuple[float] | list[float] | list[str] + self, + key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], + user_value: str | np.ndarray | tuple[float] | list[float] | list[str], ): if isinstance(key, tuple): # directly setting RGBA values for points, we do no parsing @@ -91,7 +91,9 @@ def __setitem__( # make sure it's 1D if not key.ndim == 1: - raise TypeError("If slicing colors with an array, it must be a 1D bool or int array") + raise TypeError( + "If slicing colors with an array, it must be a 1D bool or int array" + ) if key.dtype == bool: # make sure len is same @@ -103,7 +105,9 @@ def __setitem__( n_colors = key.size else: - raise TypeError("If slicing colors with an array, it must be a 1D bool or int array") + raise TypeError( + "If slicing colors with an array, it must be a 1D bool or int array" + ) value = parse_colors(user_value, n_colors) @@ -130,7 +134,7 @@ class UniformColor(GraphicFeature): def __init__(self, value: str | np.ndarray | tuple | list | pygfx.Color): self._value = pygfx.Color(value) super().__init__() - + @property def value(self) -> pygfx.Color: return self._value @@ -202,7 +206,11 @@ def _fix_data(self, data): return to_gpu_supported_dtype(data) - def __setitem__(self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], value: np.ndarray | float | list[float]): + def __setitem__( + self, + key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], + value: np.ndarray | float | list[float], + ): # directly use the key to slice the buffer self.buffer.data[key] = value @@ -225,10 +233,10 @@ class PointsSizesFeature(BufferManager): """ def __init__( - self, - sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], - n_datapoints: int, - isolated_buffer: bool = True + self, + sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], + n_datapoints: int, + isolated_buffer: bool = True, ): """ Manages sizes buffer of scatter points. @@ -236,7 +244,11 @@ def __init__( sizes = self._fix_sizes(sizes, n_datapoints) super().__init__(data=sizes, isolated_buffer=isolated_buffer) - def _fix_sizes(self, sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], n_datapoints: int): + def _fix_sizes( + self, + sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], + n_datapoints: int, + ): if np.issubdtype(type(sizes), np.number): # single value given sizes = np.full( @@ -254,8 +266,10 @@ def _fix_sizes(self, sizes: int | float | np.ndarray | list[int | float] | tuple ) else: - raise TypeError("sizes must be a single , , or a sequence (array, list, tuple) of int" - "or float with the length equal to the number of datapoints") + raise TypeError( + "sizes must be a single , , or a sequence (array, list, tuple) of int" + "or float with the length equal to the number of datapoints" + ) if np.count_nonzero(sizes < 0) > 1: raise ValueError( @@ -264,7 +278,11 @@ def _fix_sizes(self, sizes: int | float | np.ndarray | list[int | float] | tuple return sizes - def __setitem__(self, key: int | slice | np.ndarray[int | bool] | list[int | bool], value: int | float | np.ndarray | list[int | float] | tuple[int | float]): + def __setitem__( + self, + key: int | slice | np.ndarray[int | bool] | list[int | bool], + value: int | float | np.ndarray | list[int | float] | tuple[int | float], + ): # this is a very simple 1D buffer, no parsing required, directly set buffer self.buffer.data[key] = value self._update_range(key) @@ -274,6 +292,7 @@ def __setitem__(self, key: int | slice | np.ndarray[int | bool] | list[int | boo class Thickness(GraphicFeature): """line thickness""" + def __init__(self, value: float): self._value = value super().__init__() @@ -295,7 +314,12 @@ class VertexCmap(BufferManager): Sliceable colormap feature, manages a VertexColors instance and just provides a way to set colormaps. """ - def __init__(self, vertex_colors: VertexColors, cmap_name: str | None, cmap_values: np.ndarray | None): + def __init__( + self, + vertex_colors: VertexColors, + cmap_name: str | None, + cmap_values: np.ndarray | None, + ): super().__init__(data=vertex_colors.buffer) self._vertex_colors = vertex_colors @@ -312,7 +336,9 @@ def __init__(self, vertex_colors: VertexColors, cmap_name: str | None, cmap_valu n_datapoints = vertex_colors.value.shape[0] colors = parse_cmap_values( - n_colors=n_datapoints, cmap_name=self._cmap_name, cmap_values=self._cmap_values + n_colors=n_datapoints, + cmap_name=self._cmap_name, + cmap_values=self._cmap_values, ) # set vertex colors from cmap self._vertex_colors[:] = colors @@ -354,7 +380,11 @@ def values(self) -> np.ndarray: return self._cmap_values @values.setter - def values(self, values: np.ndarray | list[float | int], indices: slice | list | np.ndarray = None): + def values( + self, + values: np.ndarray | list[float | int], + indices: slice | list | np.ndarray = None, + ): if self._cmap_name is None: raise AttributeError( "cmap is not set, set the cmap before setting the cmap_values" diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index 0bf0d1d55..71ba53425 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -98,9 +98,7 @@ class LinearRegionSelectionFeature(GraphicFeature): """ - def __init__( - self, value: tuple[int, int], axis: str, limits: tuple[float, float] - ): + def __init__(self, value: tuple[int, int], axis: str, limits: tuple[float, float]): super().__init__() self._axis = axis diff --git a/fastplotlib/graphics/_features/_sizes.py b/fastplotlib/graphics/_features/_sizes.py index b28b04f64..8b1378917 100644 --- a/fastplotlib/graphics/_features/_sizes.py +++ b/fastplotlib/graphics/_features/_sizes.py @@ -1,3 +1 @@ - - diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index fa2480d4d..6c93c4456 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -15,7 +15,7 @@ ImageVmax, ImageInterpolation, ImageCmapInterpolation, - WGPU_MAX_TEXTURE_SIZE + WGPU_MAX_TEXTURE_SIZE, ) @@ -24,7 +24,10 @@ class _ImageTile(pygfx.Image): Similar to pygfx.Image, only difference is that it contains a few properties to keep track of row chunk index, column chunk index """ - def __init__(self, geometry, material, row_chunk_ix: int, col_chunk_ix: int, **kwargs): + + def __init__( + self, geometry, material, row_chunk_ix: int, col_chunk_ix: int, **kwargs + ): super().__init__(geometry, material, **kwargs) self._row_chunk_index = row_chunk_ix @@ -196,7 +199,9 @@ def __init__( self._material = pygfx.ImageBasicMaterial( clim=(vmin, vmax), - map=self._cmap.texture if self._data.value.ndim == 2 else None, # RGB vs. grayscale + map=self._cmap.texture + if self._data.value.ndim == 2 + else None, # RGB vs. grayscale interpolation=self._interpolation.value, map_interpolation=self._cmap_interpolation.value, pick_write=True, @@ -208,7 +213,7 @@ def __init__( geometry=pygfx.Geometry(grid=self._data.buffer[row_ix, col_ix]), material=self._material, row_chunk_ix=row_ix, - col_chunk_ix=col_ix + col_chunk_ix=col_ix, ) img.world.x = self._data.row_indices[row_ix] @@ -255,9 +260,7 @@ def add_linear_selector( center = size / 2 limits = (0, self._data.value.shape[0]) else: - raise ValueError( - "`axis` must be one of 'x' | 'y'" - ) + raise ValueError("`axis` must be one of 'x' | 'y'") # default padding is 25% the height or width of the image if padding is None: @@ -286,12 +289,17 @@ def add_linear_selector( self._plot_area.add_graphic(selector, center=False) # place selector above this graphic - selector.offset = selector.offset + (0., 0., self.offset[-1] + 1) + selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) return weakref.proxy(selector) def add_linear_region_selector( - self, selection: tuple[float, float] = None, axis: str = "x", padding: float = 0., fill_color = (0, 0, 0.35, 0.2), **kwargs, + self, + selection: tuple[float, float] = None, + axis: str = "x", + padding: float = 0.0, + fill_color=(0, 0, 0.35, 0.2), + **kwargs, ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, @@ -327,9 +335,7 @@ def add_linear_region_selector( center = size / 2 limits = (0, self._data.value.shape[0]) else: - raise ValueError( - "`axis` must be one of 'x' | 'y'" - ) + raise ValueError("`axis` must be one of 'x' | 'y'") # default padding is 25% the height or width of the image if padding is None: @@ -360,6 +366,6 @@ def add_linear_region_selector( self._plot_area.add_graphic(selector, center=False) # place above this graphic - selector.offset = selector.offset + (0., 0., self.offset[-1] + 1) + selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) return weakref.proxy(selector) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 887d1dcc5..421a9b32a 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -94,7 +94,7 @@ def __init__( cmap=cmap, cmap_values=cmap_values, isolated_buffer=isolated_buffer, - **kwargs + **kwargs, ) self._thickness = Thickness(thickness) @@ -106,20 +106,23 @@ def __init__( if uniform_colors: geometry = pygfx.Geometry(positions=self._data.buffer) - material = MaterialCls(thickness=self.thickness, color_mode="uniform", pick_write=True) + material = MaterialCls( + thickness=self.thickness, color_mode="uniform", 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) + 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 - ) + world_object: pygfx.Line = pygfx.Line(geometry=geometry, material=material) self._set_world_object(world_object) def add_linear_selector( - self, selection: float = None, padding: float = 0., axis: str = "x",**kwargs + self, selection: float = None, padding: float = 0.0, axis: str = "x", **kwargs ) -> LinearSelector: """ Adds a linear selector. @@ -181,12 +184,12 @@ def add_linear_selector( self._plot_area.add_graphic(selector, center=False) # place selector above this graphic - selector.offset = selector.offset + (0., 0., self.offset[-1] + 1) + selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) return weakref.proxy(selector) def add_linear_region_selector( - self, padding: float = 0., axis: str = "x", **kwargs + self, padding: float = 0.0, axis: str = "x", **kwargs ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, @@ -246,7 +249,7 @@ def add_linear_region_selector( self._plot_area.add_graphic(selector, center=False) # place selector below this graphic - selector.offset = selector.offset + (0., 0., self.offset[-1] - 1) + 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 diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 4e8373ca1..e286462e8 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -63,9 +63,7 @@ def cmap(self) -> CollectionFeature: @cmap.setter def cmap(self, name: str): - colors = parse_cmap_values( - n_colors=len(self), cmap_name=name - ) + colors = parse_cmap_values(n_colors=len(self), cmap_name=name) self.colors = colors @property @@ -263,7 +261,7 @@ def __init__( cmap=_cmap, metadata=_m, isolated_buffer=isolated_buffer, - **kwargs + **kwargs, ) self.add_graphic(lg) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index a935b8092..bc46c4923 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -97,7 +97,7 @@ def __init__( cmap=cmap, cmap_values=cmap_values, isolated_buffer=isolated_buffer, - **kwargs + **kwargs, ) n_datapoints = self.data.value.shape[0] diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index b1082f5aa..d9f516c17 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -172,7 +172,7 @@ def __init__( axis=axis, parent=parent, name=name, - offset=offset + offset=offset, ) self._set_world_object(world_object) @@ -349,7 +349,10 @@ def _get_selected_index(self, graphic): elif self.axis == "y": data = graphic.data[:, 1] - if "Line" in graphic.__class__.__name__ or "Scatter" in graphic.__class__.__name__: + if ( + "Line" in graphic.__class__.__name__ + or "Scatter" in graphic.__class__.__name__ + ): # we want to find the index of the data closest to the slider position find_value = self.selection diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index c6c40fa88..2c51386cb 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -61,19 +61,19 @@ def limits(self, values: Tuple[float, float]): self.selection._limits = self._limits def __init__( - self, - selection: Sequence[float], - limits: Sequence[float], - size: int, - center: float, - axis: str = "x", - parent: Graphic = None, - resizable: bool = True, - fill_color=(0, 0, 0.35), - edge_color=(0.8, 0.6, 0), - edge_thickness: float = 8, - arrow_keys_modifier: str = "Shift", - name: str = None, + self, + selection: Sequence[float], + limits: Sequence[float], + size: int, + center: float, + axis: str = "x", + parent: Graphic = None, + resizable: bool = True, + fill_color=(0, 0, 0.35), + edge_color=(0.8, 0.6, 0), + edge_thickness: float = 8, + arrow_keys_modifier: str = "Shift", + name: str = None, ): """ Create a LinearRegionSelector graphic which can be moved only along either the x-axis or y-axis. @@ -167,12 +167,9 @@ def __init__( if axis == "x": # just some data to initialize the edge lines - init_line_data = np.array( - [ - [0, -size / 2, 0], - [0, size / 2, 0] - ] - ).astype(np.float32) + init_line_data = np.array([[0, -size / 2, 0], [0, size / 2, 0]]).astype( + np.float32 + ) elif axis == "y": # just some line data to initialize y axis edge lines @@ -187,13 +184,17 @@ def __init__( raise ValueError("axis argument must be one of 'x' or 'y'") line0 = pygfx.Line( - pygfx.Geometry(positions=init_line_data.copy()), # copy so the line buffer is isolated + pygfx.Geometry( + positions=init_line_data.copy() + ), # copy so the line buffer is isolated pygfx.LineMaterial( thickness=edge_thickness, color=edge_color, pick_write=True ), ) line1 = pygfx.Line( - pygfx.Geometry(positions=init_line_data.copy()), # copy so the line buffer is isolated + pygfx.Geometry( + positions=init_line_data.copy() + ), # copy so the line buffer is isolated pygfx.LineMaterial( thickness=edge_thickness, color=edge_color, pick_write=True ), @@ -216,9 +217,7 @@ def __init__( # compensate for any offset from the parent graphic # selection feature only works in world space, not data space self._selection = LinearRegionSelectionFeature( - selection, - axis=axis, - limits=self._limits + selection, axis=axis, limits=self._limits ) self._handled_widgets = list() @@ -242,7 +241,7 @@ def __init__( self.selection = selection def get_selected_data( - self, graphic: Graphic = None + self, graphic: Graphic = None ) -> Union[np.ndarray, List[np.ndarray]]: """ Get the ``Graphic`` data bounded by the current selection. @@ -280,9 +279,13 @@ def get_selected_data( for i, g in enumerate(source.graphics): if ixs[i].size == 0: - data_selections.append(np.array([], dtype=np.float32).reshape(0, 3)) + data_selections.append( + np.array([], dtype=np.float32).reshape(0, 3) + ) else: - s = slice(ixs[i][0], ixs[i][-1] + 1) # add 1 because these are direct indices + s = slice( + ixs[i][0], ixs[i][-1] + 1 + ) # add 1 because these are direct indices # slices n_datapoints dim data_selections.append(g.data[s]) @@ -292,7 +295,9 @@ def get_selected_data( # 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 + 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 return source.data[s] @@ -309,7 +314,7 @@ def get_selected_data( return source.data[s] def get_selected_indices( - self, graphic: Graphic = None + self, graphic: Graphic = None ) -> Union[np.ndarray, List[np.ndarray]]: """ Returns the indices of the ``Graphic`` data bounded by the current selection. @@ -339,7 +344,10 @@ def get_selected_indices( # selector (min, max) data values along axis bounds = self.selection - if "Line" in source.__class__.__name__ or "Scatter" in source.__class__.__name__: + if ( + "Line" in source.__class__.__name__ + or "Scatter" in source.__class__.__name__ + ): # gets indices corresponding to n_datapoints dim # data is [n_datapoints, xyz], so we return # indices that can be used to slice `n_datapoints` @@ -420,7 +428,7 @@ def add_ipywidget_handler(self, widget, step: Union[int, float] = None): """ if not isinstance( - widget, (ipywidgets.IntRangeSlider, ipywidgets.FloatRangeSlider) + widget, (ipywidgets.IntRangeSlider, ipywidgets.FloatRangeSlider) ): raise TypeError( f"`widget` must be one of: ipywidgets.IntRangeSlider or ipywidgets.FloatRangeSlider\n" diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 1c7439613..2c157db8f 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -128,7 +128,6 @@ def __init__( # if controller instances have been specified for each subplot if controllers is not None: - # one controller for all subplots if isinstance(controllers, pygfx.Controller): controllers = [controllers] * len(self) diff --git a/fastplotlib/utils/gui.py b/fastplotlib/utils/gui.py index b59c7799b..1941674ee 100644 --- a/fastplotlib/utils/gui.py +++ b/fastplotlib/utils/gui.py @@ -44,7 +44,6 @@ def _notebook_print_banner(): - from ipywidgets import Image from IPython.display import display diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 2a4dc31b4..c7de0a126 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -366,7 +366,6 @@ def __init__( if isinstance(data, list): # verify that it's a list of np.ndarray if all([_is_arraylike(d) for d in data]): - # Grid computations if figure_shape is None: figure_shape = calculate_figure_shape(len(data)) From d785b9205193eaad49afb438cdae6ad1ec1d9c6e Mon Sep 17 00:00:00 2001 From: Caitlin Date: Tue, 4 Jun 2024 14:34:22 -0400 Subject: [PATCH 082/196] update examples --- examples/desktop/line/line_cmap.py | 2 +- examples/desktop/line/line_dataslice.py | 2 +- fastplotlib/graphics/_features/_positions_graphics.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/desktop/line/line_cmap.py b/examples/desktop/line/line_cmap.py index 0bdc78aaf..7d8e1e7d6 100644 --- a/examples/desktop/line/line_cmap.py +++ b/examples/desktop/line/line_cmap.py @@ -35,7 +35,7 @@ data=cosine, thickness=10, cmap="tab10", - cmap_values=np.array(cmap_values) + cmap_values=cmap_values ) fig.show() diff --git a/examples/desktop/line/line_dataslice.py b/examples/desktop/line/line_dataslice.py index 12a5f0f04..d8a107559 100644 --- a/examples/desktop/line/line_dataslice.py +++ b/examples/desktop/line/line_dataslice.py @@ -42,7 +42,7 @@ cosine_graphic.data[0] = np.array([[-10, 0, 0]]) # additional fancy indexing using numpy -key2 = np.array([True, False, True, False, True, True, True, True]) +key2 = [True, False] * 50 sinc_graphic.data[key2] = np.array([[5, 1, 2]]) fig.canvas.set_logical_size(800, 800) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index 79f750b5d..e555a3111 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, List import numpy as np import pygfx @@ -330,6 +330,8 @@ def __init__( if not isinstance(self._cmap_name, str): raise TypeError if self._cmap_values is not None: + if isinstance(self._cmap_values, List): + self._cmap_values = np.asarray(self._cmap_values) if not isinstance(self._cmap_values, np.ndarray): raise TypeError From 3a424d2b434ca9978d70e4175d7fa46effb4fe32 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 4 Jun 2024 15:06:09 -0400 Subject: [PATCH 083/196] fix image tiling, better heatmap example --- examples/desktop/heatmap/heatmap.py | 10 +++------- fastplotlib/graphics/_features/_image.py | 20 ++++++++++---------- fastplotlib/graphics/image.py | 4 ++-- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/examples/desktop/heatmap/heatmap.py b/examples/desktop/heatmap/heatmap.py index fa5ec6715..c43f0ba84 100644 --- a/examples/desktop/heatmap/heatmap.py +++ b/examples/desktop/heatmap/heatmap.py @@ -14,16 +14,12 @@ xs = np.linspace(0, 1_000, 10_000) -sine = np.sin(xs) -cosine = np.cos(xs) +sine = np.sin(np.sqrt(xs)) -# alternating sines and cosines -data = np.zeros((10_000, 10_000), dtype=np.float32) -data[::2] = sine -data[1::2] = cosine +data = np.vstack([sine * i for i in range(20_000)]) # plot the image data -heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") +img = fig[0, 0].add_image(data=data, name="heatmap") fig.show() diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py index a70f551e0..4b48e028f 100644 --- a/fastplotlib/graphics/_features/_image.py +++ b/fastplotlib/graphics/_features/_image.py @@ -1,3 +1,4 @@ +from itertools import product from math import ceil import numpy as np @@ -48,17 +49,16 @@ def __init__(self, data, isolated_buffer: bool = True): row_max = self.value.shape[0] - 1 col_max = self.value.shape[1] - 1 - for (buffer_row, row_ix), (buffer_col, col_ix) in zip( - enumerate(self.row_indices), enumerate(self.col_indices) - ): - # stop index for this chunk - row_stop = min(row_max, row_ix + WGPU_MAX_TEXTURE_SIZE) - col_stop = min(col_max, col_ix + WGPU_MAX_TEXTURE_SIZE) + for buffer_row, row_ix in enumerate(self.row_indices): + for buffer_col, col_ix in enumerate(self.col_indices): + # stop index for this chunk + row_stop = min(row_max, row_ix + WGPU_MAX_TEXTURE_SIZE) + col_stop = min(col_max, col_ix + WGPU_MAX_TEXTURE_SIZE) - # make texture from slice - texture = pygfx.Texture(self.value[row_ix:row_stop, col_ix:col_stop], dim=2) + # make texture from slice + texture = pygfx.Texture(self.value[row_ix:row_stop, col_ix:col_stop], dim=2) - self.buffer[buffer_row, buffer_col] = texture + self.buffer[buffer_row, buffer_col] = texture self._shared: int = 0 @@ -79,7 +79,7 @@ def row_indices(self) -> np.ndarray: @property def col_indices(self) -> np.ndarray: - return self._row_indices + return self._col_indices @property def shared(self) -> int: diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 6c93c4456..e379f56f9 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -216,8 +216,8 @@ def __init__( col_chunk_ix=col_ix, ) - img.world.x = self._data.row_indices[row_ix] - img.world.y = self._data.row_indices[col_ix] + img.world.y = row_ix * WGPU_MAX_TEXTURE_SIZE + img.world.x = col_ix * WGPU_MAX_TEXTURE_SIZE world_object.add(img) From 87c8d9db5d40d81b5fecddbda68472a9657f8064 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 4 Jun 2024 15:17:09 -0400 Subject: [PATCH 084/196] update hm examples --- examples/desktop/heatmap/heatmap_cmap.py | 12 ++++-------- examples/desktop/heatmap/heatmap_data.py | 16 ++++++---------- examples/desktop/heatmap/heatmap_vmin_vmax.py | 14 +++++--------- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/examples/desktop/heatmap/heatmap_cmap.py b/examples/desktop/heatmap/heatmap_cmap.py index a1434bb0e..a06f587d6 100644 --- a/examples/desktop/heatmap/heatmap_cmap.py +++ b/examples/desktop/heatmap/heatmap_cmap.py @@ -14,16 +14,12 @@ xs = np.linspace(0, 1_000, 10_000) -sine = np.sin(xs) -cosine = np.cos(xs) +sine = np.sin(np.sqrt(xs)) -# alternating sines and cosines -data = np.zeros((10_000, 10_000), dtype=np.float32) -data[::2] = sine -data[1::2] = cosine +data = np.vstack([sine * i for i in range(20_000)]) # plot the image data -heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") +img = fig[0, 0].add_image(data=data, name="heatmap") fig.show() @@ -31,7 +27,7 @@ fig[0, 0].auto_scale() -heatmap_graphic.cmap = "viridis" +img.cmap = "viridis" if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/heatmap/heatmap_data.py b/examples/desktop/heatmap/heatmap_data.py index 67aee1668..9e914be9b 100644 --- a/examples/desktop/heatmap/heatmap_data.py +++ b/examples/desktop/heatmap/heatmap_data.py @@ -14,26 +14,22 @@ xs = np.linspace(0, 1_000, 10_000) -sine = np.sin(xs) -cosine = np.cos(xs) +sine = np.sin(np.sqrt(xs)) -# alternating sines and cosines -data = np.zeros((10_000, 10_000), dtype=np.float32) -data[::2] = sine -data[1::2] = cosine +data = np.vstack([sine * i for i in range(20_000)]) # plot the image data -heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") +img = fig[0, 0].add_image(data=data, name="heatmap") fig.show() fig.canvas.set_logical_size(1500, 1500) fig[0, 0].auto_scale() +cosine = np.cos(np.sqrt(xs)) -heatmap_graphic.data[:5_000] = sine -heatmap_graphic.data[5_000:] = cosine - +# change first 10,000 rows and 9,000 columns +img.data[:10_000, :9000] = np.vstack([cosine[:9000] * i * 2 for i in range(10_000)]) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/heatmap/heatmap_vmin_vmax.py b/examples/desktop/heatmap/heatmap_vmin_vmax.py index 6fe8a08b8..58dfc5542 100644 --- a/examples/desktop/heatmap/heatmap_vmin_vmax.py +++ b/examples/desktop/heatmap/heatmap_vmin_vmax.py @@ -14,16 +14,12 @@ xs = np.linspace(0, 1_000, 10_000) -sine = np.sin(xs) -cosine = np.cos(xs) +sine = np.sin(np.sqrt(xs)) -# alternating sines and cosines -data = np.zeros((10_000, 10_000), dtype=np.float32) -data[::2] = sine -data[1::2] = cosine +data = np.vstack([sine * i for i in range(20_000)]) # plot the image data -heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") +img = fig[0, 0].add_image(data=data, name="heatmap") fig.show() @@ -31,8 +27,8 @@ fig[0, 0].auto_scale() -heatmap_graphic.cmap.vmin = -0.5 -heatmap_graphic.cmap.vmax = 0.5 +img.vmin = -5_000 +img.vmax = 10_000 if __name__ == "__main__": print(__doc__) From d99a94b6d77c0cb1ea92162aa36dcfd71226703b Mon Sep 17 00:00:00 2001 From: Caitlin Date: Tue, 4 Jun 2024 17:05:18 -0400 Subject: [PATCH 085/196] fix hlut --- examples/desktop/image/image_widget.py | 2 +- fastplotlib/graphics/_base.py | 13 ++++ .../graphics/selectors/_linear_region.py | 1 + fastplotlib/widgets/histogram_lut.py | 73 ++++++++++--------- 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/examples/desktop/image/image_widget.py b/examples/desktop/image/image_widget.py index ddfc7c68d..80aafe0b1 100644 --- a/examples/desktop/image/image_widget.py +++ b/examples/desktop/image/image_widget.py @@ -10,7 +10,7 @@ a = iio.imread("imageio:camera.png") -iw = fpl.ImageWidget(data=a, cmap="viridis", histogram_widget=False) +iw = fpl.ImageWidget(data=a, cmap="viridis") iw.show() diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index ad272d8d1..a5de92c16 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -98,6 +98,15 @@ def deleted(self) -> bool: def deleted(self, value: bool): self._deleted.set_value(self, value) + @property + def block_events(self) -> bool: + """Used to block events for a graphic and prevent recursion.""" + return self._block_events + + @block_events.setter + def block_events(self, value: bool): + self._block_events = value + def __init_subclass__(cls, **kwargs): # set the type of the graphic in lower case like "image", "line_collection", etc. cls.type = ( @@ -165,6 +174,7 @@ def __init__( self._rotation = Rotation(rotation) self._offset = Offset(offset) self._visible = Visible(True) + self._block_events = False @property def world_object(self) -> pygfx.WorldObject: @@ -269,6 +279,9 @@ def _handle_event(self, callback, event: pygfx.Event): """Wrap pygfx event to add graphic to pick_info""" event.graphic = self + if self.block_events: + return + if event.type in self.features: # for feature events event._target = self.world_object diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 2c51386cb..9337bc63b 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -208,6 +208,7 @@ def __init__( group.add(edge) # TODO: if parent offset changes, we should set the selector offset too + # TODO: add check if parent is `None`, will throw error otherwise if axis == "x": offset = (parent.offset[0], center, 0) elif axis == "y": diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 971bc1a28..9902f8b7f 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -52,22 +52,23 @@ def __init__( origin = (hist_scaled.max() / 2, 0) self._linear_region_selector = LinearRegionSelector( - bounds=bounds, + selection=bounds, limits=limits, size=size, - origin=origin, + center=origin[0], axis="y", edge_thickness=8, + parent=self._histogram_line ) - # there will be a small difference with the histogram edges so this makes them both line up exactly + #there will be a small difference with the histogram edges so this makes them both line up exactly self._linear_region_selector.selection = ( - image_graphic.cmap.vmin, - image_graphic.cmap.vmax, + self._image_graphic.vmin, + self._image_graphic.vmax, ) - self._vmin = self.image_graphic.cmap.vmin - self._vmax = self.image_graphic.cmap.vmax + self._vmin = self.image_graphic.vmin + self._vmax = self.image_graphic.vmax vmin_str, vmax_str = self._get_vmin_vmax_str() @@ -105,17 +106,15 @@ def __init__( self.world_object.local.scale_x *= -1 - self._text_vmin.position_x = -120 - self._text_vmin.position_y = self._linear_region_selector.selection()[0] + self._text_vmin.offset = (-120, self._linear_region_selector.selection[0], 0) - self._text_vmax.position_x = -120 - self._text_vmax.position_y = self._linear_region_selector.selection()[1] + self._text_vmax.offset = (-120, self._linear_region_selector.selection[1], 0) - self._linear_region_selector.selection.add_event_handler( - self._linear_region_handler + self._linear_region_selector.add_event_handler( + self._linear_region_handler, "selection" ) - self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) + self.image_graphic.add_event_handler(self._image_cmap_handler, "vmin", "vmax") def _get_vmin_vmax_str(self) -> tuple[str, str]: if self.vmin < 0.001 or self.vmin > 99_999: @@ -198,16 +197,13 @@ def _calculate_histogram(self, data): def _linear_region_handler(self, ev): # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges - vmin, vmax = self._linear_region_selector.selection() + selected_ixs = self._linear_region_selector.selection + vmin, vmax = selected_ixs[0], selected_ixs[1] vmin, vmax = vmin / self._scale_factor, vmax / self._scale_factor self.vmin, self.vmax = vmin, vmax def _image_cmap_handler(self, ev): - self.vmin, self.vmax = ev.pick_info["vmin"], ev.pick_info["vmax"] - - def _block_events(self, b: bool): - self.image_graphic.cmap.block_events(b) - self._linear_region_selector.selection.block_events(b) + setattr(self, ev.type, ev.info["value"]) @property def vmin(self) -> float: @@ -215,22 +211,24 @@ def vmin(self) -> float: @vmin.setter def vmin(self, value: float): - self._block_events(True) + self.image_graphic.block_events = True + self._linear_region_selector.block_events = True # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges self._linear_region_selector.selection = ( value * self._scale_factor, - self._linear_region_selector.selection()[1], + self._linear_region_selector.selection[1], ) - self.image_graphic.cmap.vmin = value + self.image_graphic.vmin = value - self._block_events(False) + self.image_graphic.block_events = False + self._linear_region_selector.block_events = False self._vmin = value vmin_str, vmax_str = self._get_vmin_vmax_str() - self._text_vmin.position_y = self._linear_region_selector.selection()[0] + self._text_vmin.offset = (-120, self._linear_region_selector.selection[0], 0) self._text_vmin.text = vmin_str @property @@ -239,22 +237,25 @@ def vmax(self) -> float: @vmax.setter def vmax(self, value: float): - self._block_events(True) + self.image_graphic.block_events = True + self._linear_region_selector.block_events = True # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges self._linear_region_selector.selection = ( - self._linear_region_selector.selection()[0], + self._linear_region_selector.selection[0], value * self._scale_factor, ) - self.image_graphic.cmap.vmax = value - self._block_events(False) + self.image_graphic.vmax = value + + self.image_graphic.block_events = False + self._linear_region_selector.block_events = False self._vmax = value vmin_str, vmax_str = self._get_vmin_vmax_str() - self._text_vmax.position_y = self._linear_region_selector.selection()[1] + self._text_vmax.offset = (-120, self._linear_region_selector.selection[1], 0) self._text_vmax.text = vmax_str def set_data(self, data, reset_vmin_vmax: bool = True): @@ -275,9 +276,11 @@ def set_data(self, data, reset_vmin_vmax: bool = True): self._linear_region_selector.selection = bounds else: # don't change the current selection - self._block_events(True) + self.image_graphic.block_events = True + self._linear_region_selector.block_events = True self._linear_region_selector.limits = limits - self._block_events(False) + self.image_graphic.block_events = False + self._linear_region_selector.block_events = False self._data = weakref.proxy(data) @@ -297,14 +300,14 @@ def image_graphic(self, graphic): if self._image_graphic is not None: # cleanup events from current image graphic - self._image_graphic.cmap.remove_event_handler(self._image_cmap_handler) + self._image_graphic.remove_event_handler(self._image_cmap_handler) self._image_graphic = graphic - self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) + self.image_graphic.add_event_handler(self._image_cmap_handler) def disconnect_image_graphic(self): - self._image_graphic.cmap.remove_event_handler(self._image_cmap_handler) + self._image_graphic.remove_event_handler(self._image_cmap_handler) del self._image_graphic # self._image_graphic = None From fb1dde6b567b43b03e22a4d3d13927b21633f6e9 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 4 Jun 2024 17:25:01 -0400 Subject: [PATCH 086/196] refactor graphics base --- fastplotlib/graphics/_base.py | 478 ------------------ fastplotlib/graphics/_collection_base.py | 277 ++++++++++ fastplotlib/graphics/_positions_base.py | 162 ++++++ fastplotlib/graphics/line.py | 2 +- fastplotlib/graphics/line_collection.py | 2 +- fastplotlib/graphics/scatter.py | 3 +- fastplotlib/graphics/selectors/_linear.py | 3 +- .../graphics/selectors/_linear_region.py | 3 +- 8 files changed, 446 insertions(+), 484 deletions(-) create mode 100644 fastplotlib/graphics/_collection_base.py create mode 100644 fastplotlib/graphics/_positions_base.py diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index a5de92c16..16e85c63d 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -2,9 +2,6 @@ from functools import partial from typing import Any, Literal, TypeAlias import weakref -from warnings import warn -from abc import ABC, abstractmethod -from dataclasses import dataclass import numpy as np import pylinalg as la @@ -13,18 +10,12 @@ import pygfx from ._features import ( - GraphicFeature, BufferManager, Deleted, - VertexPositions, - VertexColors, - VertexCmap, - PointsSizesFeature, Name, Offset, Rotation, Visible, - UniformColor, ) HexStr: TypeAlias = str @@ -396,472 +387,3 @@ def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"): f"`axis` must be either `x`, `y`, or `z`. `{axis}` provided instead!" ) self.rotation = la.quat_mul(rot, self.rotation) - - -class PositionsGraphic(Graphic): - """Base class for LineGraphic and ScatterGraphic""" - - @property - def data(self) -> VertexPositions: - """Get or set the vertex positions data""" - return self._data - - @data.setter - def data(self, value): - self._data[:] = value - - @property - def colors(self) -> VertexColors | pygfx.Color: - """Get or set the colors data""" - if isinstance(self._colors, VertexColors): - return self._colors - - elif isinstance(self._colors, UniformColor): - return self._colors.value - - @colors.setter - def colors(self, value: str | np.ndarray | tuple[float] | list[float] | list[str]): - if isinstance(self._colors, VertexColors): - self._colors[:] = value - - elif isinstance(self._colors, UniformColor): - self._colors.set_value(self, value) - - @property - def cmap(self) -> VertexCmap: - """Control cmap""" - return self._cmap - - @cmap.setter - def cmap(self, name: str): - if self._cmap is None: - raise BufferError("Cannot use cmap with uniform_colors=True") - - self._cmap[:] = name - - def __init__( - self, - data: Any, - colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", - uniform_colors: bool = False, - alpha: float = 1.0, - cmap: str | VertexCmap = None, - cmap_values: np.ndarray = None, - isolated_buffer: bool = True, - *args, - **kwargs, - ): - if isinstance(data, VertexPositions): - self._data = data - else: - self._data = VertexPositions(data, isolated_buffer=isolated_buffer) - - if cmap is not None: - # if a cmap is specified it overrides colors argument - if uniform_colors: - raise TypeError("Cannot use cmap if uniform_colors=True") - - if isinstance(cmap, str): - # make colors from cmap - if isinstance(colors, VertexColors): - # share buffer with existing colors instance for the cmap - self._colors = colors - self._colors._shared += 1 - else: - # create vertex colors buffer - self._colors = VertexColors("w", n_colors=self._data.value.shape[0]) - # make cmap using vertex colors buffer - self._cmap = VertexCmap( - self._colors, cmap_name=cmap, cmap_values=cmap_values - ) - elif isinstance(cmap, VertexCmap): - # use existing cmap instance - self._cmap = cmap - self._colors = cmap._vertex_colors - else: - raise TypeError - else: - # no cmap given - if isinstance(colors, VertexColors): - # share buffer with existing colors instance - self._colors = colors - self._colors._shared += 1 - # blank colormap instance - self._cmap = VertexCmap(self._colors, cmap_name=None, cmap_values=None) - else: - if uniform_colors: - self._colors = UniformColor(colors) - self._cmap = None - else: - self._colors = VertexColors( - colors, - n_colors=self._data.value.shape[0], - alpha=alpha, - ) - self._cmap = VertexCmap( - self._colors, cmap_name=None, cmap_values=None - ) - - super().__init__(*args, **kwargs) - - def detach_feature(self, feature: str): - if not isinstance(feature, str): - raise TypeError - - f = getattr(self, feature) - if f.shared == 0: - raise BufferError("Cannot detach an independent buffer") - - if feature == "colors" and isinstance(feature, VertexColors): - self._colors._buffer = pygfx.Buffer(self._colors.value.copy()) - self.world_object.geometry.colors = self._colors.buffer - self._colors._shared -= 1 - - elif feature == "data": - self._data._buffer = pygfx.Buffer(self._data.value.copy()) - self.world_object.geometry.positions = self._data.buffer - self._data._shared -= 1 - - elif feature == "sizes": - self._sizes._buffer = pygfx.Buffer(self._sizes.value.copy()) - self.world_object.geometry.positions = self._sizes.buffer - self._sizes._shared -= 1 - - def attach_feature( - self, feature: VertexPositions | VertexColors | PointsSizesFeature - ): - if isinstance(feature, VertexPositions): - # TODO: check if this causes a memory leak - self._data._shared -= 1 - - self._data = feature - self._data._shared += 1 - self.world_object.geometry.positions = self._data.buffer - - elif isinstance(feature, VertexColors): - self._colors._shared -= 1 - - self._colors = feature - self._colors._shared += 1 - self.world_object.geometry.colors = self._colors.buffer - - elif isinstance(feature, PointsSizesFeature): - self._sizes._shared -= 1 - - self._sizes = feature - self._sizes._shared += 1 - self.world_object.geometry.sizes = self._sizes.buffer - - def _event_handler(self, event): - """Handles the event after it occurs when two graphic have been linked together.""" - if event.type in self.registered_callbacks.keys(): - for target_info in self.registered_callbacks[event.type]: - if target_info.callback_function is not None: - # if callback_function is not None, then callback function should handle the entire event - target_info.callback_function( - source=self, - target=target_info.target, - event=event, - new_data=target_info.new_data, - ) - - elif isinstance(self, GraphicCollection): - # if target is a GraphicCollection, then indices will be stored in collection_index - if event.type in self.feature_events: - indices = event.pick_info["collection-index"] - - # for now we only have line collections so this works - else: - # get index of world object that made this event - for i, item in enumerate(self.graphics): - wo = WORLD_OBJECTS[item._fpl_address] - # we only store hex id of worldobject, but worldobject `pick_info` is always the real object - # so if pygfx worldobject triggers an event by itself, such as `click`, etc., this will be - # the real world object in the pick_info and not the proxy - if wo is event.pick_info["world_object"]: - indices = i - target_info.target.set_feature( - feature=target_info.feature, - new_data=target_info.new_data, - indices=indices, - ) - else: - # if target is a single graphic, then indices do not matter - target_info.target.set_feature( - feature=target_info.feature, - new_data=target_info.new_data, - indices=None, - ) - - -# Dict that holds all collection graphics in one python instance -COLLECTION_GRAPHICS: dict[HexStr, Graphic] = dict() - - -class CollectionIndexer: - """Collection Indexer""" - - @property - def name(self) -> np.ndarray[str | None]: - return np.asarray([g.name for g in self.graphics]) - - @name.setter - def name(self, values: np.ndarray[str] | list[str]): - self._set_feature("name", values) - - @property - def offset(self) -> np.ndarray: - return np.stack([g.offset for g in self.graphics]) - - @offset.setter - def offset(self, values: np.ndarray | list[np.ndarray]): - self._set_feature("offset", values) - - @property - def rotation(self) -> np.ndarray: - return np.stack([g.rotation for g in self.graphics]) - - @rotation.setter - def rotation(self, values: np.ndarray | list[np.ndarray]): - self._set_feature("rotation", values) - - @property - def visible(self) -> np.ndarray[bool]: - return np.asarray([g.visible for g in self.graphics]) - - @visible.setter - def visible(self, values: np.ndarray[bool] | list[bool]): - self._set_feature("visible", values) - - # TODO: how to work with deleted feature in a collection - - def _set_feature(self, feature, values): - if not len(values) == len(self): - raise IndexError - - for g, v in zip(self.graphics, values): - setattr(g, feature, v) - - def __init__(self, selection: np.ndarray[Graphic], features: set[str]): - """ - - Parameters - ---------- - - selection: np.ndarray of Graphics - array of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` - - """ - - self._selection = selection - self._features = features - - @property - def graphics(self) -> np.ndarray[Graphic]: - """Returns an array of the selected graphics. Always returns a proxy to the Graphic""" - return tuple(self._selection) - - def add_event_handler(self, *args): - """ - Register an event handler. - - Parameters - ---------- - callback: callable, the first argument - Event handler, must accept a single event argument - *types: list of strings - A list of event types, ex: "click", "data", "colors", "pointer_down" - - For the available renderer event types, see - https://jupyter-rfb.readthedocs.io/en/stable/events.html - - All feature support events, i.e. ``graphic.features`` will give a set of - all features that are evented - - Can also be used as a decorator. - - Example - ------- - - .. code-block:: py - - def my_handler(event): - print(event) - - graphic.add_event_handler(my_handler, "pointer_up", "pointer_down") - - Decorator usage example: - - .. code-block:: py - - @graphic.add_event_handler("click") - def my_handler(event): - print(event) - """ - - decorating = not callable(args[0]) - callback = None if decorating else args[0] - types = args if decorating else args[1:] - - if not all(t in set(PYGFX_EVENTS).union(self._features) for t in types): - raise KeyError( - f"event types must be strings for a valid event from the following:\n" - f"{PYGFX_EVENTS + list(self._features)}" - ) - - def decorator(_callback): - for g in self.graphics: - g.add_event_handler(_callback, types) - return _callback - - if decorating: - return decorator - - return decorator(callback) - - def remove_event_handler(self, callback, *types): - for g in self.graphics: - g.remove_event_handler(callback, *types) - - def __getitem__(self, item): - return self.graphics[item] - - def __len__(self): - return len(self._selection) - - def __repr__(self): - return ( - f"{self.__class__.__name__} @ {hex(id(self))}\n" - f"Selection of <{len(self._selection)}> {self._selection[0].__class__.__name__}" - ) - - -class GraphicCollection(Graphic): - """Graphic Collection base class""" - - child_type: type - _indexer: type - - def __init__(self, name: str = None): - super().__init__(name) - - # list of mem locations of the graphics - self._graphics: list[str] = list() - - self._graphics_changed: bool = True - self._graphics_array: np.ndarray[Graphic] = None - - @property - def graphics(self) -> np.ndarray[Graphic]: - """The Graphics within this collection. Always returns a proxy to the Graphics.""" - if self._graphics_changed: - proxies = [ - weakref.proxy(COLLECTION_GRAPHICS[addr]) for addr in self._graphics - ] - self._graphics_array = np.array(proxies) - self._graphics_array.flags["WRITEABLE"] = False - self._graphics_changed = False - - return self._graphics_array - - def add_graphic(self, graphic: Graphic): - """ - Add a graphic to the collection. - - Parameters - ---------- - graphic: Graphic - graphic to add, must be a real ``Graphic`` not a proxy - - """ - - if not type(graphic) == self.child_type: - raise TypeError( - f"Can only add graphics of the same type to a collection.\n" - f"You can only add {self.child_type.__name__} to a {self.__class__.__name__}, " - f"you are trying to add a {graphic.__class__.__name__}." - ) - - addr = graphic._fpl_address - COLLECTION_GRAPHICS[addr] = graphic - - self._graphics.append(addr) - - self.world_object.add(graphic.world_object) - - self._graphics_changed = True - - def remove_graphic(self, graphic: Graphic): - """ - Remove a graphic from the collection. - - Note: Only removes the graphic from the collection. Does not remove - the graphic from the scene, and does not delete the graphic. - - Parameters - ---------- - graphic: Graphic - graphic to remove - - """ - - self._graphics.remove(graphic._fpl_address) - - self.world_object.remove(graphic.world_object) - - self._graphics_changed = True - - def add_event_handler(self, *args): - raise NotImplementedError("Slice graphic collection to add event handlers") - - def remove_event_handler(self, callback, *types): - raise NotImplementedError("Slice graphic collection to remove event handlers") - - def __getitem__(self, key) -> CollectionIndexer: - return self._indexer( - selection=self.graphics[key], - ) - - def __del__(self): - self.world_object.clear() - - for addr in self._graphics: - del COLLECTION_GRAPHICS[addr] - - super().__del__() - - def __len__(self): - return len(self._graphics) - - def __repr__(self): - rval = super().__repr__() - return f"{rval}\nCollection of <{len(self._graphics)}> Graphics" - - -class CollectionFeature: - """Collection Feature""" - - def __init__(self, selection: np.ndarray[Graphic], feature: str): - """ - selection: list of Graphics - a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` - - feature: str - feature of Graphics in the GraphicCollection being indexed - - """ - - self._selection = selection - self._feature = feature - - self._feature_instances = [getattr(g, feature) for g in self._selection] - - def __getitem__(self, item): - return [fi[item] for fi in self._feature_instances] - - def __setitem__(self, key, value): - for fi in self._feature_instances: - fi[key] = value - - def __repr__(self): - return f"Collection feature for: <{self._feature}>" diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py new file mode 100644 index 000000000..50e6ffb19 --- /dev/null +++ b/fastplotlib/graphics/_collection_base.py @@ -0,0 +1,277 @@ +import weakref + +import numpy as np + +from ._base import HexStr, Graphic, PYGFX_EVENTS + +# Dict that holds all collection graphics in one python instance +COLLECTION_GRAPHICS: dict[HexStr, Graphic] = dict() + + +class CollectionIndexer: + """Collection Indexer""" + + @property + def name(self) -> np.ndarray[str | None]: + return np.asarray([g.name for g in self.graphics]) + + @name.setter + def name(self, values: np.ndarray[str] | list[str]): + self._set_feature("name", values) + + @property + def offset(self) -> np.ndarray: + return np.stack([g.offset for g in self.graphics]) + + @offset.setter + def offset(self, values: np.ndarray | list[np.ndarray]): + self._set_feature("offset", values) + + @property + def rotation(self) -> np.ndarray: + return np.stack([g.rotation for g in self.graphics]) + + @rotation.setter + def rotation(self, values: np.ndarray | list[np.ndarray]): + self._set_feature("rotation", values) + + @property + def visible(self) -> np.ndarray[bool]: + return np.asarray([g.visible for g in self.graphics]) + + @visible.setter + def visible(self, values: np.ndarray[bool] | list[bool]): + self._set_feature("visible", values) + + # TODO: how to work with deleted feature in a collection + + def _set_feature(self, feature, values): + if not len(values) == len(self): + raise IndexError + + for g, v in zip(self.graphics, values): + setattr(g, feature, v) + + def __init__(self, selection: np.ndarray[Graphic], features: set[str]): + """ + + Parameters + ---------- + + selection: np.ndarray of Graphics + array of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` + + """ + + self._selection = selection + self._features = features + + @property + def graphics(self) -> np.ndarray[Graphic]: + """Returns an array of the selected graphics. Always returns a proxy to the Graphic""" + return tuple(self._selection) + + def add_event_handler(self, *args): + """ + Register an event handler. + + Parameters + ---------- + callback: callable, the first argument + Event handler, must accept a single event argument + *types: list of strings + A list of event types, ex: "click", "data", "colors", "pointer_down" + + For the available renderer event types, see + https://jupyter-rfb.readthedocs.io/en/stable/events.html + + All feature support events, i.e. ``graphic.features`` will give a set of + all features that are evented + + Can also be used as a decorator. + + Example + ------- + + .. code-block:: py + + def my_handler(event): + print(event) + + graphic.add_event_handler(my_handler, "pointer_up", "pointer_down") + + Decorator usage example: + + .. code-block:: py + + @graphic.add_event_handler("click") + def my_handler(event): + print(event) + """ + + decorating = not callable(args[0]) + callback = None if decorating else args[0] + types = args if decorating else args[1:] + + if not all(t in set(PYGFX_EVENTS).union(self._features) for t in types): + raise KeyError( + f"event types must be strings for a valid event from the following:\n" + f"{PYGFX_EVENTS + list(self._features)}" + ) + + def decorator(_callback): + for g in self.graphics: + g.add_event_handler(_callback, types) + return _callback + + if decorating: + return decorator + + return decorator(callback) + + def remove_event_handler(self, callback, *types): + for g in self.graphics: + g.remove_event_handler(callback, *types) + + def __getitem__(self, item): + return self.graphics[item] + + def __len__(self): + return len(self._selection) + + def __repr__(self): + return ( + f"{self.__class__.__name__} @ {hex(id(self))}\n" + f"Selection of <{len(self._selection)}> {self._selection[0].__class__.__name__}" + ) + + +class GraphicCollection(Graphic): + """Graphic Collection base class""" + + child_type: type + _indexer: type + + def __init__(self, name: str = None): + super().__init__(name) + + # list of mem locations of the graphics + self._graphics: list[str] = list() + + self._graphics_changed: bool = True + self._graphics_array: np.ndarray[Graphic] = None + + @property + def graphics(self) -> np.ndarray[Graphic]: + """The Graphics within this collection. Always returns a proxy to the Graphics.""" + if self._graphics_changed: + proxies = [ + weakref.proxy(COLLECTION_GRAPHICS[addr]) for addr in self._graphics + ] + self._graphics_array = np.array(proxies) + self._graphics_array.flags["WRITEABLE"] = False + self._graphics_changed = False + + return self._graphics_array + + def add_graphic(self, graphic: Graphic): + """ + Add a graphic to the collection. + + Parameters + ---------- + graphic: Graphic + graphic to add, must be a real ``Graphic`` not a proxy + + """ + + if not type(graphic) == self.child_type: + raise TypeError( + f"Can only add graphics of the same type to a collection.\n" + f"You can only add {self.child_type.__name__} to a {self.__class__.__name__}, " + f"you are trying to add a {graphic.__class__.__name__}." + ) + + addr = graphic._fpl_address + COLLECTION_GRAPHICS[addr] = graphic + + self._graphics.append(addr) + + self.world_object.add(graphic.world_object) + + self._graphics_changed = True + + def remove_graphic(self, graphic: Graphic): + """ + Remove a graphic from the collection. + + Note: Only removes the graphic from the collection. Does not remove + the graphic from the scene, and does not delete the graphic. + + Parameters + ---------- + graphic: Graphic + graphic to remove + + """ + + self._graphics.remove(graphic._fpl_address) + + self.world_object.remove(graphic.world_object) + + self._graphics_changed = True + + def add_event_handler(self, *args): + raise NotImplementedError("Slice graphic collection to add event handlers") + + def remove_event_handler(self, callback, *types): + raise NotImplementedError("Slice graphic collection to remove event handlers") + + def __getitem__(self, key) -> CollectionIndexer: + return self._indexer( + selection=self.graphics[key], + ) + + def __del__(self): + self.world_object.clear() + + for addr in self._graphics: + del COLLECTION_GRAPHICS[addr] + + super().__del__() + + def __len__(self): + return len(self._graphics) + + def __repr__(self): + rval = super().__repr__() + return f"{rval}\nCollection of <{len(self._graphics)}> Graphics" + + +class CollectionFeature: + """Collection Feature""" + + def __init__(self, selection: np.ndarray[Graphic], feature: str): + """ + selection: list of Graphics + a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` + + feature: str + feature of Graphics in the GraphicCollection being indexed + + """ + + self._selection = selection + self._feature = feature + + self._feature_instances = [getattr(g, feature) for g in self._selection] + + def __getitem__(self, item): + return [fi[item] for fi in self._feature_instances] + + def __setitem__(self, key, value): + for fi in self._feature_instances: + fi[key] = value + + def __repr__(self): + return f"Collection feature for: <{self._feature}>" diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py new file mode 100644 index 000000000..e9a3c92c7 --- /dev/null +++ b/fastplotlib/graphics/_positions_base.py @@ -0,0 +1,162 @@ +from typing import Any + +import numpy as np + +import pygfx +from ._base import Graphic +from ._features import VertexPositions, VertexColors, UniformColor, VertexCmap, PointsSizesFeature + + +class PositionsGraphic(Graphic): + """Base class for LineGraphic and ScatterGraphic""" + + @property + def data(self) -> VertexPositions: + """Get or set the vertex positions data""" + return self._data + + @data.setter + def data(self, value): + self._data[:] = value + + @property + def colors(self) -> VertexColors | pygfx.Color: + """Get or set the colors data""" + if isinstance(self._colors, VertexColors): + return self._colors + + elif isinstance(self._colors, UniformColor): + return self._colors.value + + @colors.setter + def colors(self, value: str | np.ndarray | tuple[float] | list[float] | list[str]): + if isinstance(self._colors, VertexColors): + self._colors[:] = value + + elif isinstance(self._colors, UniformColor): + self._colors.set_value(self, value) + + @property + def cmap(self) -> VertexCmap: + """Control cmap""" + return self._cmap + + @cmap.setter + def cmap(self, name: str): + if self._cmap is None: + raise BufferError("Cannot use cmap with uniform_colors=True") + + self._cmap[:] = name + + def __init__( + self, + data: Any, + colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", + uniform_colors: bool = False, + alpha: float = 1.0, + cmap: str | VertexCmap = None, + cmap_values: np.ndarray = None, + isolated_buffer: bool = True, + *args, + **kwargs, + ): + if isinstance(data, VertexPositions): + self._data = data + else: + self._data = VertexPositions(data, isolated_buffer=isolated_buffer) + + if cmap is not None: + # if a cmap is specified it overrides colors argument + if uniform_colors: + raise TypeError("Cannot use cmap if uniform_colors=True") + + if isinstance(cmap, str): + # make colors from cmap + if isinstance(colors, VertexColors): + # share buffer with existing colors instance for the cmap + self._colors = colors + self._colors._shared += 1 + else: + # create vertex colors buffer + self._colors = VertexColors("w", n_colors=self._data.value.shape[0]) + # make cmap using vertex colors buffer + self._cmap = VertexCmap( + self._colors, cmap_name=cmap, cmap_values=cmap_values + ) + elif isinstance(cmap, VertexCmap): + # use existing cmap instance + self._cmap = cmap + self._colors = cmap._vertex_colors + else: + raise TypeError + else: + # no cmap given + if isinstance(colors, VertexColors): + # share buffer with existing colors instance + self._colors = colors + self._colors._shared += 1 + # blank colormap instance + self._cmap = VertexCmap(self._colors, cmap_name=None, cmap_values=None) + else: + if uniform_colors: + self._colors = UniformColor(colors) + self._cmap = None + else: + self._colors = VertexColors( + colors, + n_colors=self._data.value.shape[0], + alpha=alpha, + ) + self._cmap = VertexCmap( + self._colors, cmap_name=None, cmap_values=None + ) + + super().__init__(*args, **kwargs) + + def detach_feature(self, feature: str): + if not isinstance(feature, str): + raise TypeError + + f = getattr(self, feature) + if f.shared == 0: + raise BufferError("Cannot detach an independent buffer") + + if feature == "colors" and isinstance(feature, VertexColors): + self._colors._buffer = pygfx.Buffer(self._colors.value.copy()) + self.world_object.geometry.colors = self._colors.buffer + self._colors._shared -= 1 + + elif feature == "data": + self._data._buffer = pygfx.Buffer(self._data.value.copy()) + self.world_object.geometry.positions = self._data.buffer + self._data._shared -= 1 + + elif feature == "sizes": + self._sizes._buffer = pygfx.Buffer(self._sizes.value.copy()) + self.world_object.geometry.positions = self._sizes.buffer + self._sizes._shared -= 1 + + def attach_feature( + self, feature: VertexPositions | VertexColors | PointsSizesFeature + ): + if isinstance(feature, VertexPositions): + # TODO: check if this causes a memory leak + self._data._shared -= 1 + + self._data = feature + self._data._shared += 1 + self.world_object.geometry.positions = self._data.buffer + + elif isinstance(feature, VertexColors): + self._colors._shared -= 1 + + self._colors = feature + self._colors._shared += 1 + self.world_object.geometry.colors = self._colors.buffer + + elif isinstance(feature, PointsSizesFeature): + self._sizes._shared -= 1 + + self._sizes = feature + self._sizes._shared += 1 + self.world_object.geometry.sizes = self._sizes.buffer diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 421a9b32a..10067404c 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -5,7 +5,7 @@ import pygfx -from ._base import PositionsGraphic +from ._positions_base import PositionsGraphic from .selectors import LinearRegionSelector, LinearSelector from ._features import Thickness diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index e286462e8..72ee04f37 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -6,7 +6,7 @@ import pygfx from ..utils import parse_cmap_values -from ._base import GraphicCollection, CollectionIndexer, CollectionFeature +from ._collection_base import CollectionIndexer, GraphicCollection, CollectionFeature from .line import LineGraphic from .selectors import LinearRegionSelector, LinearSelector diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index bc46c4923..1c0445b28 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -3,8 +3,7 @@ import numpy as np import pygfx -from ..utils import parse_cmap_values -from ._base import PositionsGraphic +from ._positions_base import PositionsGraphic from ._features import PointsSizesFeature, UniformSizes diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index d9f516c17..e2debc6d7 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -6,7 +6,8 @@ import pygfx from ...utils.gui import IS_JUPYTER -from .._base import Graphic, GraphicCollection +from .._base import Graphic +from .._collection_base import GraphicCollection from .._features._selection_features import LinearSelectionFeature from ._base_selector import BaseSelector diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 9337bc63b..545723df8 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -5,7 +5,8 @@ import pygfx from ...utils.gui import IS_JUPYTER -from .._base import Graphic, GraphicCollection +from .._base import Graphic +from .._collection_base import GraphicCollection from .._features._selection_features import LinearRegionSelectionFeature from ._base_selector import BaseSelector From 428aab0134fee516c17270c6d1973042f8a4d268 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 4 Jun 2024 18:25:39 -0400 Subject: [PATCH 087/196] update line examples --- examples/desktop/line/line_dataslice.py | 6 +-- examples/desktop/line/line_present_scaling.py | 49 ------------------- 2 files changed, 3 insertions(+), 52 deletions(-) delete mode 100644 examples/desktop/line/line_present_scaling.py diff --git a/examples/desktop/line/line_dataslice.py b/examples/desktop/line/line_dataslice.py index d8a107559..c2c6b9d36 100644 --- a/examples/desktop/line/line_dataslice.py +++ b/examples/desktop/line/line_dataslice.py @@ -41,9 +41,9 @@ cosine_graphic.data[90:, 1] = 7 cosine_graphic.data[0] = np.array([[-10, 0, 0]]) -# additional fancy indexing using numpy -key2 = [True, False] * 50 -sinc_graphic.data[key2] = np.array([[5, 1, 2]]) +# additional fancy indexing with boolean array +bool_key = [True, True, True, False, False] * 20 +sinc_graphic.data[bool_key, 1] = 7 # y vals to 1 fig.canvas.set_logical_size(800, 800) diff --git a/examples/desktop/line/line_present_scaling.py b/examples/desktop/line/line_present_scaling.py deleted file mode 100644 index d334e6fbd..000000000 --- a/examples/desktop/line/line_present_scaling.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Line Plot -============ -Example showing present and scaling feature for lines. -""" - -# test_example = true - -import fastplotlib as fpl -import numpy as np - - -fig = fpl.Figure() - -xs = np.linspace(-10, 10, 100) -# sine wave -ys = np.sin(xs) -sine = np.dstack([xs, ys])[0] - -# cosine wave -ys = np.cos(xs) + 5 -cosine = np.dstack([xs, ys])[0] - -# sinc function -a = 0.5 -ys = np.sinc(xs) * 3 + 8 -sinc = np.dstack([xs, ys])[0] - -sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") - -# you can also use colormaps for lines! -cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") - -# or a list of colors for each datapoint -colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) - -fig.show() - -sinc_graphic.present = False - -fig.canvas.set_logical_size(800, 800) - -fig[0, 0].auto_scale() - - -if __name__ == "__main__": - print(__doc__) - fpl.run() From a41432f75838d10f497d8766f0dbd4812dfc7ee9 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 5 Jun 2024 14:50:05 -0400 Subject: [PATCH 088/196] bug fixes --- fastplotlib/graphics/selectors/_linear_region.py | 4 ++-- fastplotlib/widgets/histogram_lut.py | 3 ++- fastplotlib/widgets/image.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 545723df8..c792abd80 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -59,7 +59,7 @@ def limits(self, values: Tuple[float, float]): self._limits = tuple( map(round, values) ) # if values are close to zero things get weird so round them - self.selection._limits = self._limits + self._selection._limits = self._limits def __init__( self, @@ -460,7 +460,7 @@ def _setup_ipywidget_slider(self, widget): widget.observe(self._ipywidget_callback, "value") # user changes linear selection -> widget changes - self.selection.add_event_handler(self._update_ipywidgets, "selection") + self.add_event_handler(self._update_ipywidgets, "selection") self._plot_area.renderer.add_event_handler(self._set_slider_layout, "resize") diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 9902f8b7f..7c81b233b 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -263,7 +263,8 @@ def set_data(self, data, reset_vmin_vmax: bool = True): line_data = np.column_stack([hist_scaled, edges_flanked]) - self._histogram_line.data = line_data + # set x and y vals + self._histogram_line.data[:, :2] = line_data bounds = (edges[0], edges[-1]) limits = (edges_flanked[0], edges_flanked[-11]) diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index c7de0a126..df9b46b55 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -754,7 +754,7 @@ def reset_vmin_vmax(self): Reset the vmin and vmax w.r.t. the full data """ for ig in self.managed_graphics: - ig.cmap.reset_vmin_vmax() + ig.reset_vmin_vmax() def reset_vmin_vmax_frame(self): """ @@ -772,7 +772,7 @@ def reset_vmin_vmax_frame(self): hlut = subplot.docks["right"]["histogram_lut"] # set the data using the current image graphic data - hlut.set_data(subplot["image_widget_managed"].data()) + hlut.set_data(subplot["image_widget_managed"].data.value) def set_data( self, From 0eb6ad48938c93dc352493b87bd3fe57333abd6a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 5 Jun 2024 20:22:11 -0400 Subject: [PATCH 089/196] update text to use new gfeatures --- fastplotlib/graphics/_features/__init__.py | 3 + fastplotlib/graphics/_features/_text.py | 92 +++++++++++++++ fastplotlib/graphics/text.py | 127 +++++++++------------ fastplotlib/layouts/_subplot.py | 2 +- fastplotlib/widgets/histogram_lut.py | 4 +- 5 files changed, 153 insertions(+), 75 deletions(-) create mode 100644 fastplotlib/graphics/_features/_text.py diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index 40d9e181f..b3399fc82 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -22,5 +22,8 @@ FeatureEvent, to_gpu_supported_dtype, ) + +from ._text import TextData, FontSize, TextFaceColor, TextOutlineColor, TextOutlineThickness + from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature from ._common import Name, Offset, Rotation, Visible, Deleted diff --git a/fastplotlib/graphics/_features/_text.py b/fastplotlib/graphics/_features/_text.py new file mode 100644 index 000000000..ab9c18f77 --- /dev/null +++ b/fastplotlib/graphics/_features/_text.py @@ -0,0 +1,92 @@ +import numpy as np + +import pygfx + +from ._base import GraphicFeature, FeatureEvent + + +class TextData(GraphicFeature): + def __init__(self, value: str): + self._value = value + super().__init__() + + @property + def value(self) -> str: + return self._value + + def set_value(self, graphic, value: str): + graphic.world_object.geometry.set_text(value) + self._value = value + + event = FeatureEvent(type="text", info={"value": value}) + self._call_event_handlers(event) + + +class FontSize(GraphicFeature): + def __init__(self, value: float | int): + self._value = value + super().__init__() + + @property + def value(self) -> float | int: + return self._value + + def set_value(self, graphic, value: float | int): + graphic.world_object.geometry.font_size = value + self._value = value + + event = FeatureEvent(type="font_size", info={"value": value}) + self._call_event_handlers(event) + + +class TextFaceColor(GraphicFeature): + def __init__(self, value: str | np.ndarray | list[float] | tuple[float]): + self._value = pygfx.Color(value) + super().__init__() + + @property + def value(self) -> pygfx.Color: + return self._value + + def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float]): + value = pygfx.Color(value) + graphic.world_object.material.color = value + self._value = value + + event = FeatureEvent(type="face_color", info={"value": value}) + self._call_event_handlers(event) + + +class TextOutlineColor(GraphicFeature): + def __init__(self, value: str | np.ndarray | list[float] | tuple[float]): + self._value = pygfx.Color(value) + super().__init__() + + @property + def value(self) -> pygfx.Color: + return self._value + + def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float]): + value = pygfx.Color(value) + graphic.world_object.material.outline_color = value + self._value = value + + event = FeatureEvent(type="outline_color", info={"value": value}) + self._call_event_handlers(event) + + +class TextOutlineThickness(GraphicFeature): + def __init__(self, value: float | int): + self._value = value + super().__init__() + + @property + def value(self) -> float | int: + return self._value + + def set_value(self, graphic, value: float | int): + graphic.world_object.material.outline_thickness = value + self._value = value + + event = FeatureEvent(type="outline_thickness", info={"value": value}) + self._call_event_handlers(event) diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index 27d49eece..9f1714206 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -1,20 +1,20 @@ -from typing import * import pygfx import numpy as np from ._base import Graphic +from ._features import TextData, FontSize, TextFaceColor, TextOutlineColor, TextOutlineThickness class TextGraphic(Graphic): def __init__( self, text: str, - position: Tuple[int] = (0, 0, 0), - size: int = 14, - face_color: Union[str, np.ndarray] = "w", - outline_color: Union[str, np.ndarray] = "w", - outline_thickness=0, + font_size: float | int = 14, + face_color: str | np.ndarray | list[float] | tuple[float] = "w", + outline_color: str | np.ndarray | list[float] | tuple[float] = "w", + outline_thickness: float | int = 0, screen_space: bool = True, + offset: tuple[float] = (0, 0, 0), anchor: str = "middle-center", **kwargs, ): @@ -24,13 +24,10 @@ def __init__( Parameters ---------- text: str - display text + text to display - position: int tuple, default (0, 0, 0) - int tuple indicating location of text in scene - - size: int, default 10 - text size + font_size: float | int, default 10 + font size face_color: str or array, default "w" str or RGBA array to set the color of the text @@ -38,14 +35,14 @@ def __init__( outline_color: str or array, default "w" str or RGBA array to set the outline color of the text - outline_thickness: int, default 0 + outline_thickness: float | int, default 0 text outline thickness screen_space: bool = True - whether the text is rendered in screen space, in contrast to world space + if True, text size is in screen space, if False the text size is in data space - name: str, optional - name of graphic, passed to Graphic + offset: (float, float, float), default (0, 0, 0) + places the text at this location anchor: str, default "middle-center" position of the origin of the text @@ -53,94 +50,80 @@ def __init__( * Vertical values: "top", "middle", "baseline", "bottom" * Horizontal values: "left", "center", "right" + + **kwargs + passed to Graphic + """ + super().__init__(**kwargs) - self._text = text + self._text = TextData(text) + self._font_size = FontSize(font_size) + self._face_color = TextFaceColor(face_color) + self._outline_color = TextOutlineColor(outline_color) + self._outline_thickness = TextOutlineThickness(outline_thickness) world_object = pygfx.Text( pygfx.TextGeometry( - text=str(text), - font_size=size, + text=self.text, + font_size=self.font_size, screen_space=screen_space, anchor=anchor, ), pygfx.TextMaterial( - color=face_color, - outline_color=outline_color, - outline_thickness=outline_thickness, + color=self.face_color, + outline_color=self.outline_color, + outline_thickness=self.outline_thickness, pick_write=True, ), ) self._set_world_object(world_object) - self.world_object.position = position + self.offset = offset @property - def text(self): - """Returns the text of this graphic.""" - return self._text + def text(self) -> str: + """the text displayed""" + return self._text.value @text.setter def text(self, text: str): - """Set the text of this graphic.""" - if not isinstance(text, str): - raise ValueError("Text must be of type str.") - - self._text = text - self.world_object.geometry.set_text(self._text) + self._text.set_value(self, text) @property - def text_size(self): - """Returns the text size of this graphic.""" - return self.world_object.geometry.font_size + def font_size(self) -> float | int: + """"text font size""" + return self._font_size.value - @text_size.setter - def text_size(self, size: Union[int, float]): - """Set the text size of this graphic.""" - if not (isinstance(size, int) or isinstance(size, float)): - raise ValueError("Text size must be of type int or float") - - self.world_object.geometry.font_size = size + @font_size.setter + def font_size(self, size: float | int): + self._font_size.set_value(self, size) @property - def face_color(self): - """Returns the face color of this graphic.""" - return self.world_object.material.color + def face_color(self) -> pygfx.Color: + """text face color""" + return self._face_color.value @face_color.setter - def face_color(self, color: Union[str, np.ndarray]): - """Set the face color of this graphic.""" - if not (isinstance(color, str) or isinstance(color, np.ndarray)): - raise ValueError("Face color must be of type str or np.ndarray") - - color = pygfx.Color(color) - - self.world_object.material.color = color + def face_color(self, color: str | np.ndarray | list[float] | tuple[float]): + self._face_color.set_value(self, color) @property - def outline_size(self): - """Returns the outline size of this graphic.""" - return self.world_object.material.outline_thickness + def outline_thickness(self) -> float | int: + """text outline thickness""" + return self._outline_thickness.value - @outline_size.setter - def outline_size(self, size: Union[int, float]): - """Set the outline size of this text graphic.""" - if not (isinstance(size, int) or isinstance(size, float)): - raise ValueError("Outline size must be of type int or float") - - self.world_object.material.outline_thickness = size + @outline_thickness.setter + def outline_thickness(self, thickness: float | int): + self._outline_thickness.set_value(self, thickness) @property - def outline_color(self): - """Returns the outline color of this graphic.""" - return self.world_object.material.outline_color + def outline_color(self) -> pygfx.Color: + """text outline color""" + return self._outline_color.value @outline_color.setter - def outline_color(self, color: Union[str, np.ndarray]): - """Set the outline color of this graphic""" - if not (isinstance(color, str) or isinstance(color, np.ndarray)): - raise ValueError("Outline color must be of type str or np.ndarray") - - self.world_object.material.outline_color = color + def outline_color(self, color: str | np.ndarray | list[float] | tuple[float]): + self._outline_color.set_value(self, color) diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index a541c9d78..059307e6b 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -149,7 +149,7 @@ def set_title(self, text: str): if self._title_graphic is not None: self._title_graphic.text = text else: - tg = TextGraphic(text=text, size=18) + tg = TextGraphic(text=text, font_size=18) self._title_graphic = tg self.docks["top"].size = 35 diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 7c81b233b..44bddd9b0 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -74,7 +74,7 @@ def __init__( self._text_vmin = TextGraphic( text=vmin_str, - size=16, + font_size=16, position=(0, 0), anchor="top-left", outline_color="black", @@ -85,7 +85,7 @@ def __init__( self._text_vmax = TextGraphic( text=vmax_str, - size=16, + font_size=16, position=(0, 0), anchor="bottom-left", outline_color="black", From 739b47a3d6f0fd7ed1a20558c6c1dc8d1e55b9bf Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 5 Jun 2024 20:31:53 -0400 Subject: [PATCH 090/196] cleanup --- fastplotlib/graphics/_features/_sizes.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 fastplotlib/graphics/_features/_sizes.py diff --git a/fastplotlib/graphics/_features/_sizes.py b/fastplotlib/graphics/_features/_sizes.py deleted file mode 100644 index 8b1378917..000000000 --- a/fastplotlib/graphics/_features/_sizes.py +++ /dev/null @@ -1 +0,0 @@ - From ce8082d3162744bbbcb8ec8257ecd9f55e2dff6d Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Jun 2024 00:21:23 -0400 Subject: [PATCH 091/196] implement iterator for TextureArray, much simpler now :D --- fastplotlib/graphics/_features/_image.py | 61 +++++++++++++---- fastplotlib/graphics/image.py | 87 +++++++++++++++--------- 2 files changed, 103 insertions(+), 45 deletions(-) diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py index 4b48e028f..6f0c423da 100644 --- a/fastplotlib/graphics/_features/_image.py +++ b/fastplotlib/graphics/_features/_image.py @@ -1,4 +1,5 @@ from itertools import product + from math import ceil import numpy as np @@ -28,7 +29,7 @@ def __init__(self, data, isolated_buffer: bool = True): # user's input array is used as the buffer self._value = data - # indices for each Texture + # data start indices for each Texture self._row_indices = np.arange( 0, ceil(self.value.shape[0] / WGPU_MAX_TEXTURE_SIZE) * WGPU_MAX_TEXTURE_SIZE, @@ -45,20 +46,14 @@ def __init__(self, data, isolated_buffer: bool = True): shape=(self.row_indices.size, self.col_indices.size), dtype=object ) - # max index - row_max = self.value.shape[0] - 1 - col_max = self.value.shape[1] - 1 - - for buffer_row, row_ix in enumerate(self.row_indices): - for buffer_col, col_ix in enumerate(self.col_indices): - # stop index for this chunk - row_stop = min(row_max, row_ix + WGPU_MAX_TEXTURE_SIZE) - col_stop = min(col_max, col_ix + WGPU_MAX_TEXTURE_SIZE) + self._iter = None - # make texture from slice - texture = pygfx.Texture(self.value[row_ix:row_stop, col_ix:col_stop], dim=2) + # iterate through each chunk of passed `data` + # create a pygfx.Texture from this chunk + for _, buffer_index, data_slice in self: + texture = pygfx.Texture(self.value[data_slice], dim=2) - self.buffer[buffer_row, buffer_col] = texture + self.buffer[buffer_index] = texture self._shared: int = 0 @@ -75,10 +70,18 @@ def buffer(self) -> np.ndarray[pygfx.Texture]: @property def row_indices(self) -> np.ndarray: + """ + row indices that are used to chunk the big data array + into individual Textures on the GPU + """ return self._row_indices @property def col_indices(self) -> np.ndarray: + """ + column indices that are used to chunk the big data array + into individual Textures on the GPU + """ return self._col_indices @property @@ -95,6 +98,38 @@ def _fix_data(self, data): # let's just cast to float32 always return data.astype(np.float32) + def __iter__(self): + self._iter = product(enumerate(self.row_indices), enumerate(self.col_indices)) + return self + + def __next__(self) -> tuple[pygfx.Texture, tuple[int, int], tuple[slice, slice]]: + """ + Iterate through each Texture within the texture array + + Returns + ------- + Texture, tuple[int, int], tuple[slice, slice] + | Texture: pygfx.Texture + | tuple[int, int]: chunk index, i.e corresponding index of ``self.buffer`` array + | tuple[slice, slice]: data slice of big array in this chunk and Texture + """ + (chunk_row, data_row_start), (chunk_col, data_col_start) = next(self._iter) + + # indices for to self.buffer for this chunk + chunk_index = (chunk_row, chunk_col) + + # stop indices of big data array for this chunk + row_stop = min(self.value.shape[0] - 1, data_row_start + WGPU_MAX_TEXTURE_SIZE) + col_stop = min(self.value.shape[1] - 1, data_col_start + WGPU_MAX_TEXTURE_SIZE) + + # row and column slices that slice the data for this chunk from the big data array + data_slice = (slice(data_row_start, row_stop), slice(data_col_start, col_stop)) + + # texture for this chunk + texture = self.buffer[chunk_index] + + return texture, chunk_index, data_slice + def __getitem__(self, item): return self.value[item] diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index e379f56f9..5ba9daec1 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -21,49 +21,48 @@ class _ImageTile(pygfx.Image): """ - Similar to pygfx.Image, only difference is that it contains a few properties to keep track of - row chunk index, column chunk index + Similar to pygfx.Image, only difference is that it modifies the pick_info + by adding the data row start indices that correspond to this chunk of the big image """ def __init__( - self, geometry, material, row_chunk_ix: int, col_chunk_ix: int, **kwargs + self, geometry, material, data_slice: tuple[slice, slice], chunk_index: tuple[int, int], **kwargs ): super().__init__(geometry, material, **kwargs) - self._row_chunk_index = row_chunk_ix - self._col_chunk_index = col_chunk_ix + self._data_slice = data_slice + self._chunk_index = chunk_index def _wgpu_get_pick_info(self, pick_value): pick_info = super()._wgpu_get_pick_info(pick_value) - row_start_ix = WGPU_MAX_TEXTURE_SIZE * self.row_chunk_index - col_start_ix = WGPU_MAX_TEXTURE_SIZE * self.col_chunk_index + data_row_start, data_col_start = self.data_slice[0].start, self.data_slice[1].start - # adjust w.r.t. chunk + # add the actual data row and col start indices x, y = pick_info["index"] - x += col_start_ix - y += row_start_ix + x += data_col_start + y += data_row_start pick_info["index"] = (x, y) xp, yp = pick_info["pixel_coord"] - xp += col_start_ix - yp += row_start_ix + xp += data_col_start + yp += data_row_start pick_info["pixel_coord"] = (xp, yp) # add row chunk and col chunk index to pick_info dict return { **pick_info, - "row_chunk_index": self.row_chunk_index, - "col_chunk_index": self.col_chunk_index, + "data_slice": self.data_slice, + "chunk_index": self.chunk_index } @property - def row_chunk_index(self) -> int: - return self._row_chunk_index + def data_slice(self) -> tuple[slice, slice]: + return self._data_slice @property - def col_chunk_index(self) -> int: - return self._col_chunk_index + def chunk_index(self) -> tuple[int, int]: + return self._chunk_index class ImageGraphic(Graphic): @@ -184,11 +183,13 @@ def __init__( world_object = pygfx.Group() + # texture array that manages the textures on the GPU for displaying this image self._data = TextureArray(data, isolated_buffer=isolated_buffer) if (vmin is None) or (vmax is None): vmin, vmax = quick_min_max(data) + # other graphic features self._vmin = ImageVmin(vmin) self._vmax = ImageVmax(vmax) @@ -197,33 +198,55 @@ def __init__( self._interpolation = ImageInterpolation(interpolation) self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation) + # use cmap if not RGB + if self._data.value.ndim == 2: + _map = None + else: + _map = self._cmap.texture + + # one common material is used for every Texture chunk self._material = pygfx.ImageBasicMaterial( clim=(vmin, vmax), - map=self._cmap.texture - if self._data.value.ndim == 2 - else None, # RGB vs. grayscale + map=_map, interpolation=self._interpolation.value, map_interpolation=self._cmap_interpolation.value, pick_write=True, ) - for row_ix in range(self._data.row_indices.size): - for col_ix in range(self._data.col_indices.size): - img = _ImageTile( - geometry=pygfx.Geometry(grid=self._data.buffer[row_ix, col_ix]), - material=self._material, - row_chunk_ix=row_ix, - col_chunk_ix=col_ix, - ) + # iterate through each texture chunk and create + # an _ImageTIle, offset the tile using the data indices + for texture, chunk_index, data_slice in self._data: + # row and column start index for this chunk + data_row_start = data_slice[0].start + data_col_start = data_slice[1].start + + # create an ImageTile using the texture for this chunk + img = _ImageTile( + geometry=pygfx.Geometry(grid=texture), + material=self._material, + data_slice=(data_row_start, data_col_start), # used to parse pick_info + chunk_index=chunk_index + ) - img.world.y = row_ix * WGPU_MAX_TEXTURE_SIZE - img.world.x = col_ix * WGPU_MAX_TEXTURE_SIZE + # offset tile position using the indices from the big data array + # that correspond to this chunk + img.world.x = data_col_start + img.world.y = data_row_start - world_object.add(img) + world_object.add(img) self._set_world_object(world_object) def reset_vmin_vmax(self): + """ + Reset the vmin, vmax by estimating it from the data + + Returns + ------- + None + + """ + vmin, vmax = quick_min_max(self._data.value) self.vmin = vmin self.vmax = vmax From f141d2317ddb4ebf416cf0eb72017521fbad8878 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Jun 2024 01:18:30 -0400 Subject: [PATCH 092/196] basic texture tests --- tests/test_texture_array.py | 167 ++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 tests/test_texture_array.py diff --git a/tests/test_texture_array.py b/tests/test_texture_array.py new file mode 100644 index 000000000..78b901aa7 --- /dev/null +++ b/tests/test_texture_array.py @@ -0,0 +1,167 @@ +import numpy as np +from numpy import testing as npt + +import pygfx + +from fastplotlib.graphics._features import TextureArray, WGPU_MAX_TEXTURE_SIZE + + +def make_data(n_rows: int, n_cols: int) -> np.ndarray: + """ + Makes a 2D array where the amplitude of the sine wave + is increasing along the y-direction (along rows), and + the wavelength is increasing along the x-axis (columns) + """ + xs = np.linspace(0, 1_000, n_cols) + + sine = np.sin(np.sqrt(xs)) + + return np.vstack([sine * i for i in range(n_rows)]).astype(np.float32) + + +def check_texture_array( + data: np.ndarray, + ta: TextureArray, + buffer_size: int, + buffer_shape: tuple[int, int], + row_indices_size: int, + col_indices_size: int, + row_indices_values: np.ndarray, + col_indices_values: np.ndarray, +): + + npt.assert_almost_equal(ta.value, data) + + assert ta.buffer.size == buffer_size + assert ta.buffer.shape == buffer_shape + + assert all([isinstance(texture, pygfx.Texture) for texture in ta.buffer.ravel()]) + + assert ta.row_indices.size == row_indices_size + assert ta.col_indices.size == col_indices_size + npt.assert_array_equal(ta.row_indices, row_indices_values) + npt.assert_array_equal(ta.col_indices, col_indices_values) + + # make sure chunking is correct + for texture, chunk_index, data_slice in ta: + assert ta.buffer[chunk_index] is texture + chunk_row, chunk_col = chunk_index + + data_row_start_index = chunk_row * WGPU_MAX_TEXTURE_SIZE + data_col_start_index = chunk_col * WGPU_MAX_TEXTURE_SIZE + + data_row_stop_index = min(data.shape[0] - 1, data_row_start_index + WGPU_MAX_TEXTURE_SIZE) + data_col_stop_index = min(data.shape[1] - 1, data_col_start_index + WGPU_MAX_TEXTURE_SIZE) + + row_slice = slice(data_row_start_index, data_row_stop_index) + col_slice = slice(data_col_start_index, data_col_stop_index) + + assert data_slice == (row_slice, col_slice) + + +def check_set_slice(data, ta, row_slice, col_slice): + ta[row_slice, col_slice] = 1 + npt.assert_almost_equal(ta[row_slice, col_slice], 1) + + # make sure other vals unchanged + npt.assert_almost_equal(ta[:row_slice.start], data[:row_slice.start]) + npt.assert_almost_equal(ta[row_slice.stop:], data[row_slice.stop:]) + npt.assert_almost_equal(ta[:, :col_slice.start], data[:, :col_slice.start]) + npt.assert_almost_equal(ta[:, col_slice.stop:], data[:, col_slice.stop:]) + + +def test_small_texture(): + # tests TextureArray with dims that requires only 1 texture + data = make_data(1_000, 1_000) + + ta = TextureArray(data) + + check_texture_array( + data=data, + ta=ta, + buffer_size=1, + buffer_shape=(1, 1), + row_indices_size=1, + col_indices_size=1, + row_indices_values=np.array([0]), + col_indices_values=np.array([0]) + ) + + check_set_slice(data, ta, slice(50, 200), slice(600, 800)) + + +def test_texture_at_limit(): + # tests TextureArray with data that is 8192 x 8192 + data = make_data(WGPU_MAX_TEXTURE_SIZE, WGPU_MAX_TEXTURE_SIZE) + + ta = TextureArray(data) + + check_texture_array( + data, + ta=ta, + buffer_size=1, + buffer_shape=(1, 1), + row_indices_size=1, + col_indices_size=1, + row_indices_values=np.array([0]), + col_indices_values=np.array([0]) + ) + + check_set_slice(data, ta, slice(5000, 8000), slice(2000, 3000)) + + +def test_wide(): + data = make_data(10_000, 20_000) + + ta = TextureArray(data) + + check_texture_array( + data, + ta=ta, + buffer_size=6, + buffer_shape=(2, 3), + row_indices_size=2, + col_indices_size=3, + row_indices_values=np.array([0, 8192]), + col_indices_values=np.array([0, 8192, 16384]) + ) + + check_set_slice(data, ta, slice(6_000, 9_000), slice(12_000, 18_000)) + + +def test_tall(): + data = make_data(20_000, 10_000) + + ta = TextureArray(data) + + check_texture_array( + data, + ta=ta, + buffer_size=6, + buffer_shape=(3, 2), + row_indices_size=3, + col_indices_size=2, + row_indices_values=np.array([0, 8192, 16384]), + col_indices_values=np.array([0, 8192]) + ) + + check_set_slice(data, ta, slice(12_000, 18_000), slice(6_000, 9_000)) + + +def test_square(): + data = make_data(20_000, 20_000) + + ta = TextureArray(data) + + check_texture_array( + data, + ta=ta, + buffer_size=9, + buffer_shape=(3, 3), + row_indices_size=3, + col_indices_size=3, + row_indices_values=np.array([0, 8192, 16384]), + col_indices_values=np.array([0, 8192, 16384]) + ) + + check_set_slice(data, ta, slice(12_000, 18_000), slice(16_000, 19_000)) From 1f762242b2a6e72d95679828fd0fd02858916ba9 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Jun 2024 01:18:58 -0400 Subject: [PATCH 093/196] bugfix, cleanup --- fastplotlib/graphics/image.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 5ba9daec1..4097a5719 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -15,7 +15,6 @@ ImageVmax, ImageInterpolation, ImageCmapInterpolation, - WGPU_MAX_TEXTURE_SIZE, ) @@ -200,9 +199,9 @@ def __init__( # use cmap if not RGB if self._data.value.ndim == 2: - _map = None - else: _map = self._cmap.texture + else: + _map = None # one common material is used for every Texture chunk self._material = pygfx.ImageBasicMaterial( @@ -216,18 +215,19 @@ def __init__( # iterate through each texture chunk and create # an _ImageTIle, offset the tile using the data indices for texture, chunk_index, data_slice in self._data: - # row and column start index for this chunk - data_row_start = data_slice[0].start - data_col_start = data_slice[1].start # create an ImageTile using the texture for this chunk img = _ImageTile( geometry=pygfx.Geometry(grid=texture), material=self._material, - data_slice=(data_row_start, data_col_start), # used to parse pick_info + data_slice=data_slice, # used to parse pick_info chunk_index=chunk_index ) + # row and column start index for this chunk + data_row_start = data_slice[0].start + data_col_start = data_slice[1].start + # offset tile position using the indices from the big data array # that correspond to this chunk img.world.x = data_col_start From 273962f8b8ca672a1b66acaa1b6199716e0c2e70 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Jun 2024 02:13:09 -0400 Subject: [PATCH 094/196] image graphic tests --- tests/test_image_graphic.py | 138 ++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tests/test_image_graphic.py diff --git a/tests/test_image_graphic.py b/tests/test_image_graphic.py new file mode 100644 index 000000000..600a51245 --- /dev/null +++ b/tests/test_image_graphic.py @@ -0,0 +1,138 @@ +import numpy as np +from numpy import testing as npt +import imageio.v3 as iio + +import fastplotlib as fpl +from fastplotlib.utils import make_colors + +GRAY_IMAGE = iio.imread("imageio:camera.png") +RGB_IMAGE = iio.imread("imageio:astronaut.png") + + +COFFEE_IMAGE = iio.imread("imageio:coffee.png") + +# image cmap, vmin, vmax, interpolations +# new screenshot tests too for these when in graphics + + +def check_set_slice( + data: np.ndarray, + image_graphic: fpl.ImageGraphic, + row_slice: slice, + col_slice: slice, +): + image_graphic.data[row_slice, col_slice] = 1 + data_values = image_graphic.data.value + npt.assert_almost_equal(data_values[row_slice, col_slice], 1) + + # make sure other vals unchanged + npt.assert_almost_equal(data_values[:row_slice.start], data[:row_slice.start]) + npt.assert_almost_equal(data_values[row_slice.stop:], data[row_slice.stop:]) + npt.assert_almost_equal(data_values[:, :col_slice.start], data[:, :col_slice.start]) + npt.assert_almost_equal(data_values[:, col_slice.stop:], data[:, col_slice.stop:]) + + +def test_gray(): + fig = fpl.Figure() + ig = fig[0, 0].add_image(GRAY_IMAGE) + assert isinstance(ig, fpl.ImageGraphic) + + npt.assert_almost_equal(ig.data.value, GRAY_IMAGE) + + ig.cmap = "viridis" + assert ig.cmap == "viridis" + + new_colors = make_colors(256, "viridis") + for child in ig.world_object.children: + npt.assert_almost_equal(child.material.map.data, new_colors) + + ig.cmap = "jet" + assert ig.cmap == "jet" + + new_colors = make_colors(256, "jet") + for child in ig.world_object.children: + npt.assert_almost_equal(child.material.map.data, new_colors) + + assert ig.interpolation == "nearest" + for child in ig.world_object.children: + assert child.material.interpolation == "nearest" + + ig.interpolation = "linear" + assert ig.interpolation == "linear" + for child in ig.world_object.children: + assert child.material.interpolation == "linear" + + assert ig.cmap_interpolation == "linear" + for child in ig.world_object.children: + assert child.material.map_interpolation == "linear" + + ig.cmap_interpolation = "nearest" + assert ig.cmap_interpolation == "nearest" + for child in ig.world_object.children: + assert child.material.map_interpolation == "nearest" + + npt.assert_almost_equal(ig.vmin, GRAY_IMAGE.min()) + npt.assert_almost_equal(ig.vmax, GRAY_IMAGE.max()) + + ig.vmin = 50 + assert ig.vmin == 50 + for child in ig.world_object.children: + assert child.material.clim == (50, ig.vmax) + + ig.vmax = 100 + assert ig.vmax == 100 + for child in ig.world_object.children: + assert child.material.clim == (ig.vmin, 100) + + check_set_slice(GRAY_IMAGE, ig, slice(100, 200), slice(200, 300)) + + +def test_rgb(): + fig = fpl.Figure() + ig = fig[0, 0].add_image(RGB_IMAGE) + assert isinstance(ig, fpl.ImageGraphic) + + npt.assert_almost_equal(ig.data.value, RGB_IMAGE) + + assert ig.interpolation == "nearest" + for child in ig.world_object.children: + assert child.material.interpolation == "nearest" + + ig.interpolation = "linear" + assert ig.interpolation == "linear" + for child in ig.world_object.children: + assert child.material.interpolation == "linear" + + npt.assert_almost_equal(ig.vmin, RGB_IMAGE.min()) + npt.assert_almost_equal(ig.vmax, RGB_IMAGE.max()) + + ig.vmin = 50 + assert ig.vmin == 50 + for child in ig.world_object.children: + assert child.material.clim == (50, ig.vmax) + + ig.vmax = 100 + assert ig.vmax == 100 + for child in ig.world_object.children: + assert child.material.clim == (ig.vmin, 100) + + check_set_slice(RGB_IMAGE, ig, slice(100, 200), slice(200, 300)) + + +def test_rgba(): + rgba = np.zeros(shape=(*COFFEE_IMAGE.shape[:2], 4), dtype=np.float32) + + fig = fpl.Figure() + ig = fig[0, 0].add_image(rgba) + assert isinstance(ig, fpl.ImageGraphic) + + npt.assert_almost_equal(ig.data.value, rgba) + + # fancy indexing + # set the blue values of some pixels with an alpha > 1 + ig.data[COFFEE_IMAGE[:, :, -1] > 200] = np.array([0.0, 0.0, 1.0, 0.6]).astype(np.float32) + + rgba[COFFEE_IMAGE[:, :, -1] > 200] = np.array([0.0, 0.0, 1.0, 0.6]).astype(np.float32) + + # check that fancy indexing works + npt.assert_almost_equal(ig.data.value, rgba) From 61db9414580d0d865a8efde7587fd8129f6ed222 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Jun 2024 02:13:29 -0400 Subject: [PATCH 095/196] type annot --- fastplotlib/graphics/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 4097a5719..9d23946fc 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -68,7 +68,7 @@ class ImageGraphic(Graphic): features = {"data", "cmap", "vmin", "vmax"} @property - def data(self) -> NDArray: + def data(self) -> TextureArray: """Get or set the image data""" return self._data From aafa45260253c1d5670af6426bd540325c79c055 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Jun 2024 02:24:22 -0400 Subject: [PATCH 096/196] test stuff --- tests/test_image_graphic.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_image_graphic.py b/tests/test_image_graphic.py index 600a51245..70a25a527 100644 --- a/tests/test_image_graphic.py +++ b/tests/test_image_graphic.py @@ -86,6 +86,10 @@ def test_gray(): check_set_slice(GRAY_IMAGE, ig, slice(100, 200), slice(200, 300)) + # test setting all values + ig.data = 1 + npt.assert_almost_equal(ig.data.value, 1) + def test_rgb(): fig = fpl.Figure() From fa4b2aca15c6f4e400f052271cb84df4c34ce881 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Jun 2024 03:19:45 -0400 Subject: [PATCH 097/196] tests for common and visible kwarg for Graphic --- fastplotlib/graphics/_base.py | 5 +- tests/test_common_features.py | 154 ++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 tests/test_common_features.py diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 16e85c63d..6608b6ade 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -123,6 +123,7 @@ def __init__( name: str = None, offset: np.ndarray | list | tuple = (0.0, 0.0, 0.0), rotation: np.ndarray | list | tuple = (0.0, 0.0, 0.0, 1.0), + visible: bool = True, metadata: Any = None, ): """ @@ -164,7 +165,7 @@ def __init__( self._deleted = Deleted(False) self._rotation = Rotation(rotation) self._offset = Offset(offset) - self._visible = Visible(True) + self._visible = Visible(visible) self._block_events = False @property @@ -176,6 +177,8 @@ def world_object(self) -> pygfx.WorldObject: def _set_world_object(self, wo: pygfx.WorldObject): WORLD_OBJECTS[self._fpl_address] = wo + self.world_object.visible = self.visible + # set offset if it's not (0., 0., 0.) if not all(self.world_object.world.position == self.offset): self.offset = self.offset diff --git a/tests/test_common_features.py b/tests/test_common_features.py new file mode 100644 index 000000000..49763cb27 --- /dev/null +++ b/tests/test_common_features.py @@ -0,0 +1,154 @@ +import numpy +import numpy as np +from numpy import testing as npt +import pytest + +import fastplotlib as fpl +from fastplotlib.graphics._features import FeatureEvent, Name, Offset, Rotation, Visible + + +def make_graphic(kind: str, **kwargs): + match kind: + case "image": + return fpl.ImageGraphic(np.random.rand(10, 10), **kwargs) + case "line": + return fpl.LineGraphic(np.random.rand(10), **kwargs) + case "scatter": + return fpl.ScatterGraphic( + np.column_stack([np.random.rand(10), np.random.rand(10)]), + **kwargs + ) + case "text": + return fpl.TextGraphic("bah", **kwargs) + + +graphic_kinds = [ + "image", + "line", + "scatter", + "text", +] + + +RETURN_EVENT_VALUE: FeatureEvent = None + + +def return_event(ev: FeatureEvent): + global RETURN_EVENT_VALUE + RETURN_EVENT_VALUE = ev + + +@pytest.mark.parametrize("graphic", [make_graphic(k) for k in graphic_kinds]) +def test_name(graphic): + assert graphic.name is None + + graphic.add_event_handler(return_event, "name") + + graphic.name = "new_name" + + assert graphic.name == "new_name" + + global RETURN_EVENT_VALUE + + assert RETURN_EVENT_VALUE.type == "name" + assert RETURN_EVENT_VALUE.graphic is graphic + assert RETURN_EVENT_VALUE.target is graphic.world_object + assert RETURN_EVENT_VALUE.info["value"] == "new_name" + + +@pytest.mark.parametrize("graphic", [make_graphic(k, name="init_name") for k in graphic_kinds]) +def test_name_init(graphic): + assert graphic.name == "init_name" + + graphic.name = "new_name" + + assert graphic.name == "new_name" + + +@pytest.mark.parametrize("graphic", [make_graphic(k) for k in graphic_kinds]) +def test_offset(graphic): + npt.assert_almost_equal(graphic.offset, (0., 0., 0.)) + npt.assert_almost_equal(graphic.world_object.world.position, (0., 0., 0.)) + + graphic.add_event_handler(return_event, "offset") + + graphic.offset = (1., 2., 3.) + + npt.assert_almost_equal(graphic.offset, (1., 2., 3.)) + npt.assert_almost_equal(graphic.world_object.world.position, (1., 2., 3.)) + + global RETURN_EVENT_VALUE + + assert RETURN_EVENT_VALUE.type == "offset" + assert RETURN_EVENT_VALUE.graphic is graphic + assert RETURN_EVENT_VALUE.target is graphic.world_object + npt.assert_almost_equal(RETURN_EVENT_VALUE.info["value"], (1., 2., 3.)) + + +@pytest.mark.parametrize("graphic", [make_graphic(k, offset=(3., 4., 5.)) for k in graphic_kinds]) +def test_offset_init(graphic): + npt.assert_almost_equal(graphic.offset, (3., 4., 5.)) + npt.assert_almost_equal(graphic.world_object.world.position, (3., 4., 5.)) + + graphic.offset = (6., 7., 8.) + + npt.assert_almost_equal(graphic.offset, (6., 7., 8.)) + npt.assert_almost_equal(graphic.world_object.world.position, (6., 7., 8.)) + + +@pytest.mark.parametrize("graphic", [make_graphic(k) for k in graphic_kinds]) +def test_rotation(graphic): + npt.assert_almost_equal(graphic.rotation, (0, 0, 0, 1)) + npt.assert_almost_equal(graphic.world_object.world.rotation, (0, 0, 0, 1)) + + graphic.add_event_handler(return_event, "rotation") + + graphic.rotation = (0., 0., 0.30001427, 0.95393471) + + npt.assert_almost_equal(graphic.rotation, (0., 0., 0.30001427, 0.95393471)) + npt.assert_almost_equal(graphic.world_object.world.rotation, (0., 0., 0.30001427, 0.95393471)) + + global RETURN_EVENT_VALUE + + assert RETURN_EVENT_VALUE.type == "rotation" + assert RETURN_EVENT_VALUE.graphic is graphic + assert RETURN_EVENT_VALUE.target is graphic.world_object + npt.assert_almost_equal(RETURN_EVENT_VALUE.info["value"], (0., 0., 0.30001427, 0.95393471)) + + +@pytest.mark.parametrize("graphic", [make_graphic(k, rotation=(0., 0., 0.30001427, 0.95393471)) for k in graphic_kinds]) +def test_rotation(graphic): + npt.assert_almost_equal(graphic.rotation, (0., 0., 0.30001427, 0.95393471)) + npt.assert_almost_equal(graphic.world_object.world.rotation, (0., 0., 0.30001427, 0.95393471)) + + graphic.rotation = (0, 0.0, 0.6, 0.8) + + npt.assert_almost_equal(graphic.rotation, (0, 0.0, 0.6, 0.8)) + npt.assert_almost_equal(graphic.world_object.world.rotation, (0, 0.0, 0.6, 0.8)) + + +@pytest.mark.parametrize("graphic", [make_graphic(k)for k in graphic_kinds]) +def test_visible(graphic): + assert graphic.visible is True + assert graphic.world_object.visible is True + + graphic.add_event_handler(return_event, "rotation") + + graphic.visible = False + assert graphic.visible is False + assert graphic.world_object.visible is False + + assert RETURN_EVENT_VALUE.type == "visible" + assert RETURN_EVENT_VALUE.graphic is graphic + assert RETURN_EVENT_VALUE.target is graphic.world_object + assert RETURN_EVENT_VALUE.info["value"] == False + + +@pytest.mark.parametrize("graphic", [make_graphic(k, visible=False) for k in graphic_kinds]) +def test_visible(graphic): + assert graphic.visible is False + assert graphic.world_object.visible is False + + graphic.visible = True + assert graphic.visible is True + assert graphic.world_object.visible is True From 2c6843bcbf13a70ed64078ec27eebf121191dfdf Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Jun 2024 19:53:38 -0400 Subject: [PATCH 098/196] bugfix --- fastplotlib/widgets/histogram_lut.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 44bddd9b0..db1161145 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -75,7 +75,7 @@ def __init__( self._text_vmin = TextGraphic( text=vmin_str, font_size=16, - position=(0, 0), + offset=(0, 0, 0), anchor="top-left", outline_color="black", outline_thickness=1, @@ -86,7 +86,7 @@ def __init__( self._text_vmax = TextGraphic( text=vmax_str, font_size=16, - position=(0, 0), + offset=(0, 0, 0), anchor="bottom-left", outline_color="black", outline_thickness=1, @@ -269,7 +269,6 @@ def set_data(self, data, reset_vmin_vmax: bool = True): bounds = (edges[0], edges[-1]) limits = (edges_flanked[0], edges_flanked[-11]) origin = (hist_scaled.max() / 2, 0) - # self.linear_region.fill.world.position = (*origin, -2) if reset_vmin_vmax: # reset according to the new data From 9ca2c2a4c413b60eb98e63f1ba873ebdde453644 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Jun 2024 21:03:16 -0400 Subject: [PATCH 099/196] test remove event hanlders common fea --- tests/test_common_features.py | 113 +++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/tests/test_common_features.py b/tests/test_common_features.py index 49763cb27..7eff0aad2 100644 --- a/tests/test_common_features.py +++ b/tests/test_common_features.py @@ -31,6 +31,7 @@ def make_graphic(kind: str, **kwargs): RETURN_EVENT_VALUE: FeatureEvent = None +DECORATED_EVENT_VALUE: FeatureEvent = None def return_event(ev: FeatureEvent): @@ -55,6 +56,33 @@ def test_name(graphic): assert RETURN_EVENT_VALUE.target is graphic.world_object assert RETURN_EVENT_VALUE.info["value"] == "new_name" + # check removing event handler + RETURN_EVENT_VALUE = None + graphic.remove_event_handler(return_event, "name") + assert len(graphic._event_handlers["name"]) == 0 + + graphic.name = "new_name2" + + assert RETURN_EVENT_VALUE is None + assert graphic.name == "new_name2" + + # check adding event with decorator + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = None + + @graphic.add_event_handler("name") + def decorated_handler(ev): + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = ev + + graphic.name = "test_dec" + assert graphic.name == "test_dec" + + assert DECORATED_EVENT_VALUE.type == "name" + assert DECORATED_EVENT_VALUE.graphic is graphic + assert DECORATED_EVENT_VALUE.target is graphic.world_object + assert DECORATED_EVENT_VALUE.info["value"] == "test_dec" + @pytest.mark.parametrize("graphic", [make_graphic(k, name="init_name") for k in graphic_kinds]) def test_name_init(graphic): @@ -84,6 +112,33 @@ def test_offset(graphic): assert RETURN_EVENT_VALUE.target is graphic.world_object npt.assert_almost_equal(RETURN_EVENT_VALUE.info["value"], (1., 2., 3.)) + # check removing event handler + RETURN_EVENT_VALUE = None + graphic.remove_event_handler(return_event, "offset") + assert len(graphic._event_handlers["offset"]) == 0 + + graphic.offset = (4, 5, 6) + + assert RETURN_EVENT_VALUE is None + npt.assert_almost_equal(graphic.offset, (4., 5., 6.)) + + # check adding event with decorator + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = None + + @graphic.add_event_handler("offset") + def decorated_handler(ev): + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = ev + + graphic.offset = (7, 8, 9) + npt.assert_almost_equal(graphic.offset, (7., 8., 9.)) + + assert DECORATED_EVENT_VALUE.type == "offset" + assert DECORATED_EVENT_VALUE.graphic is graphic + assert DECORATED_EVENT_VALUE.target is graphic.world_object + assert DECORATED_EVENT_VALUE.info["value"] == (7., 8., 9.) + @pytest.mark.parametrize("graphic", [make_graphic(k, offset=(3., 4., 5.)) for k in graphic_kinds]) def test_offset_init(graphic): @@ -115,6 +170,33 @@ def test_rotation(graphic): assert RETURN_EVENT_VALUE.target is graphic.world_object npt.assert_almost_equal(RETURN_EVENT_VALUE.info["value"], (0., 0., 0.30001427, 0.95393471)) + # check removing event handler + RETURN_EVENT_VALUE = None + graphic.remove_event_handler(return_event, "rotation") + assert len(graphic._event_handlers["rotation"]) == 0 + + graphic.rotation = (0, 0, 0, 1) + + assert RETURN_EVENT_VALUE is None + npt.assert_almost_equal(graphic.rotation, (0, 0, 0, 1)) + + # check adding event with decorator + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = None + + @graphic.add_event_handler("rotation") + def decorated_handler(ev): + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = ev + + graphic.rotation = (0, 0, 0.6, 0.8) + npt.assert_almost_equal(graphic.rotation, (0, 0, 0.6, 0.8)) + + assert DECORATED_EVENT_VALUE.type == "rotation" + assert DECORATED_EVENT_VALUE.graphic is graphic + assert DECORATED_EVENT_VALUE.target is graphic.world_object + assert DECORATED_EVENT_VALUE.info["value"] == (0, 0, 0.6, 0.8) + @pytest.mark.parametrize("graphic", [make_graphic(k, rotation=(0., 0., 0.30001427, 0.95393471)) for k in graphic_kinds]) def test_rotation(graphic): @@ -138,10 +220,39 @@ def test_visible(graphic): assert graphic.visible is False assert graphic.world_object.visible is False + global RETURN_EVENT_VALUE + assert RETURN_EVENT_VALUE.type == "visible" assert RETURN_EVENT_VALUE.graphic is graphic assert RETURN_EVENT_VALUE.target is graphic.world_object - assert RETURN_EVENT_VALUE.info["value"] == False + assert RETURN_EVENT_VALUE.info["value"] is False + + # check removing event handler + RETURN_EVENT_VALUE = None + graphic.remove_event_handler(return_event, "visible") + assert len(graphic._event_handlers["visible"]) == 0 + + graphic.visible = True + + assert RETURN_EVENT_VALUE is None + assert graphic.visible is True + + # check adding event with decorator + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = None + + @graphic.add_event_handler("visible") + def decorated_handler(ev): + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = ev + + graphic.visible = False + assert graphic.visible is False + + assert DECORATED_EVENT_VALUE.type == "visible" + assert DECORATED_EVENT_VALUE.graphic is graphic + assert DECORATED_EVENT_VALUE.target is graphic.world_object + assert DECORATED_EVENT_VALUE.info["value"] is False @pytest.mark.parametrize("graphic", [make_graphic(k, visible=False) for k in graphic_kinds]) From 72824ebf5166ff58747d03ec9b42fa7190b68c9f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Jun 2024 21:12:03 -0400 Subject: [PATCH 100/196] rename --- ...ta_buffer_manager.py => test_positions_data_buffer_manager.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_points_data_buffer_manager.py => test_positions_data_buffer_manager.py} (100%) diff --git a/tests/test_points_data_buffer_manager.py b/tests/test_positions_data_buffer_manager.py similarity index 100% rename from tests/test_points_data_buffer_manager.py rename to tests/test_positions_data_buffer_manager.py From 105cd86da5ec2a977f615cd757104730a0b88372 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Jun 2024 21:12:12 -0400 Subject: [PATCH 101/196] start test positions graphics --- tests/test_positions_graphics.py | 46 ++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/test_positions_graphics.py diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py new file mode 100644 index 000000000..77b153887 --- /dev/null +++ b/tests/test_positions_graphics.py @@ -0,0 +1,46 @@ +import numpy as np +from numpy import testing as npt + +import pygfx + +import fastplotlib as fpl +from fastplotlib.graphics._features import VertexPositions, VertexColors, VertexCmap, UniformColor, UniformSizes, PointsSizesFeature + + +# TODO: use same functions to generate data and colors for graphics as the buffer test modules + + +def test_data_slice(): + pass + + +def test_color_slice(): + pass + + +def test_cmap_slice(): + pass + + +def test_sizes_slice(): + pass + + +def test_change_thickness(): + pass + + +def test_uniform_color(): + pass + + +def test_uniform_size(): + pass + + +def test_create_line(): + pass + + +def test_create_scatter(): + pass From feabe7a78cbc77e4f0848a2fa5ff7a5d615cb7a0 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Jun 2024 00:24:25 -0400 Subject: [PATCH 102/196] test progress --- tests/test_colors_buffer_manager.py | 62 ++++++------- tests/test_common_features.py | 71 +++++++++------ tests/test_figure.py | 96 +++++++++++--------- tests/test_image_graphic.py | 24 +++-- tests/test_positions_data_buffer_manager.py | 76 ++++++++-------- tests/test_positions_graphics.py | 98 ++++++++++++++++++++- tests/test_sizes_buffer_manager.py | 10 ++- tests/test_texture_array.py | 42 +++++---- tests/utils.py | 44 ++++++++- 9 files changed, 343 insertions(+), 180 deletions(-) diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py index 3479fcd59..4d8cbc242 100644 --- a/tests/test_colors_buffer_manager.py +++ b/tests/test_colors_buffer_manager.py @@ -5,18 +5,7 @@ import pygfx from fastplotlib.graphics._features import VertexColors -from .utils import generate_slice_indices, assert_pending_uploads - - -def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple]: - color = pygfx.Color(name) - - s = name - a = np.array(color) - l = list(color) - t = tuple(color) - - return [s, a, l, t] +from .utils import generate_slice_indices, assert_pending_uploads, generate_color_inputs def make_colors_buffer() -> VertexColors: @@ -24,7 +13,14 @@ def make_colors_buffer() -> VertexColors: return colors -@pytest.mark.parametrize("color_input", [*generate_color_inputs("r"), *generate_color_inputs("g"), *generate_color_inputs("b")]) +@pytest.mark.parametrize( + "color_input", + [ + *generate_color_inputs("r"), + *generate_color_inputs("g"), + *generate_color_inputs("b"), + ], +) def test_create_buffer(color_input): colors = VertexColors(colors=color_input, n_colors=10) truth = np.repeat([pygfx.Color(color_input)], 10, axis=0) @@ -38,23 +34,25 @@ def test_int(): colors.buffer._gfx_pending_uploads.clear() colors[3] = "r" - npt.assert_almost_equal(colors[3], [1., 0., 0., 1.]) + npt.assert_almost_equal(colors[3], [1.0, 0.0, 0.0, 1.0]) assert colors.buffer._gfx_pending_uploads[-1] == (3, 1) - colors[6] = [0., 1., 1., 1.] - npt.assert_almost_equal(colors[6], [0., 1., 1., 1.]) + colors[6] = [0.0, 1.0, 1.0, 1.0] + npt.assert_almost_equal(colors[6], [0.0, 1.0, 1.0, 1.0]) - colors[7] = (0., 1., 1., 1.) - npt.assert_almost_equal(colors[6], [0., 1., 1., 1.]) + colors[7] = (0.0, 1.0, 1.0, 1.0) + npt.assert_almost_equal(colors[6], [0.0, 1.0, 1.0, 1.0]) colors[8] = np.array([1, 0, 1, 1]) - npt.assert_almost_equal(colors[8], [1., 0., 1., 1.]) + npt.assert_almost_equal(colors[8], [1.0, 0.0, 1.0, 1.0]) colors[2] = [1, 0, 1, 0.5] - npt.assert_almost_equal(colors[2], [1., 0., 1., 0.5]) + npt.assert_almost_equal(colors[2], [1.0, 0.0, 1.0, 0.5]) -@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(0, 16)]) +@pytest.mark.parametrize( + "slice_method", [generate_slice_indices(i) for i in range(0, 16)] +) def test_tuple(slice_method): # setting entire array manually colors = make_colors_buffer() @@ -69,44 +67,46 @@ def test_tuple(slice_method): npt.assert_almost_equal(colors[indices], truth) # check others are not modified - others_truth = np.repeat([[1., 1., 1., 1.]], repeats=len(others), axis=0) + others_truth = np.repeat([[1.0, 1.0, 1.0, 1.0]], repeats=len(others), axis=0) npt.assert_almost_equal(colors[others], others_truth) # reset colors[:] = (1, 1, 1, 1) - npt.assert_almost_equal(colors[:], np.repeat([[1., 1., 1., 1.]], 10, axis=0)) + npt.assert_almost_equal(colors[:], np.repeat([[1.0, 1.0, 1.0, 1.0]], 10, axis=0)) # set just R values colors[s, 0] = 0.5 - truth = np.repeat([[0.5, 1., 1., 1.]], repeats=len(indices), axis=0) + truth = np.repeat([[0.5, 1.0, 1.0, 1.0]], repeats=len(indices), axis=0) # check others not modified npt.assert_almost_equal(colors[indices], truth) npt.assert_almost_equal(colors[others], others_truth) # reset colors[:] = (1, 1, 1, 1) - npt.assert_almost_equal(colors[:], np.repeat([[1., 1., 1., 1.]], 10, axis=0)) + npt.assert_almost_equal(colors[:], np.repeat([[1.0, 1.0, 1.0, 1.0]], 10, axis=0)) # set green and blue colors[s, 1:-1] = 0.7 - truth = np.repeat([[1., 0.7, 0.7, 1.0]], repeats=len(indices), axis=0) + truth = np.repeat([[1.0, 0.7, 0.7, 1.0]], repeats=len(indices), axis=0) npt.assert_almost_equal(colors[indices], truth) npt.assert_almost_equal(colors[others], others_truth) # reset colors[:] = (1, 1, 1, 1) - npt.assert_almost_equal(colors[:], np.repeat([[1., 1., 1., 1.]], 10, axis=0)) + npt.assert_almost_equal(colors[:], np.repeat([[1.0, 1.0, 1.0, 1.0]], 10, axis=0)) # set only alpha colors[s, -1] = 0.2 - truth = np.repeat([[1., 1., 1., 0.2]], repeats=len(indices), axis=0) + truth = np.repeat([[1.0, 1.0, 1.0, 0.2]], repeats=len(indices), axis=0) npt.assert_almost_equal(colors[indices], truth) npt.assert_almost_equal(colors[others], others_truth) @pytest.mark.parametrize("color_input", generate_color_inputs("red")) # skip testing with int since that results in shape [1, 4] with np.repeat, int tested in independent unit test -@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(1, 16)]) +@pytest.mark.parametrize( + "slice_method", [generate_slice_indices(i) for i in range(1, 16)] +) def test_slice(color_input, slice_method: dict): # slicing only first dim colors = make_colors_buffer() @@ -130,9 +130,9 @@ def test_slice(color_input, slice_method: dict): assert_pending_uploads(colors.buffer, offset, size) # check that others are not touched - others_truth = np.repeat([[1., 1., 1., 1.]], repeats=len(others), axis=0) + others_truth = np.repeat([[1.0, 1.0, 1.0, 1.0]], repeats=len(others), axis=0) npt.assert_almost_equal(colors[others], others_truth) # reset colors[:] = (1, 1, 1, 1) - npt.assert_almost_equal(colors[:], np.repeat([[1., 1., 1., 1.]], 10, axis=0)) + npt.assert_almost_equal(colors[:], np.repeat([[1.0, 1.0, 1.0, 1.0]], 10, axis=0)) diff --git a/tests/test_common_features.py b/tests/test_common_features.py index 7eff0aad2..332ac71ae 100644 --- a/tests/test_common_features.py +++ b/tests/test_common_features.py @@ -15,8 +15,7 @@ def make_graphic(kind: str, **kwargs): return fpl.LineGraphic(np.random.rand(10), **kwargs) case "scatter": return fpl.ScatterGraphic( - np.column_stack([np.random.rand(10), np.random.rand(10)]), - **kwargs + np.column_stack([np.random.rand(10), np.random.rand(10)]), **kwargs ) case "text": return fpl.TextGraphic("bah", **kwargs) @@ -84,7 +83,9 @@ def decorated_handler(ev): assert DECORATED_EVENT_VALUE.info["value"] == "test_dec" -@pytest.mark.parametrize("graphic", [make_graphic(k, name="init_name") for k in graphic_kinds]) +@pytest.mark.parametrize( + "graphic", [make_graphic(k, name="init_name") for k in graphic_kinds] +) def test_name_init(graphic): assert graphic.name == "init_name" @@ -95,22 +96,22 @@ def test_name_init(graphic): @pytest.mark.parametrize("graphic", [make_graphic(k) for k in graphic_kinds]) def test_offset(graphic): - npt.assert_almost_equal(graphic.offset, (0., 0., 0.)) - npt.assert_almost_equal(graphic.world_object.world.position, (0., 0., 0.)) + npt.assert_almost_equal(graphic.offset, (0.0, 0.0, 0.0)) + npt.assert_almost_equal(graphic.world_object.world.position, (0.0, 0.0, 0.0)) graphic.add_event_handler(return_event, "offset") - graphic.offset = (1., 2., 3.) + graphic.offset = (1.0, 2.0, 3.0) - npt.assert_almost_equal(graphic.offset, (1., 2., 3.)) - npt.assert_almost_equal(graphic.world_object.world.position, (1., 2., 3.)) + npt.assert_almost_equal(graphic.offset, (1.0, 2.0, 3.0)) + npt.assert_almost_equal(graphic.world_object.world.position, (1.0, 2.0, 3.0)) global RETURN_EVENT_VALUE assert RETURN_EVENT_VALUE.type == "offset" assert RETURN_EVENT_VALUE.graphic is graphic assert RETURN_EVENT_VALUE.target is graphic.world_object - npt.assert_almost_equal(RETURN_EVENT_VALUE.info["value"], (1., 2., 3.)) + npt.assert_almost_equal(RETURN_EVENT_VALUE.info["value"], (1.0, 2.0, 3.0)) # check removing event handler RETURN_EVENT_VALUE = None @@ -120,7 +121,7 @@ def test_offset(graphic): graphic.offset = (4, 5, 6) assert RETURN_EVENT_VALUE is None - npt.assert_almost_equal(graphic.offset, (4., 5., 6.)) + npt.assert_almost_equal(graphic.offset, (4.0, 5.0, 6.0)) # check adding event with decorator global DECORATED_EVENT_VALUE @@ -132,23 +133,25 @@ def decorated_handler(ev): DECORATED_EVENT_VALUE = ev graphic.offset = (7, 8, 9) - npt.assert_almost_equal(graphic.offset, (7., 8., 9.)) + npt.assert_almost_equal(graphic.offset, (7.0, 8.0, 9.0)) assert DECORATED_EVENT_VALUE.type == "offset" assert DECORATED_EVENT_VALUE.graphic is graphic assert DECORATED_EVENT_VALUE.target is graphic.world_object - assert DECORATED_EVENT_VALUE.info["value"] == (7., 8., 9.) + assert DECORATED_EVENT_VALUE.info["value"] == (7.0, 8.0, 9.0) -@pytest.mark.parametrize("graphic", [make_graphic(k, offset=(3., 4., 5.)) for k in graphic_kinds]) +@pytest.mark.parametrize( + "graphic", [make_graphic(k, offset=(3.0, 4.0, 5.0)) for k in graphic_kinds] +) def test_offset_init(graphic): - npt.assert_almost_equal(graphic.offset, (3., 4., 5.)) - npt.assert_almost_equal(graphic.world_object.world.position, (3., 4., 5.)) + npt.assert_almost_equal(graphic.offset, (3.0, 4.0, 5.0)) + npt.assert_almost_equal(graphic.world_object.world.position, (3.0, 4.0, 5.0)) - graphic.offset = (6., 7., 8.) + graphic.offset = (6.0, 7.0, 8.0) - npt.assert_almost_equal(graphic.offset, (6., 7., 8.)) - npt.assert_almost_equal(graphic.world_object.world.position, (6., 7., 8.)) + npt.assert_almost_equal(graphic.offset, (6.0, 7.0, 8.0)) + npt.assert_almost_equal(graphic.world_object.world.position, (6.0, 7.0, 8.0)) @pytest.mark.parametrize("graphic", [make_graphic(k) for k in graphic_kinds]) @@ -158,17 +161,21 @@ def test_rotation(graphic): graphic.add_event_handler(return_event, "rotation") - graphic.rotation = (0., 0., 0.30001427, 0.95393471) + graphic.rotation = (0.0, 0.0, 0.30001427, 0.95393471) - npt.assert_almost_equal(graphic.rotation, (0., 0., 0.30001427, 0.95393471)) - npt.assert_almost_equal(graphic.world_object.world.rotation, (0., 0., 0.30001427, 0.95393471)) + npt.assert_almost_equal(graphic.rotation, (0.0, 0.0, 0.30001427, 0.95393471)) + npt.assert_almost_equal( + graphic.world_object.world.rotation, (0.0, 0.0, 0.30001427, 0.95393471) + ) global RETURN_EVENT_VALUE assert RETURN_EVENT_VALUE.type == "rotation" assert RETURN_EVENT_VALUE.graphic is graphic assert RETURN_EVENT_VALUE.target is graphic.world_object - npt.assert_almost_equal(RETURN_EVENT_VALUE.info["value"], (0., 0., 0.30001427, 0.95393471)) + npt.assert_almost_equal( + RETURN_EVENT_VALUE.info["value"], (0.0, 0.0, 0.30001427, 0.95393471) + ) # check removing event handler RETURN_EVENT_VALUE = None @@ -198,10 +205,18 @@ def decorated_handler(ev): assert DECORATED_EVENT_VALUE.info["value"] == (0, 0, 0.6, 0.8) -@pytest.mark.parametrize("graphic", [make_graphic(k, rotation=(0., 0., 0.30001427, 0.95393471)) for k in graphic_kinds]) +@pytest.mark.parametrize( + "graphic", + [ + make_graphic(k, rotation=(0.0, 0.0, 0.30001427, 0.95393471)) + for k in graphic_kinds + ], +) def test_rotation(graphic): - npt.assert_almost_equal(graphic.rotation, (0., 0., 0.30001427, 0.95393471)) - npt.assert_almost_equal(graphic.world_object.world.rotation, (0., 0., 0.30001427, 0.95393471)) + npt.assert_almost_equal(graphic.rotation, (0.0, 0.0, 0.30001427, 0.95393471)) + npt.assert_almost_equal( + graphic.world_object.world.rotation, (0.0, 0.0, 0.30001427, 0.95393471) + ) graphic.rotation = (0, 0.0, 0.6, 0.8) @@ -209,7 +224,7 @@ def test_rotation(graphic): npt.assert_almost_equal(graphic.world_object.world.rotation, (0, 0.0, 0.6, 0.8)) -@pytest.mark.parametrize("graphic", [make_graphic(k)for k in graphic_kinds]) +@pytest.mark.parametrize("graphic", [make_graphic(k) for k in graphic_kinds]) def test_visible(graphic): assert graphic.visible is True assert graphic.world_object.visible is True @@ -255,7 +270,9 @@ def decorated_handler(ev): assert DECORATED_EVENT_VALUE.info["value"] is False -@pytest.mark.parametrize("graphic", [make_graphic(k, visible=False) for k in graphic_kinds]) +@pytest.mark.parametrize( + "graphic", [make_graphic(k, visible=False) for k in graphic_kinds] +) def test_visible(graphic): assert graphic.visible is False assert graphic.world_object.visible is False diff --git a/tests/test_figure.py b/tests/test_figure.py index 27b74c0b6..757b1eeae 100644 --- a/tests/test_figure.py +++ b/tests/test_figure.py @@ -6,21 +6,18 @@ def test_cameras_controller_properties(): - cameras = [ - ["2d", "3d", "3d"], - ["3d", "3d", "3d"] - ] + cameras = [["2d", "3d", "3d"], ["3d", "3d", "3d"]] controller_types = [ ["panzoom", "panzoom", "fly"], - ["orbit", "trackball", "panzoom"] + ["orbit", "trackball", "panzoom"], ] fig = fpl.Figure( shape=(2, 3), cameras=cameras, controller_types=controller_types, - canvas="offscreen" + canvas="offscreen", ) print(fig.canvas) @@ -34,13 +31,17 @@ def test_cameras_controller_properties(): for c1, c2 in zip(subplot_controllers, fig.controllers.ravel()): assert c1 is c2 - for camera_type, subplot_camera in zip(np.asarray(cameras).ravel(), fig.cameras.ravel()): + for camera_type, subplot_camera in zip( + np.asarray(cameras).ravel(), fig.cameras.ravel() + ): if camera_type == "2d": assert subplot_camera.fov == 0 else: assert subplot_camera.fov == 50 - for controller_type, subplot_controller in zip(np.asarray(controller_types).ravel(), fig.controllers.ravel()): + for controller_type, subplot_controller in zip( + np.asarray(controller_types).ravel(), fig.controllers.ravel() + ): match controller_type: case "panzoom": assert isinstance(subplot_controller, pygfx.PanZoomController) @@ -67,11 +68,7 @@ def test_cameras_controller_properties(): def test_controller_ids_int(): - ids = [ - [0, 1, 1], - [0, 2, 3], - [4, 1, 2] - ] + ids = [[0, 1, 1], [0, 2, 3], [4, 1, 2]] fig = fpl.Figure(shape=(3, 3), controller_ids=ids, canvas="offscreen") @@ -81,19 +78,13 @@ def test_controller_ids_int(): def test_controller_ids_int_change_controllers(): - ids = [ - [0, 1, 1], - [0, 2, 3], - [4, 1, 2] - ] + ids = [[0, 1, 1], [0, 2, 3], [4, 1, 2]] - cameras = [ - ["2d", "3d", "3d"], - ["2d", "3d", "2d"], - ["3d", "3d", "3d"] - ] + cameras = [["2d", "3d", "3d"], ["2d", "3d", "2d"], ["3d", "3d", "3d"]] - fig = fpl.Figure(shape=(3, 3), cameras=cameras, controller_ids=ids, canvas="offscreen") + fig = fpl.Figure( + shape=(3, 3), cameras=cameras, controller_ids=ids, canvas="offscreen" + ) assert isinstance(fig[0, 1].controller, pygfx.FlyController) @@ -101,30 +92,46 @@ def test_controller_ids_int_change_controllers(): fig[0, 1].controller = "panzoom" assert isinstance(fig[0, 1].controller, pygfx.PanZoomController) assert fig[0, 1].controller is fig[0, 2].controller is fig[2, 1].controller - assert set(fig[0, 1].controller.cameras) == {fig[0, 1].camera, fig[0, 2].camera, fig[2, 1].camera} + assert set(fig[0, 1].controller.cameras) == { + fig[0, 1].camera, + fig[0, 2].camera, + fig[2, 1].camera, + } # change to orbit fig[0, 1].controller = "orbit" assert isinstance(fig[0, 1].controller, pygfx.OrbitController) assert fig[0, 1].controller is fig[0, 2].controller is fig[2, 1].controller - assert set(fig[0, 1].controller.cameras) == {fig[0, 1].camera, fig[0, 2].camera, fig[2, 1].camera} + assert set(fig[0, 1].controller.cameras) == { + fig[0, 1].camera, + fig[0, 2].camera, + fig[2, 1].camera, + } def test_controller_ids_str(): - names = [ - ["a", "b", "c"], - ["d", "e", "f"] - ] + names = [["a", "b", "c"], ["d", "e", "f"]] - controller_ids = [ - ["a", "f"], - ["b", "d", "e"] - ] + controller_ids = [["a", "f"], ["b", "d", "e"]] - fig = fpl.Figure(shape=(2, 3), controller_ids=controller_ids, names=names, canvas="offscreen") + fig = fpl.Figure( + shape=(2, 3), controller_ids=controller_ids, names=names, canvas="offscreen" + ) - assert fig[0, 0].controller is fig[1, 2].controller is fig["a"].controller is fig["f"].controller - assert fig[0, 1].controller is fig[1, 0].controller is fig[1, 1].controller is fig["b"].controller is fig["d"].controller is fig["e"].controller + assert ( + fig[0, 0].controller + is fig[1, 2].controller + is fig["a"].controller + is fig["f"].controller + ) + assert ( + fig[0, 1].controller + is fig[1, 0].controller + is fig[1, 1].controller + is fig["b"].controller + is fig["d"].controller + is fig["e"].controller + ) # make sure subplot c is unique exclude_c = [fig[n].controller for n in ["a", "b", "d", "e", "f"]] @@ -137,22 +144,23 @@ def test_set_controllers_from_existing_controllers(): assert fig.controllers[:-1].size == 6 with pytest.raises(ValueError): - fig3 = fpl.Figure(shape=fig.shape, controllers=fig.controllers[:-1], canvas="offscreen") + fig3 = fpl.Figure( + shape=fig.shape, controllers=fig.controllers[:-1], canvas="offscreen" + ) for fig1_subplot, fig2_subplot in zip(fig, fig2): assert fig1_subplot.controller is fig2_subplot.controller - cameras = [ - [pygfx.PerspectiveCamera(), "3d"], - ["3d", "2d"] - ] + cameras = [[pygfx.PerspectiveCamera(), "3d"], ["3d", "2d"]] controllers = [ [pygfx.FlyController(cameras[0][0]), pygfx.TrackballController()], - [pygfx.OrbitController(), pygfx.PanZoomController()] + [pygfx.OrbitController(), pygfx.PanZoomController()], ] - fig = fpl.Figure(shape=(2, 2), cameras=cameras, controllers=controllers, canvas="offscreen") + fig = fpl.Figure( + shape=(2, 2), cameras=cameras, controllers=controllers, canvas="offscreen" + ) assert fig[0, 0].controller is controllers[0][0] assert fig[0, 1].controller is controllers[0][1] diff --git a/tests/test_image_graphic.py b/tests/test_image_graphic.py index 70a25a527..d30ad76b0 100644 --- a/tests/test_image_graphic.py +++ b/tests/test_image_graphic.py @@ -17,19 +17,21 @@ def check_set_slice( data: np.ndarray, - image_graphic: fpl.ImageGraphic, - row_slice: slice, - col_slice: slice, + image_graphic: fpl.ImageGraphic, + row_slice: slice, + col_slice: slice, ): image_graphic.data[row_slice, col_slice] = 1 data_values = image_graphic.data.value npt.assert_almost_equal(data_values[row_slice, col_slice], 1) # make sure other vals unchanged - npt.assert_almost_equal(data_values[:row_slice.start], data[:row_slice.start]) - npt.assert_almost_equal(data_values[row_slice.stop:], data[row_slice.stop:]) - npt.assert_almost_equal(data_values[:, :col_slice.start], data[:, :col_slice.start]) - npt.assert_almost_equal(data_values[:, col_slice.stop:], data[:, col_slice.stop:]) + npt.assert_almost_equal(data_values[: row_slice.start], data[: row_slice.start]) + npt.assert_almost_equal(data_values[row_slice.stop :], data[row_slice.stop :]) + npt.assert_almost_equal( + data_values[:, : col_slice.start], data[:, : col_slice.start] + ) + npt.assert_almost_equal(data_values[:, col_slice.stop :], data[:, col_slice.stop :]) def test_gray(): @@ -134,9 +136,13 @@ def test_rgba(): # fancy indexing # set the blue values of some pixels with an alpha > 1 - ig.data[COFFEE_IMAGE[:, :, -1] > 200] = np.array([0.0, 0.0, 1.0, 0.6]).astype(np.float32) + ig.data[COFFEE_IMAGE[:, :, -1] > 200] = np.array([0.0, 0.0, 1.0, 0.6]).astype( + np.float32 + ) - rgba[COFFEE_IMAGE[:, :, -1] > 200] = np.array([0.0, 0.0, 1.0, 0.6]).astype(np.float32) + rgba[COFFEE_IMAGE[:, :, -1] > 200] = np.array([0.0, 0.0, 1.0, 0.6]).astype( + np.float32 + ) # check that fancy indexing works npt.assert_almost_equal(ig.data.value, rgba) diff --git a/tests/test_positions_data_buffer_manager.py b/tests/test_positions_data_buffer_manager.py index 86181adfa..fc0be9b89 100644 --- a/tests/test_positions_data_buffer_manager.py +++ b/tests/test_positions_data_buffer_manager.py @@ -3,61 +3,45 @@ import pytest from fastplotlib.graphics._features import VertexPositions -from .utils import generate_slice_indices, assert_pending_uploads +from .utils import ( + generate_slice_indices, + assert_pending_uploads, + generate_positions_spiral_data, +) -def generate_data(inputs: str) -> np.ndarray: - """ - Generates a spiral/spring - - Only 10 points so a very pointy spiral but easier to spot changes :D - """ - xs = np.linspace(0, 10 * np.pi, 10) - ys = np.sin(xs) - zs = np.cos(xs) - - match inputs: - case "y": - data = ys - - case "xy": - data = np.column_stack([xs, ys]) - - case "xyz": - data = np.column_stack([xs, ys, zs]) - - return data.astype(np.float32) - - -@pytest.mark.parametrize("data", [generate_data(v) for v in ["y", "xy", "xyz"]]) +@pytest.mark.parametrize( + "data", [generate_positions_spiral_data(v) for v in ["y", "xy", "xyz"]] +) def test_create_buffer(data): points_data = VertexPositions(data) if data.ndim == 1: # only y-vals specified - npt.assert_almost_equal(points_data[:, 1], generate_data("y")) + npt.assert_almost_equal(points_data[:, 1], generate_positions_spiral_data("y")) # x-vals are auto generated just using arange npt.assert_almost_equal(points_data[:, 0], np.arange(data.size)) elif data.shape[1] == 2: # test 2D - npt.assert_almost_equal(points_data[:, :-1], generate_data("xy")) - npt.assert_almost_equal(points_data[:, -1], 0.) - + npt.assert_almost_equal( + points_data[:, :-1], generate_positions_spiral_data("xy") + ) + npt.assert_almost_equal(points_data[:, -1], 0.0) elif data.shape[1] == 3: # test 3D spiral - npt.assert_almost_equal(points_data[:], generate_data("xyz")) + npt.assert_almost_equal(points_data[:], generate_positions_spiral_data("xyz")) def test_int(): - data = generate_data("xyz") + data = generate_positions_spiral_data("xyz") # test setting single points points = VertexPositions(data) # set all x, y, z points, create a kink in the spiral - points[2] = 1. - npt.assert_almost_equal(points[2], 1.) + points[2] = 1.0 + npt.assert_almost_equal(points[2], 1.0) # make sure other points are not affected indices = list(range(10)) indices.pop(2) @@ -68,8 +52,8 @@ def test_int(): npt.assert_almost_equal(points[:], data) # just set y value - points[3, 1] = 1. - npt.assert_almost_equal(points[3, 1], 1.) + points[3, 1] = 1.0 + npt.assert_almost_equal(points[3, 1], 1.0) # make sure others not modified npt.assert_almost_equal(points[3, 0], data[3, 0]) npt.assert_almost_equal(points[3, 2], data[3, 2]) @@ -78,10 +62,12 @@ def test_int(): npt.assert_almost_equal(points[indices], data[indices]) -@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(1, 16)]) # int tested separately +@pytest.mark.parametrize( + "slice_method", [generate_slice_indices(i) for i in range(1, 16)] +) # int tested separately @pytest.mark.parametrize("test_axis", ["y", "xy", "xyz"]) def test_slice(slice_method: dict, test_axis: str): - data = generate_data("xyz") + data = generate_positions_spiral_data("xyz") s = slice_method["slice"] indices = slice_method["indices"] @@ -99,16 +85,24 @@ def test_slice(slice_method: dict, test_axis: str): npt.assert_almost_equal(points[s, 1], -data[s, 1]) npt.assert_almost_equal(points[indices, 1], -data[indices, 1]) # make sure other points are not modified - npt.assert_almost_equal(points[others, 1], data[others, 1]) # other points in same dimension - npt.assert_almost_equal(points[:, 2:], data[:, 2:]) # dimensions that are not sliced + npt.assert_almost_equal( + points[others, 1], data[others, 1] + ) # other points in same dimension + npt.assert_almost_equal( + points[:, 2:], data[:, 2:] + ) # dimensions that are not sliced case "xy": points[s, :-1] = -data[s, :-1] npt.assert_almost_equal(points[s, :-1], -data[s, :-1]) npt.assert_almost_equal(points[indices, :-1], -data[s, :-1]) # make sure other points are not modified - npt.assert_almost_equal(points[others, :-1], data[others, :-1]) # other points in the same dimensions - npt.assert_almost_equal(points[:, -1], data[:, -1]) # dimensions that are not touched + npt.assert_almost_equal( + points[others, :-1], data[others, :-1] + ) # other points in the same dimensions + npt.assert_almost_equal( + points[:, -1], data[:, -1] + ) # dimensions that are not touched case "xyz": points[s] = -data[s] diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index 77b153887..4c67ce121 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -1,10 +1,20 @@ import numpy as np from numpy import testing as npt +import pytest import pygfx import fastplotlib as fpl -from fastplotlib.graphics._features import VertexPositions, VertexColors, VertexCmap, UniformColor, UniformSizes, PointsSizesFeature +from fastplotlib.graphics._features import ( + VertexPositions, + VertexColors, + VertexCmap, + UniformColor, + UniformSizes, + PointsSizesFeature, +) + +from .utils import generate_positions_spiral_data, generate_color_inputs # TODO: use same functions to generate data and colors for graphics as the buffer test modules @@ -38,9 +48,91 @@ def test_uniform_size(): pass -def test_create_line(): - pass +@pytest.mark.parametrize( + "data", [generate_positions_spiral_data(v) for v in ["y", "xy", "xyz"]] +) +@pytest.mark.parametrize( + "colors", [None, *generate_color_inputs("r")] +) +@pytest.mark.parametrize( + "uniform_colors", [None, True] +) +@pytest.mark.parametrize( + "alpha", [None, 0.5, 0.0] +) +def test_create_line( + data, + colors, + uniform_colors, + alpha, + # cmap, + # cmap_values, + # thickness +): + # test creating line with all combinations of arguments + + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["colors", "uniform_colors", "alpha"]:#, "cmap", "cmap_values", "thickness"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + lg = fig[0, 0].add_line(data=data, **kwargs) + + # n_datapoints must match + assert len(lg.data.value) == len(data) + + # make sure data is correct + match data.shape[-1]: + case 1: # only y-vals given + npt.assert_almost_equal(lg.data[:, 1], data) # y vals must match + npt.assert_almost_equal( + lg.data[:, 0], np.arange(data.size) + ) # VertexData makes x-vals with arange + npt.assert_almost_equal(lg.data[:, -1], 0) # z-vals must be zeros + case 2: # xy vals given + npt.assert_almost_equal(lg.data[:, :-1], data) # x and y must match + npt.assert_almost_equal(lg.data[:, -1], 0) # z-vals must be zero + case 3: # xyz vals given + npt.assert_almost_equal(lg.data[:], data[:]) # everything must match + + if alpha is None: # default arg + alpha = 1 + + if uniform_colors is None: # default arg + uniform_colors = False + + # make sure colors are correct + if not uniform_colors: + assert isinstance(lg._colors, VertexColors) + assert isinstance(lg.colors, VertexColors) + if colors is None: + # should be default, "w" + npt.assert_almost_equal(lg.colors.value, np.repeat([[1, 1, 1, alpha]], repeats=len(lg.data), axis=0)) + else: + # should be red, regardless of input variant (i.e. str, array, RGBA tuple, etc. + npt.assert_almost_equal(lg.colors.value, np.repeat([[1, 0, 0, alpha]], repeats=len(lg.data), axis=0)) + + else: + assert isinstance(lg._colors, UniformColor) + assert isinstance(lg.colors, pygfx.Color) + if colors is None: + # default "w" + assert lg.colors == pygfx.Color("w") + else: + assert lg.colors == pygfx.Color("r") + def test_create_scatter(): pass + + +def test_line_feature_events(): + pass + + +def test_scatter_feature_events(): + pass diff --git a/tests/test_sizes_buffer_manager.py b/tests/test_sizes_buffer_manager.py index 0f90353f4..0b34f9588 100644 --- a/tests/test_sizes_buffer_manager.py +++ b/tests/test_sizes_buffer_manager.py @@ -16,7 +16,7 @@ def generate_data(input_type: str) -> np.ndarray | float: one of "sine", "cosine", or "float" """ if input_type == "float": - return 10. + return 10.0 xs = np.linspace(0, 10 * np.pi, 10) if input_type == "sine": @@ -37,7 +37,9 @@ def test_create_buffer(data): npt.assert_almost_equal(sizes[:], generate_data("sine")) -@pytest.mark.parametrize("slice_method", [generate_slice_indices(i) for i in range(0, 16)]) +@pytest.mark.parametrize( + "slice_method", [generate_slice_indices(i) for i in range(0, 16)] +) @pytest.mark.parametrize("user_input", ["float", "cosine"]) def test_slice(slice_method: dict, user_input: str): data = generate_data("sine") @@ -55,8 +57,8 @@ def test_slice(slice_method: dict, user_input: str): match user_input: case "float": - sizes[s] = 20. - truth = np.full(len(indices), 20.) + sizes[s] = 20.0 + truth = np.full(len(indices), 20.0) npt.assert_almost_equal(sizes[s], truth) npt.assert_almost_equal(sizes[indices], truth) # make sure other sizes not modified diff --git a/tests/test_texture_array.py b/tests/test_texture_array.py index 78b901aa7..9cb71e253 100644 --- a/tests/test_texture_array.py +++ b/tests/test_texture_array.py @@ -20,14 +20,14 @@ def make_data(n_rows: int, n_cols: int) -> np.ndarray: def check_texture_array( - data: np.ndarray, - ta: TextureArray, - buffer_size: int, - buffer_shape: tuple[int, int], - row_indices_size: int, - col_indices_size: int, - row_indices_values: np.ndarray, - col_indices_values: np.ndarray, + data: np.ndarray, + ta: TextureArray, + buffer_size: int, + buffer_shape: tuple[int, int], + row_indices_size: int, + col_indices_size: int, + row_indices_values: np.ndarray, + col_indices_values: np.ndarray, ): npt.assert_almost_equal(ta.value, data) @@ -50,8 +50,12 @@ def check_texture_array( data_row_start_index = chunk_row * WGPU_MAX_TEXTURE_SIZE data_col_start_index = chunk_col * WGPU_MAX_TEXTURE_SIZE - data_row_stop_index = min(data.shape[0] - 1, data_row_start_index + WGPU_MAX_TEXTURE_SIZE) - data_col_stop_index = min(data.shape[1] - 1, data_col_start_index + WGPU_MAX_TEXTURE_SIZE) + data_row_stop_index = min( + data.shape[0] - 1, data_row_start_index + WGPU_MAX_TEXTURE_SIZE + ) + data_col_stop_index = min( + data.shape[1] - 1, data_col_start_index + WGPU_MAX_TEXTURE_SIZE + ) row_slice = slice(data_row_start_index, data_row_stop_index) col_slice = slice(data_col_start_index, data_col_stop_index) @@ -64,10 +68,10 @@ def check_set_slice(data, ta, row_slice, col_slice): npt.assert_almost_equal(ta[row_slice, col_slice], 1) # make sure other vals unchanged - npt.assert_almost_equal(ta[:row_slice.start], data[:row_slice.start]) - npt.assert_almost_equal(ta[row_slice.stop:], data[row_slice.stop:]) - npt.assert_almost_equal(ta[:, :col_slice.start], data[:, :col_slice.start]) - npt.assert_almost_equal(ta[:, col_slice.stop:], data[:, col_slice.stop:]) + npt.assert_almost_equal(ta[: row_slice.start], data[: row_slice.start]) + npt.assert_almost_equal(ta[row_slice.stop :], data[row_slice.stop :]) + npt.assert_almost_equal(ta[:, : col_slice.start], data[:, : col_slice.start]) + npt.assert_almost_equal(ta[:, col_slice.stop :], data[:, col_slice.stop :]) def test_small_texture(): @@ -84,7 +88,7 @@ def test_small_texture(): row_indices_size=1, col_indices_size=1, row_indices_values=np.array([0]), - col_indices_values=np.array([0]) + col_indices_values=np.array([0]), ) check_set_slice(data, ta, slice(50, 200), slice(600, 800)) @@ -104,7 +108,7 @@ def test_texture_at_limit(): row_indices_size=1, col_indices_size=1, row_indices_values=np.array([0]), - col_indices_values=np.array([0]) + col_indices_values=np.array([0]), ) check_set_slice(data, ta, slice(5000, 8000), slice(2000, 3000)) @@ -123,7 +127,7 @@ def test_wide(): row_indices_size=2, col_indices_size=3, row_indices_values=np.array([0, 8192]), - col_indices_values=np.array([0, 8192, 16384]) + col_indices_values=np.array([0, 8192, 16384]), ) check_set_slice(data, ta, slice(6_000, 9_000), slice(12_000, 18_000)) @@ -142,7 +146,7 @@ def test_tall(): row_indices_size=3, col_indices_size=2, row_indices_values=np.array([0, 8192, 16384]), - col_indices_values=np.array([0, 8192]) + col_indices_values=np.array([0, 8192]), ) check_set_slice(data, ta, slice(12_000, 18_000), slice(6_000, 9_000)) @@ -161,7 +165,7 @@ def test_square(): row_indices_size=3, col_indices_size=3, row_indices_values=np.array([0, 8192, 16384]), - col_indices_values=np.array([0, 8192, 16384]) + col_indices_values=np.array([0, 8192, 16384]), ) check_set_slice(data, ta, slice(12_000, 18_000), slice(16_000, 19_000)) diff --git a/tests/utils.py b/tests/utils.py index 8aa474b1f..b692006e1 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -97,7 +97,13 @@ def generate_slice_indices(kind: int): offset, size = (min(indices), np.ptp(indices) + 1) - return {"slice": s, "indices": indices, "others": others, "offset": offset, "size": size} + return { + "slice": s, + "indices": indices, + "others": others, + "offset": offset, + "size": size, + } def assert_pending_uploads(buffer: pygfx.Buffer, offset: int, size: int): @@ -108,4 +114,38 @@ def assert_pending_uploads(buffer: pygfx.Buffer, offset: int, size: int): # sometimes when slicing with step, it will over-estimate size # but it overestimates to upload 1 extra point so it's fine - assert (upload_size == size) or (upload_size == size + 1) \ No newline at end of file + assert (upload_size == size) or (upload_size == size + 1) + + +def generate_positions_spiral_data(inputs: str) -> np.ndarray: + """ + Generates a spiral/spring + + Only 10 points so a very pointy spiral but easier to spot changes :D + """ + xs = np.linspace(0, 10 * np.pi, 10) + ys = np.sin(xs) + zs = np.cos(xs) + + match inputs: + case "y": + data = ys + + case "xy": + data = np.column_stack([xs, ys]) + + case "xyz": + data = np.column_stack([xs, ys, zs]) + + return data.astype(np.float32) + + +def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple]: + color = pygfx.Color(name) + + s = name + a = np.array(color) + l = list(color) + t = tuple(color) + + return [s, a, l, t] From c82b90ce53cb795bda7409d88f7cdda2f20405fb Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Jun 2024 00:24:32 -0400 Subject: [PATCH 103/196] black --- tests/events.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/events.py b/tests/events.py index 57fde6886..ea160dec3 100644 --- a/tests/events.py +++ b/tests/events.py @@ -40,10 +40,7 @@ def test_positions_data_event(graphic: fpl.LineGraphic | fpl.ScatterGraphic): value = np.cos(np.linspace(0, 10 * np.pi, 10))[3:8] - info = { - "key": (slice(3, 8, None), 1), - "value": value - } + info = {"key": (slice(3, 8, None), 1), "value": value} expected = FeatureEvent(type="data", info=info) @@ -51,7 +48,9 @@ def validate(graphic, handler, expected_feature_event, event_to_test): assert expected_feature_event.type == event_to_test.type assert expected_feature_event.info["key"] == event_to_test.info["key"] - npt.assert_almost_equal(expected_feature_event.info["value"], event_to_test.info["value"]) + npt.assert_almost_equal( + expected_feature_event.info["value"], event_to_test.info["value"] + ) # should only have one event handler assert graphic._event_handlers["data"] == {handler} From 28105782bfeb3ca9c1b2963f4d564bf5b1854f1d Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Jun 2024 00:25:15 -0400 Subject: [PATCH 104/196] add __len__ to buffer managers, add __array_interface__ raises error --- fastplotlib/graphics/_features/_base.py | 10 ++++++++++ .../graphics/_features/_positions_graphics.py | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 0429b05e1..4bc84f645 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -201,6 +201,13 @@ def shared(self) -> int: """Number of graphics that share this buffer""" return self._shared + @property + def __array_interface__(self): + raise BufferError( + f"Cannot use graphic feature buffer as an array, use .value instead.\n" + f"Examples: line.data.value, line.colors.value, scatter.data.value, scatter.sizes.value" + ) + def __getitem__(self, item): return self.buffer.data[item] @@ -315,5 +322,8 @@ def _emit_event(self, type: str, key, value): self._call_event_handlers(event) + def __len__(self): + raise NotImplementedError + def __repr__(self): return f"{self.__class__.__name__} buffer data:\n" f"{self.value.__repr__()}" diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index e555a3111..29da8bd29 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -129,6 +129,9 @@ def __setitem__( event = FeatureEvent("colors", info=event_info) self._call_event_handlers(event) + def __len__(self): + return len(self.buffer.data) + class UniformColor(GraphicFeature): def __init__(self, value: str | np.ndarray | tuple | list | pygfx.Color): @@ -220,6 +223,9 @@ def __setitem__( self._emit_event("data", key, value) + def __len__(self): + return len(self.buffer.data) + class PointsSizesFeature(BufferManager): """ @@ -289,6 +295,9 @@ def __setitem__( self._emit_event("sizes", key, value) + def __len__(self): + return len(self.buffer.data) + class Thickness(GraphicFeature): """line thickness""" @@ -406,3 +415,6 @@ def values( self._vertex_colors[indices] = colors self._emit_event("cmap.values", indices, values) + + def __len__(self): + raise NotImplementedError("len not implemented for `cmap`, use len(colors) instead") From 4d07e4f3eddd879c557bd414ada44a13a336caf8 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Jun 2024 00:25:46 -0400 Subject: [PATCH 105/196] TextureArray has len() --- fastplotlib/graphics/_features/_image.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py index 6f0c423da..e31184c4b 100644 --- a/fastplotlib/graphics/_features/_image.py +++ b/fastplotlib/graphics/_features/_image.py @@ -3,7 +3,6 @@ from math import ceil import numpy as np -from numpy.typing import NDArray import pygfx from ._base import GraphicFeature, FeatureEvent, WGPU_MAX_TEXTURE_SIZE @@ -58,7 +57,7 @@ def __init__(self, data, isolated_buffer: bool = True): self._shared: int = 0 @property - def value(self) -> NDArray: + def value(self) -> np.ndarray: return self._value def set_value(self, graphic, value): @@ -142,6 +141,9 @@ def __setitem__(self, key, value): event = FeatureEvent("data", info={"key": key, "value": value}) self._call_event_handlers(event) + def __len__(self): + return self.buffer.size + class ImageVmin(GraphicFeature): """lower contrast limit""" From 84c0be9c41263ef286235e5995fedb008d7aa925 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Jun 2024 00:26:26 -0400 Subject: [PATCH 106/196] docstring --- fastplotlib/graphics/line.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 10067404c..2b37d6bb1 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -62,28 +62,9 @@ def __init__( z_position: float, optional z-axis position for placing the graphic - kwargs + **kwargs passed to Graphic - Features - -------- - - - **data**: :class:`.ImageDataFeature` - Manages the line [x, y, z] positions data buffer, allows regular and fancy indexing. - - **colors**: :class:`.ColorFeature` - Manages the color buffer, allows regular and fancy indexing. - - **cmap**: :class:`.CmapFeature` - Manages the cmap, wraps :class:`.ColorFeature` to add additional functionality relevant to cmaps. - - **thickness**: :class:`.ThicknessFeature` - Manages the thickness feature of the lines. - - **present**: :class:`.PresentFeature` - Control the presence of the Graphic in the scene, set to ``True`` or ``False`` - """ super().__init__( From ed686dd4205a430a96fe696127632fb060e0dae8 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Jun 2024 02:35:57 -0400 Subject: [PATCH 107/196] updates and tests --- .../graphics/_features/_positions_graphics.py | 22 +- fastplotlib/graphics/_positions_base.py | 22 +- fastplotlib/graphics/line.py | 6 +- fastplotlib/graphics/line_collection.py | 2 +- fastplotlib/graphics/scatter.py | 6 +- fastplotlib/layouts/_graphic_methods_mixin.py | 64 ++--- tests/test_positions_graphics.py | 254 ++++++++++++++---- tests/utils.py | 21 +- 8 files changed, 288 insertions(+), 109 deletions(-) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index 29da8bd29..972629b86 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -134,8 +134,9 @@ def __len__(self): class UniformColor(GraphicFeature): - def __init__(self, value: str | np.ndarray | tuple | list | pygfx.Color): - self._value = pygfx.Color(value) + def __init__(self, value: str | np.ndarray | tuple | list | pygfx.Color, alpha: float = 1.0): + v = (*tuple(pygfx.Color(value))[:-1], alpha) # apply alpha + self._value = pygfx.Color(v) super().__init__() @property @@ -328,12 +329,14 @@ def __init__( vertex_colors: VertexColors, cmap_name: str | None, cmap_values: np.ndarray | None, + alpha: float = 1.0 ): super().__init__(data=vertex_colors.buffer) self._vertex_colors = vertex_colors self._cmap_name = cmap_name self._cmap_values = cmap_values + self._alpha = alpha if self._cmap_name is not None: if not isinstance(self._cmap_name, str): @@ -351,6 +354,7 @@ def __init__( cmap_name=self._cmap_name, cmap_values=self._cmap_values, ) + colors[:, -1] = alpha # set vertex colors from cmap self._vertex_colors[:] = colors @@ -373,6 +377,7 @@ def __setitem__(self, key: slice, cmap_name): colors = parse_cmap_values( n_colors=n_elements, cmap_name=cmap_name, cmap_values=self._cmap_values ) + colors[:, -1] = self.alpha self._cmap_name = cmap_name self._vertex_colors[key] = colors @@ -407,6 +412,8 @@ def values( n_colors=self.value.shape[0], cmap_name=self._cmap_name, cmap_values=values ) + colors[:, -1] = self.alpha + self._cmap_values = values if indices is None: @@ -416,5 +423,16 @@ def values( self._emit_event("cmap.values", indices, values) + @property + def alpha(self) -> float: + return self._alpha + + @alpha.setter + def alpha(self, value: float, indices: slice | list | np.ndarray = None): + self._vertex_colors[indices, -1] = value + self._alpha = value + + self._emit_event("cmap.alpha", indices, value) + def __len__(self): raise NotImplementedError("len not implemented for `cmap`, use len(colors) instead") diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index e9a3c92c7..7832aa13f 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -52,7 +52,7 @@ def __init__( self, data: Any, colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", - uniform_colors: bool = False, + uniform_color: bool = False, alpha: float = 1.0, cmap: str | VertexCmap = None, cmap_values: np.ndarray = None, @@ -65,10 +65,13 @@ def __init__( else: self._data = VertexPositions(data, isolated_buffer=isolated_buffer) + if cmap_values is not None and cmap is None: + raise ValueError("must pass `cmap` if passing `cmap_values`") + if cmap is not None: # if a cmap is specified it overrides colors argument - if uniform_colors: - raise TypeError("Cannot use cmap if uniform_colors=True") + if uniform_color: + raise TypeError("Cannot use cmap if uniform_color=True") if isinstance(cmap, str): # make colors from cmap @@ -81,7 +84,7 @@ def __init__( self._colors = VertexColors("w", n_colors=self._data.value.shape[0]) # make cmap using vertex colors buffer self._cmap = VertexCmap( - self._colors, cmap_name=cmap, cmap_values=cmap_values + self._colors, cmap_name=cmap, cmap_values=cmap_values, alpha=alpha ) elif isinstance(cmap, VertexCmap): # use existing cmap instance @@ -96,10 +99,13 @@ def __init__( self._colors = colors self._colors._shared += 1 # blank colormap instance - self._cmap = VertexCmap(self._colors, cmap_name=None, cmap_values=None) + self._cmap = VertexCmap(self._colors, cmap_name=None, cmap_values=None, alpha=alpha) else: - if uniform_colors: - self._colors = UniformColor(colors) + if uniform_color: + if not isinstance(colors, str): # not a single color + if not len(colors) in [3, 4]: # not an RGB(A) array + raise TypeError("must pass a single color if using `uniform_colors=True`") + self._colors = UniformColor(colors, alpha=alpha) self._cmap = None else: self._colors = VertexColors( @@ -108,7 +114,7 @@ def __init__( alpha=alpha, ) self._cmap = VertexCmap( - self._colors, cmap_name=None, cmap_values=None + self._colors, cmap_name=None, cmap_values=None, alpha=alpha ) super().__init__(*args, **kwargs) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 2b37d6bb1..d94cbf2f5 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -27,7 +27,7 @@ def __init__( data: Any, thickness: float = 2.0, colors: str | np.ndarray | Iterable = "w", - uniform_colors: bool = False, + uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, cmap_values: np.ndarray | Iterable = None, @@ -70,7 +70,7 @@ def __init__( super().__init__( data=data, colors=colors, - uniform_colors=uniform_colors, + uniform_color=uniform_color, alpha=alpha, cmap=cmap, cmap_values=cmap_values, @@ -85,7 +85,7 @@ def __init__( else: MaterialCls = pygfx.LineMaterial - if uniform_colors: + if uniform_color: geometry = pygfx.Geometry(positions=self._data.buffer) material = MaterialCls( thickness=self.thickness, color_mode="uniform", pick_write=True diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 72ee04f37..6c9975c95 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -257,7 +257,7 @@ def __init__( data=d, thickness=_s, colors=_c, - uniform_colors=uniform_colors, + uniform_color=uniform_colors, cmap=_cmap, metadata=_m, isolated_buffer=isolated_buffer, diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 1c0445b28..1cbf098dd 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -31,7 +31,7 @@ def __init__( self, data: Any, colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", - uniform_colors: bool = False, + uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, cmap_values: np.ndarray = None, @@ -91,7 +91,7 @@ def __init__( super().__init__( data=data, colors=colors, - uniform_colors=uniform_colors, + uniform_color=uniform_color, alpha=alpha, cmap=cmap, cmap_values=cmap_values, @@ -105,7 +105,7 @@ def __init__( geo_kwargs = {"positions": self._data.buffer} material_kwargs = {"pick_write": True} - if uniform_colors: + if uniform_color: material_kwargs["color_mode"] = "uniform" material_kwargs["color"] = self.colors.value else: diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index d523bc668..e92e3aabc 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -184,7 +184,7 @@ def add_line( data: Any, thickness: float = 2.0, colors: Union[str, numpy.ndarray, Iterable] = "w", - uniform_colors: bool = False, + uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, cmap_values: Union[numpy.ndarray, Iterable] = None, @@ -220,28 +220,9 @@ def add_line( z_position: float, optional z-axis position for placing the graphic - kwargs + **kwargs passed to Graphic - Features - -------- - - - **data**: :class:`.ImageDataFeature` - Manages the line [x, y, z] positions data buffer, allows regular and fancy indexing. - - **colors**: :class:`.ColorFeature` - Manages the color buffer, allows regular and fancy indexing. - - **cmap**: :class:`.CmapFeature` - Manages the cmap, wraps :class:`.ColorFeature` to add additional functionality relevant to cmaps. - - **thickness**: :class:`.ThicknessFeature` - Manages the thickness feature of the lines. - - **present**: :class:`.PresentFeature` - Control the presence of the Graphic in the scene, set to ``True`` or ``False`` - """ return self._create_graphic( @@ -249,7 +230,7 @@ def add_line( data, thickness, colors, - uniform_colors, + uniform_color, alpha, cmap, cmap_values, @@ -345,7 +326,7 @@ def add_scatter( self, data: Any, colors: str | numpy.ndarray | tuple[float] | list[float] | list[str] = "w", - uniform_colors: bool = False, + uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, cmap_values: numpy.ndarray = None, @@ -407,7 +388,7 @@ def add_scatter( ScatterGraphic, data, colors, - uniform_colors, + uniform_color, alpha, cmap, cmap_values, @@ -420,12 +401,12 @@ def add_scatter( def add_text( self, text: str, - position: Tuple[int] = (0, 0, 0), - size: int = 14, - face_color: Union[str, numpy.ndarray] = "w", - outline_color: Union[str, numpy.ndarray] = "w", - outline_thickness=0, + font_size: float | int = 14, + face_color: str | numpy.ndarray | list[float] | tuple[float] = "w", + outline_color: str | numpy.ndarray | list[float] | tuple[float] = "w", + outline_thickness: float | int = 0, screen_space: bool = True, + offset: tuple[float] = (0, 0, 0), anchor: str = "middle-center", **kwargs ) -> TextGraphic: @@ -436,13 +417,10 @@ def add_text( Parameters ---------- text: str - display text - - position: int tuple, default (0, 0, 0) - int tuple indicating location of text in scene + text to display - size: int, default 10 - text size + font_size: float | int, default 10 + font size face_color: str or array, default "w" str or RGBA array to set the color of the text @@ -450,14 +428,14 @@ def add_text( outline_color: str or array, default "w" str or RGBA array to set the outline color of the text - outline_thickness: int, default 0 + outline_thickness: float | int, default 0 text outline thickness screen_space: bool = True - whether the text is rendered in screen space, in contrast to world space + if True, text size is in screen space, if False the text size is in data space - name: str, optional - name of graphic, passed to Graphic + offset: (float, float, float), default (0, 0, 0) + places the text at this location anchor: str, default "middle-center" position of the origin of the text @@ -466,16 +444,20 @@ def add_text( * Vertical values: "top", "middle", "baseline", "bottom" * Horizontal values: "left", "center", "right" + **kwargs + passed to Graphic + + """ return self._create_graphic( TextGraphic, text, - position, - size, + font_size, face_color, outline_color, outline_thickness, screen_space, + offset, anchor, **kwargs ) diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index 4c67ce121..a9d978008 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -14,11 +14,39 @@ PointsSizesFeature, ) -from .utils import generate_positions_spiral_data, generate_color_inputs +from .utils import generate_positions_spiral_data, generate_color_inputs, MULTI_COLORS_TRUTH + + +TRUTH_CMAPS = { + "jet": np.array( + [[0. , 0. , 0.5 , 1. ], + [0. , 0. , 0.99910873, 1. ], + [0. , 0.37843138, 1. , 1. ], + [0. , 0.8333333 , 1. , 1. ], + [0.30044276, 1. , 0.66729915, 1. ], + [0.65464896, 1. , 0.31309298, 1. ], + [1. , 0.90123457, 0. , 1. ], + [1. , 0.4945534 , 0. , 1. ], + [1. , 0.08787218, 0. , 1. ], + [0.5 , 0. , 0. , 1. ]], + dtype=np.float32 +), + +"viridis": np.array( + [[0.267004, 0.004874, 0.329415, 1. ], + [0.281412, 0.155834, 0.469201, 1. ], + [0.244972, 0.287675, 0.53726 , 1. ], + [0.190631, 0.407061, 0.556089, 1. ], + [0.147607, 0.511733, 0.557049, 1. ], + [0.119483, 0.614817, 0.537692, 1. ], + [0.20803 , 0.718701, 0.472873, 1. ], + [0.421908, 0.805774, 0.35191 , 1. ], + [0.699415, 0.867117, 0.175971, 1. ], + [0.993248, 0.906157, 0.143936, 1. ]], + dtype=np.float32) +} -# TODO: use same functions to generate data and colors for graphics as the buffer test modules - def test_data_slice(): pass @@ -48,86 +76,212 @@ def test_uniform_size(): pass +@pytest.mark.parametrize( + "graphic_type", ["line", "scatter"] +) @pytest.mark.parametrize( "data", [generate_positions_spiral_data(v) for v in ["y", "xy", "xyz"]] ) +def test_positions_graphics_data( + graphic_type, + data, +): + # tests with multi-lines + + fig = fpl.Figure() + + if graphic_type == "line": + graphic = fig[0, 0].add_line(data=data) + + elif graphic_type == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + # n_datapoints must match + assert len(graphic.data.value) == len(data) + + # make sure data is correct + match data.shape[-1]: + case 1: # only y-vals given + npt.assert_almost_equal(graphic.data[:, 1], data) # y vals must match + npt.assert_almost_equal( + graphic.data[:, 0], np.arange(data.size) + ) # VertexData makes x-vals with arange + npt.assert_almost_equal(graphic.data[:, -1], 0) # z-vals must be zeros + case 2: # xy vals given + npt.assert_almost_equal(graphic.data[:, :-1], data) # x and y must match + npt.assert_almost_equal(graphic.data[:, -1], 0) # z-vals must be zero + case 3: # xyz vals given + npt.assert_almost_equal(graphic.data[:], data[:]) # everything must match + + +@pytest.mark.parametrize( + "graphic_type", ["line", "scatter"] +) @pytest.mark.parametrize( "colors", [None, *generate_color_inputs("r")] ) @pytest.mark.parametrize( - "uniform_colors", [None, True] + "uniform_color", [None, False] ) @pytest.mark.parametrize( "alpha", [None, 0.5, 0.0] ) -def test_create_line( - data, +def test_positions_graphic_vertex_colors( + graphic_type, colors, - uniform_colors, + uniform_color, alpha, - # cmap, - # cmap_values, - # thickness ): - # test creating line with all combinations of arguments - fig = fpl.Figure() kwargs = dict() - for kwarg in ["colors", "uniform_colors", "alpha"]:#, "cmap", "cmap_values", "thickness"]: + for kwarg in ["colors", "uniform_color", "alpha"]: if locals()[kwarg] is not None: # add to dict of arguments that will be passed kwargs[kwarg] = locals()[kwarg] - lg = fig[0, 0].add_line(data=data, **kwargs) + data = generate_positions_spiral_data("xy") - # n_datapoints must match - assert len(lg.data.value) == len(data) + if graphic_type == "line": + graphic = fig[0, 0].add_line(data=data, **kwargs) + elif graphic_type == "scatter": + graphic = fig[0, 0].add_scatter(data=data, **kwargs) - # make sure data is correct - match data.shape[-1]: - case 1: # only y-vals given - npt.assert_almost_equal(lg.data[:, 1], data) # y vals must match - npt.assert_almost_equal( - lg.data[:, 0], np.arange(data.size) - ) # VertexData makes x-vals with arange - npt.assert_almost_equal(lg.data[:, -1], 0) # z-vals must be zeros - case 2: # xy vals given - npt.assert_almost_equal(lg.data[:, :-1], data) # x and y must match - npt.assert_almost_equal(lg.data[:, -1], 0) # z-vals must be zero - case 3: # xyz vals given - npt.assert_almost_equal(lg.data[:], data[:]) # everything must match if alpha is None: # default arg alpha = 1 - if uniform_colors is None: # default arg - uniform_colors = False - - # make sure colors are correct - if not uniform_colors: - assert isinstance(lg._colors, VertexColors) - assert isinstance(lg.colors, VertexColors) - if colors is None: - # should be default, "w" - npt.assert_almost_equal(lg.colors.value, np.repeat([[1, 1, 1, alpha]], repeats=len(lg.data), axis=0)) - else: - # should be red, regardless of input variant (i.e. str, array, RGBA tuple, etc. - npt.assert_almost_equal(lg.colors.value, np.repeat([[1, 0, 0, alpha]], repeats=len(lg.data), axis=0)) + # color per vertex + # uniform colors is default False, or set to False + assert isinstance(graphic._colors, VertexColors) + assert isinstance(graphic.colors, VertexColors) + assert len(graphic.colors) == len(graphic.data) + if colors is None: + # default + npt.assert_almost_equal(graphic.colors.value, + np.repeat([[1, 1, 1, alpha]], repeats=len(graphic.data), axis=0)) else: - assert isinstance(lg._colors, UniformColor) - assert isinstance(lg.colors, pygfx.Color) - if colors is None: - # default "w" - assert lg.colors == pygfx.Color("w") + if len(colors) != len(graphic.data): + # should be single red, regardless of input variant (i.e. str, array, RGBA tuple, etc. + npt.assert_almost_equal(graphic.colors.value, + np.repeat([[1, 0, 0, alpha]], repeats=len(graphic.data), axis=0)) else: - assert lg.colors == pygfx.Color("r") + # multi colors + # use the truth for multi colors test that is pre-set + npt.assert_almost_equal(graphic.colors.value, MULTI_COLORS_TRUTH) +@pytest.mark.parametrize( + "graphic_type", ["line", "scatter"] +) +@pytest.mark.parametrize( + "colors", [None, *generate_color_inputs("r")] +) +@pytest.mark.parametrize( + "uniform_color", [None, False] +) +@pytest.mark.parametrize( + "cmap", ["jet", "viridis"] +) +@pytest.mark.parametrize( + "cmap_values", [None, [3, 5, 2, 1, 0, 6, 9, 7, 4, 8], np.arange(9, -1, -1)] +) +@pytest.mark.parametrize( + "alpha", [None, 0.5, 0.0] +) +def test_cmap( + graphic_type, + colors, + uniform_color, + cmap, + cmap_values, + alpha, +): + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["cmap", "cmap_values", "colors", "uniform_color", "alpha"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + if graphic_type == "line": + graphic = fig[0, 0].add_line(data=data, **kwargs) + elif graphic_type == "scatter": + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + + if alpha is None: + alpha = 1.0 + + truth = TRUTH_CMAPS[cmap].copy() + truth[:, -1] = alpha + + # permute if cmap_values is provided + if cmap_values is not None: + truth = truth[cmap_values] + + assert graphic.cmap.name == cmap + + # make sure buffer is identical + # cmap overrides colors argument + assert graphic.colors.buffer is graphic.cmap.buffer + + npt.assert_almost_equal(graphic.cmap.value, truth) + npt.assert_almost_equal(graphic.colors.value, truth) + + +@pytest.mark.parametrize( + "graphic_type", ["line", "scatter"] +) +@pytest.mark.parametrize( + "cmap", ["jet"] +) +@pytest.mark.parametrize( + "colors", [*generate_color_inputs("multi")] +) +@pytest.mark.parametrize( + "uniform_color", [True] # none of these will work with a uniform buffer +) +def test_incompatible_args( + graphic_type, + cmap, + colors, + uniform_color +): + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["cmap", "colors", "uniform_color"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + if graphic_type == "line": + with pytest.raises(TypeError): + graphic = fig[0, 0].add_line(data=data, **kwargs) + elif graphic_type == "scatter": + with pytest.raises(TypeError): + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + +# @pytest.mark.parametrize( +# "sizes", [None, 5.0, np.linspace(3, 8, 10)] +# ) +# def test_sizes(): +# pass +# +# +# @pytest.mark.parametrize( +# "thickness", [None, 5.0] +# ) +# def test_thicnkess(): +# pass + -def test_create_scatter(): - pass def test_line_feature_events(): diff --git a/tests/utils.py b/tests/utils.py index b692006e1..a1ef53832 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -140,7 +140,12 @@ def generate_positions_spiral_data(inputs: str) -> np.ndarray: return data.astype(np.float32) -def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple]: +def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple] | list[str, np.ndarray]: + if name == "multi": + s = ["r", "g", "b", "cyan", "magenta", "green", "yellow", "white", "purple", "orange"] + array = np.vstack([pygfx.Color(c) for c in s]) + return [s, array] + color = pygfx.Color(name) s = name @@ -149,3 +154,17 @@ def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple]: t = tuple(color) return [s, a, l, t] + + +MULTI_COLORS_TRUTH = np.array( + [[1.0, 0.0, 0.0, 1.0], + [0.0, 1.0, 0.0, 1.0], + [0.0, 0.0, 1.0, 1.0], + [0.0, 1.0, 1.0, 1.0], + [1.0, 0.0, 1.0, 1.0], + [0.0, 0.501960813999176, 0.0, 1.0], + [1.0, 1.0, 0.0, 1.0], + [1.0, 1.0, 1.0, 1.0], + [0.501960813999176, 0.0, 0.501960813999176, 1.0], + [1.0, 0.6470588445663452, 0.0, 1.0]] +) From 8d57c5096e3af924c77221cb6511b8ee2fe12a64 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Jun 2024 02:38:29 -0400 Subject: [PATCH 108/196] black --- fastplotlib/graphics/_features/__init__.py | 8 +- fastplotlib/graphics/_features/_base.py | 8 +- .../graphics/_features/_positions_graphics.py | 10 +- fastplotlib/graphics/_positions_base.py | 21 ++- fastplotlib/graphics/image.py | 16 +- fastplotlib/graphics/text.py | 10 +- tests/test_positions_graphics.py | 160 ++++++++---------- tests/utils.py | 39 +++-- 8 files changed, 150 insertions(+), 122 deletions(-) diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index b3399fc82..d6d1e6465 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -23,7 +23,13 @@ to_gpu_supported_dtype, ) -from ._text import TextData, FontSize, TextFaceColor, TextOutlineColor, TextOutlineThickness +from ._text import ( + TextData, + FontSize, + TextFaceColor, + TextOutlineColor, + TextOutlineThickness, +) from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature from ._common import Name, Offset, Rotation, Visible, Deleted diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 4bc84f645..17ad8a1ea 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -287,11 +287,9 @@ def _parse_offset_size( def _update_range( self, - key: int - | slice - | np.ndarray[int | bool] - | list[bool | int] - | tuple[slice, ...], + key: ( + int | slice | np.ndarray[int | bool] | list[bool | int] | tuple[slice, ...] + ), ): """ Uses key from slicing to determine the offset and diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index 972629b86..0b6f4b8f1 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -134,7 +134,9 @@ def __len__(self): class UniformColor(GraphicFeature): - def __init__(self, value: str | np.ndarray | tuple | list | pygfx.Color, alpha: float = 1.0): + def __init__( + self, value: str | np.ndarray | tuple | list | pygfx.Color, alpha: float = 1.0 + ): v = (*tuple(pygfx.Color(value))[:-1], alpha) # apply alpha self._value = pygfx.Color(v) super().__init__() @@ -329,7 +331,7 @@ def __init__( vertex_colors: VertexColors, cmap_name: str | None, cmap_values: np.ndarray | None, - alpha: float = 1.0 + alpha: float = 1.0, ): super().__init__(data=vertex_colors.buffer) @@ -435,4 +437,6 @@ def alpha(self, value: float, indices: slice | list | np.ndarray = None): self._emit_event("cmap.alpha", indices, value) def __len__(self): - raise NotImplementedError("len not implemented for `cmap`, use len(colors) instead") + raise NotImplementedError( + "len not implemented for `cmap`, use len(colors) instead" + ) diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 7832aa13f..3830755b3 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -4,7 +4,13 @@ import pygfx from ._base import Graphic -from ._features import VertexPositions, VertexColors, UniformColor, VertexCmap, PointsSizesFeature +from ._features import ( + VertexPositions, + VertexColors, + UniformColor, + VertexCmap, + PointsSizesFeature, +) class PositionsGraphic(Graphic): @@ -84,7 +90,10 @@ def __init__( self._colors = VertexColors("w", n_colors=self._data.value.shape[0]) # make cmap using vertex colors buffer self._cmap = VertexCmap( - self._colors, cmap_name=cmap, cmap_values=cmap_values, alpha=alpha + self._colors, + cmap_name=cmap, + cmap_values=cmap_values, + alpha=alpha, ) elif isinstance(cmap, VertexCmap): # use existing cmap instance @@ -99,12 +108,16 @@ def __init__( self._colors = colors self._colors._shared += 1 # blank colormap instance - self._cmap = VertexCmap(self._colors, cmap_name=None, cmap_values=None, alpha=alpha) + self._cmap = VertexCmap( + self._colors, cmap_name=None, cmap_values=None, alpha=alpha + ) else: if uniform_color: if not isinstance(colors, str): # not a single color if not len(colors) in [3, 4]: # not an RGB(A) array - raise TypeError("must pass a single color if using `uniform_colors=True`") + raise TypeError( + "must pass a single color if using `uniform_colors=True`" + ) self._colors = UniformColor(colors, alpha=alpha) self._cmap = None else: diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 9d23946fc..ee6bd2c70 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -25,7 +25,12 @@ class _ImageTile(pygfx.Image): """ def __init__( - self, geometry, material, data_slice: tuple[slice, slice], chunk_index: tuple[int, int], **kwargs + self, + geometry, + material, + data_slice: tuple[slice, slice], + chunk_index: tuple[int, int], + **kwargs, ): super().__init__(geometry, material, **kwargs) @@ -35,7 +40,10 @@ def __init__( def _wgpu_get_pick_info(self, pick_value): pick_info = super()._wgpu_get_pick_info(pick_value) - data_row_start, data_col_start = self.data_slice[0].start, self.data_slice[1].start + data_row_start, data_col_start = ( + self.data_slice[0].start, + self.data_slice[1].start, + ) # add the actual data row and col start indices x, y = pick_info["index"] @@ -52,7 +60,7 @@ def _wgpu_get_pick_info(self, pick_value): return { **pick_info, "data_slice": self.data_slice, - "chunk_index": self.chunk_index + "chunk_index": self.chunk_index, } @property @@ -221,7 +229,7 @@ def __init__( geometry=pygfx.Geometry(grid=texture), material=self._material, data_slice=data_slice, # used to parse pick_info - chunk_index=chunk_index + chunk_index=chunk_index, ) # row and column start index for this chunk diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index 9f1714206..cbc622255 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -2,7 +2,13 @@ import numpy as np from ._base import Graphic -from ._features import TextData, FontSize, TextFaceColor, TextOutlineColor, TextOutlineThickness +from ._features import ( + TextData, + FontSize, + TextFaceColor, + TextOutlineColor, + TextOutlineThickness, +) class TextGraphic(Graphic): @@ -94,7 +100,7 @@ def text(self, text: str): @property def font_size(self) -> float | int: - """"text font size""" + """ "text font size""" return self._font_size.value @font_size.setter diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index a9d978008..2494a9361 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -14,40 +14,47 @@ PointsSizesFeature, ) -from .utils import generate_positions_spiral_data, generate_color_inputs, MULTI_COLORS_TRUTH +from .utils import ( + generate_positions_spiral_data, + generate_color_inputs, + MULTI_COLORS_TRUTH, +) TRUTH_CMAPS = { "jet": np.array( - [[0. , 0. , 0.5 , 1. ], - [0. , 0. , 0.99910873, 1. ], - [0. , 0.37843138, 1. , 1. ], - [0. , 0.8333333 , 1. , 1. ], - [0.30044276, 1. , 0.66729915, 1. ], - [0.65464896, 1. , 0.31309298, 1. ], - [1. , 0.90123457, 0. , 1. ], - [1. , 0.4945534 , 0. , 1. ], - [1. , 0.08787218, 0. , 1. ], - [0.5 , 0. , 0. , 1. ]], - dtype=np.float32 -), - -"viridis": np.array( - [[0.267004, 0.004874, 0.329415, 1. ], - [0.281412, 0.155834, 0.469201, 1. ], - [0.244972, 0.287675, 0.53726 , 1. ], - [0.190631, 0.407061, 0.556089, 1. ], - [0.147607, 0.511733, 0.557049, 1. ], - [0.119483, 0.614817, 0.537692, 1. ], - [0.20803 , 0.718701, 0.472873, 1. ], - [0.421908, 0.805774, 0.35191 , 1. ], - [0.699415, 0.867117, 0.175971, 1. ], - [0.993248, 0.906157, 0.143936, 1. ]], - dtype=np.float32) + [ + [0.0, 0.0, 0.5, 1.0], + [0.0, 0.0, 0.99910873, 1.0], + [0.0, 0.37843138, 1.0, 1.0], + [0.0, 0.8333333, 1.0, 1.0], + [0.30044276, 1.0, 0.66729915, 1.0], + [0.65464896, 1.0, 0.31309298, 1.0], + [1.0, 0.90123457, 0.0, 1.0], + [1.0, 0.4945534, 0.0, 1.0], + [1.0, 0.08787218, 0.0, 1.0], + [0.5, 0.0, 0.0, 1.0], + ], + dtype=np.float32, + ), + "viridis": np.array( + [ + [0.267004, 0.004874, 0.329415, 1.0], + [0.281412, 0.155834, 0.469201, 1.0], + [0.244972, 0.287675, 0.53726, 1.0], + [0.190631, 0.407061, 0.556089, 1.0], + [0.147607, 0.511733, 0.557049, 1.0], + [0.119483, 0.614817, 0.537692, 1.0], + [0.20803, 0.718701, 0.472873, 1.0], + [0.421908, 0.805774, 0.35191, 1.0], + [0.699415, 0.867117, 0.175971, 1.0], + [0.993248, 0.906157, 0.143936, 1.0], + ], + dtype=np.float32, + ), } - def test_data_slice(): pass @@ -76,15 +83,13 @@ def test_uniform_size(): pass -@pytest.mark.parametrize( - "graphic_type", ["line", "scatter"] -) +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) @pytest.mark.parametrize( "data", [generate_positions_spiral_data(v) for v in ["y", "xy", "xyz"]] ) def test_positions_graphics_data( - graphic_type, - data, + graphic_type, + data, ): # tests with multi-lines @@ -114,23 +119,15 @@ def test_positions_graphics_data( npt.assert_almost_equal(graphic.data[:], data[:]) # everything must match -@pytest.mark.parametrize( - "graphic_type", ["line", "scatter"] -) -@pytest.mark.parametrize( - "colors", [None, *generate_color_inputs("r")] -) -@pytest.mark.parametrize( - "uniform_color", [None, False] -) -@pytest.mark.parametrize( - "alpha", [None, 0.5, 0.0] -) +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize("colors", [None, *generate_color_inputs("r")]) +@pytest.mark.parametrize("uniform_color", [None, False]) +@pytest.mark.parametrize("alpha", [None, 0.5, 0.0]) def test_positions_graphic_vertex_colors( - graphic_type, - colors, - uniform_color, - alpha, + graphic_type, + colors, + uniform_color, + alpha, ): fig = fpl.Figure() @@ -147,7 +144,6 @@ def test_positions_graphic_vertex_colors( elif graphic_type == "scatter": graphic = fig[0, 0].add_scatter(data=data, **kwargs) - if alpha is None: # default arg alpha = 1 @@ -159,44 +155,38 @@ def test_positions_graphic_vertex_colors( if colors is None: # default - npt.assert_almost_equal(graphic.colors.value, - np.repeat([[1, 1, 1, alpha]], repeats=len(graphic.data), axis=0)) + npt.assert_almost_equal( + graphic.colors.value, + np.repeat([[1, 1, 1, alpha]], repeats=len(graphic.data), axis=0), + ) else: if len(colors) != len(graphic.data): # should be single red, regardless of input variant (i.e. str, array, RGBA tuple, etc. - npt.assert_almost_equal(graphic.colors.value, - np.repeat([[1, 0, 0, alpha]], repeats=len(graphic.data), axis=0)) + npt.assert_almost_equal( + graphic.colors.value, + np.repeat([[1, 0, 0, alpha]], repeats=len(graphic.data), axis=0), + ) else: # multi colors # use the truth for multi colors test that is pre-set npt.assert_almost_equal(graphic.colors.value, MULTI_COLORS_TRUTH) -@pytest.mark.parametrize( - "graphic_type", ["line", "scatter"] -) -@pytest.mark.parametrize( - "colors", [None, *generate_color_inputs("r")] -) -@pytest.mark.parametrize( - "uniform_color", [None, False] -) -@pytest.mark.parametrize( - "cmap", ["jet", "viridis"] -) +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize("colors", [None, *generate_color_inputs("r")]) +@pytest.mark.parametrize("uniform_color", [None, False]) +@pytest.mark.parametrize("cmap", ["jet", "viridis"]) @pytest.mark.parametrize( "cmap_values", [None, [3, 5, 2, 1, 0, 6, 9, 7, 4, 8], np.arange(9, -1, -1)] ) -@pytest.mark.parametrize( - "alpha", [None, 0.5, 0.0] -) +@pytest.mark.parametrize("alpha", [None, 0.5, 0.0]) def test_cmap( - graphic_type, - colors, - uniform_color, - cmap, - cmap_values, - alpha, + graphic_type, + colors, + uniform_color, + cmap, + cmap_values, + alpha, ): fig = fpl.Figure() @@ -233,24 +223,13 @@ def test_cmap( npt.assert_almost_equal(graphic.colors.value, truth) -@pytest.mark.parametrize( - "graphic_type", ["line", "scatter"] -) -@pytest.mark.parametrize( - "cmap", ["jet"] -) -@pytest.mark.parametrize( - "colors", [*generate_color_inputs("multi")] -) +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize("cmap", ["jet"]) +@pytest.mark.parametrize("colors", [*generate_color_inputs("multi")]) @pytest.mark.parametrize( "uniform_color", [True] # none of these will work with a uniform buffer ) -def test_incompatible_args( - graphic_type, - cmap, - colors, - uniform_color -): +def test_incompatible_args(graphic_type, cmap, colors, uniform_color): fig = fpl.Figure() kwargs = dict() @@ -268,6 +247,7 @@ def test_incompatible_args( with pytest.raises(TypeError): graphic = fig[0, 0].add_scatter(data=data, **kwargs) + # @pytest.mark.parametrize( # "sizes", [None, 5.0, np.linspace(3, 8, 10)] # ) @@ -282,8 +262,6 @@ def test_incompatible_args( # pass - - def test_line_feature_events(): pass diff --git a/tests/utils.py b/tests/utils.py index a1ef53832..6a25968e1 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -140,9 +140,22 @@ def generate_positions_spiral_data(inputs: str) -> np.ndarray: return data.astype(np.float32) -def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple] | list[str, np.ndarray]: +def generate_color_inputs( + name: str, +) -> list[str, np.ndarray, list, tuple] | list[str, np.ndarray]: if name == "multi": - s = ["r", "g", "b", "cyan", "magenta", "green", "yellow", "white", "purple", "orange"] + s = [ + "r", + "g", + "b", + "cyan", + "magenta", + "green", + "yellow", + "white", + "purple", + "orange", + ] array = np.vstack([pygfx.Color(c) for c in s]) return [s, array] @@ -157,14 +170,16 @@ def generate_color_inputs(name: str) -> list[str, np.ndarray, list, tuple] | lis MULTI_COLORS_TRUTH = np.array( - [[1.0, 0.0, 0.0, 1.0], - [0.0, 1.0, 0.0, 1.0], - [0.0, 0.0, 1.0, 1.0], - [0.0, 1.0, 1.0, 1.0], - [1.0, 0.0, 1.0, 1.0], - [0.0, 0.501960813999176, 0.0, 1.0], - [1.0, 1.0, 0.0, 1.0], - [1.0, 1.0, 1.0, 1.0], - [0.501960813999176, 0.0, 0.501960813999176, 1.0], - [1.0, 0.6470588445663452, 0.0, 1.0]] + [ + [1.0, 0.0, 0.0, 1.0], + [0.0, 1.0, 0.0, 1.0], + [0.0, 0.0, 1.0, 1.0], + [0.0, 1.0, 1.0, 1.0], + [1.0, 0.0, 1.0, 1.0], + [0.0, 0.501960813999176, 0.0, 1.0], + [1.0, 1.0, 0.0, 1.0], + [1.0, 1.0, 1.0, 1.0], + [0.501960813999176, 0.0, 0.501960813999176, 1.0], + [1.0, 0.6470588445663452, 0.0, 1.0], + ] ) From f01811ac655937bb5c9c63c9243103218d1cee91 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Jun 2024 03:11:34 -0400 Subject: [PATCH 109/196] uniform colors tests and bug fix --- fastplotlib/graphics/line.py | 2 +- tests/test_positions_graphics.py | 53 ++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index d94cbf2f5..fea478e7a 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -88,7 +88,7 @@ def __init__( if uniform_color: geometry = pygfx.Geometry(positions=self._data.buffer) material = MaterialCls( - thickness=self.thickness, color_mode="uniform", pick_write=True + thickness=self.thickness, color_mode="uniform", color=self.colors, pick_write=True ) else: material = MaterialCls( diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index 2494a9361..dff5daf08 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -75,8 +75,55 @@ def test_change_thickness(): pass -def test_uniform_color(): - pass +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize("colors", [None, *generate_color_inputs("b")]) +@pytest.mark.parametrize( + "uniform_color", [True, False] +) +@pytest.mark.parametrize( + "alpha", [1.0, 0.5, 0.0] +) +def test_uniform_color(graphic_type, colors, uniform_color, alpha): + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["colors", "uniform_color", "alpha"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + if graphic_type == "line": + graphic = fig[0, 0].add_line(data=data, **kwargs) + elif graphic_type == "scatter": + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + + if uniform_color: + assert isinstance(graphic._colors, UniformColor) + assert isinstance(graphic.colors, pygfx.Color) + if colors is None: + # default white + assert graphic.colors == pygfx.Color([1, 1, 1, alpha]) + else: + # should be blue + assert graphic.colors == pygfx.Color([0, 0, 1, alpha]) + + # check pygfx material + npt.assert_almost_equal(graphic.world_object.material.color, np.asarray(graphic.colors)) + else: + assert isinstance(graphic._colors, VertexColors) + assert isinstance(graphic.colors, VertexColors) + if colors is None: + # default white + npt.assert_almost_equal(graphic.colors.value, + np.repeat([[1, 1, 1, alpha]], repeats=len(graphic.data), axis=0)) + else: + # blue + npt.assert_almost_equal(graphic.colors.value, np.repeat([[0, 0, 1, alpha]], repeats=len(graphic.data), axis=0)) + + # check geometry + npt.assert_almost_equal(graphic.world_object.geometry.colors.data, graphic.colors.value) def test_uniform_size(): @@ -225,7 +272,7 @@ def test_cmap( @pytest.mark.parametrize("graphic_type", ["line", "scatter"]) @pytest.mark.parametrize("cmap", ["jet"]) -@pytest.mark.parametrize("colors", [*generate_color_inputs("multi")]) +@pytest.mark.parametrize("colors", [None, *generate_color_inputs("multi")]) # cmap arg overrides colors @pytest.mark.parametrize( "uniform_color", [True] # none of these will work with a uniform buffer ) From 59b555093fc8805b9c6e14216b93339b6f3086d4 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Jun 2024 03:12:28 -0400 Subject: [PATCH 110/196] black --- tests/test_positions_graphics.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index dff5daf08..c47d673d7 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -77,12 +77,8 @@ def test_change_thickness(): @pytest.mark.parametrize("graphic_type", ["line", "scatter"]) @pytest.mark.parametrize("colors", [None, *generate_color_inputs("b")]) -@pytest.mark.parametrize( - "uniform_color", [True, False] -) -@pytest.mark.parametrize( - "alpha", [1.0, 0.5, 0.0] -) +@pytest.mark.parametrize("uniform_color", [True, False]) +@pytest.mark.parametrize("alpha", [1.0, 0.5, 0.0]) def test_uniform_color(graphic_type, colors, uniform_color, alpha): fig = fpl.Figure() @@ -110,20 +106,29 @@ def test_uniform_color(graphic_type, colors, uniform_color, alpha): assert graphic.colors == pygfx.Color([0, 0, 1, alpha]) # check pygfx material - npt.assert_almost_equal(graphic.world_object.material.color, np.asarray(graphic.colors)) + npt.assert_almost_equal( + graphic.world_object.material.color, np.asarray(graphic.colors) + ) else: assert isinstance(graphic._colors, VertexColors) assert isinstance(graphic.colors, VertexColors) if colors is None: # default white - npt.assert_almost_equal(graphic.colors.value, - np.repeat([[1, 1, 1, alpha]], repeats=len(graphic.data), axis=0)) + npt.assert_almost_equal( + graphic.colors.value, + np.repeat([[1, 1, 1, alpha]], repeats=len(graphic.data), axis=0), + ) else: # blue - npt.assert_almost_equal(graphic.colors.value, np.repeat([[0, 0, 1, alpha]], repeats=len(graphic.data), axis=0)) + npt.assert_almost_equal( + graphic.colors.value, + np.repeat([[0, 0, 1, alpha]], repeats=len(graphic.data), axis=0), + ) # check geometry - npt.assert_almost_equal(graphic.world_object.geometry.colors.data, graphic.colors.value) + npt.assert_almost_equal( + graphic.world_object.geometry.colors.data, graphic.colors.value + ) def test_uniform_size(): @@ -272,7 +277,9 @@ def test_cmap( @pytest.mark.parametrize("graphic_type", ["line", "scatter"]) @pytest.mark.parametrize("cmap", ["jet"]) -@pytest.mark.parametrize("colors", [None, *generate_color_inputs("multi")]) # cmap arg overrides colors +@pytest.mark.parametrize( + "colors", [None, *generate_color_inputs("multi")] +) # cmap arg overrides colors @pytest.mark.parametrize( "uniform_color", [True] # none of these will work with a uniform buffer ) From cd40638b31238ee965bfbda526114e58a0b71fe3 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Jun 2024 03:13:37 -0400 Subject: [PATCH 111/196] bugfix uniform color and sizes --- fastplotlib/graphics/scatter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 1cbf098dd..f1cdbfa2b 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -107,14 +107,14 @@ def __init__( if uniform_color: material_kwargs["color_mode"] = "uniform" - material_kwargs["color"] = self.colors.value + material_kwargs["color"] = self.colors else: material_kwargs["color_mode"] = "vertex" geo_kwargs["colors"] = self.colors.buffer if uniform_sizes: material_kwargs["size_mode"] = "uniform" - material_kwargs["size"] = self.sizes.value + material_kwargs["size"] = self.sizes else: material_kwargs["size_mode"] = "vertex" geo_kwargs["sizes"] = self.sizes.buffer From 10ba958733c3e94936e82880145384a4c3d5f7de Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Jun 2024 03:52:01 -0400 Subject: [PATCH 112/196] sizes and thickness tests --- tests/test_positions_graphics.py | 70 ++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index c47d673d7..2a3cedbc9 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -12,6 +12,7 @@ UniformColor, UniformSizes, PointsSizesFeature, + Thickness ) from .utils import ( @@ -153,6 +154,9 @@ def test_positions_graphics_data( elif graphic_type == "scatter": graphic = fig[0, 0].add_scatter(data=data) + assert isinstance(graphic._data, VertexPositions) + assert isinstance(graphic.data, VertexPositions) + # n_datapoints must match assert len(graphic.data.value) == len(data) @@ -265,6 +269,8 @@ def test_cmap( if cmap_values is not None: truth = truth[cmap_values] + assert isinstance(graphic._cmap, VertexCmap) + assert graphic.cmap.name == cmap # make sure buffer is identical @@ -302,18 +308,58 @@ def test_incompatible_args(graphic_type, cmap, colors, uniform_color): graphic = fig[0, 0].add_scatter(data=data, **kwargs) -# @pytest.mark.parametrize( -# "sizes", [None, 5.0, np.linspace(3, 8, 10)] -# ) -# def test_sizes(): -# pass -# -# -# @pytest.mark.parametrize( -# "thickness", [None, 5.0] -# ) -# def test_thicnkess(): -# pass +@pytest.mark.parametrize( + "sizes", [None, 5.0, np.linspace(3, 8, 10, dtype=np.float32)] +) +def test_sizes(sizes): + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["sizes"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + + assert len(data) == len(graphic.sizes) + + if sizes is None: + sizes = 1 # default sizes + + npt.assert_almost_equal(graphic.sizes.value, sizes) + + +@pytest.mark.parametrize( + "thickness", [None, 0.5, 5.0] +) +def test_thicnkess(thickness): + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["thickness"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + graphic = fig[0, 0].add_line(data=data, **kwargs) + + if thickness is None: + thickness = 2.0 # default thickness + + assert isinstance(graphic._thickness, Thickness) + + assert graphic.thickness == thickness + + if thickness == 0.5: + assert isinstance(graphic.world_object.material, pygfx.LineThinMaterial) + + else: + assert isinstance(graphic.world_object.material, pygfx.LineMaterial) def test_line_feature_events(): From e99010e3fec71bbd1b6fa3725305d3a40c283398 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Jun 2024 03:54:45 -0400 Subject: [PATCH 113/196] tests update --- tests/test_positions_graphics.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index 2a3cedbc9..2e20d75cf 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -132,10 +132,6 @@ def test_uniform_color(graphic_type, colors, uniform_color, alpha): ) -def test_uniform_size(): - pass - - @pytest.mark.parametrize("graphic_type", ["line", "scatter"]) @pytest.mark.parametrize( "data", [generate_positions_spiral_data(v) for v in ["y", "xy", "xyz"]] @@ -324,6 +320,8 @@ def test_sizes(sizes): graphic = fig[0, 0].add_scatter(data=data, **kwargs) + assert isinstance(graphic.sizes, PointsSizesFeature) + assert isinstance(graphic._sizes, PointsSizesFeature) assert len(data) == len(graphic.sizes) if sizes is None: @@ -332,6 +330,10 @@ def test_sizes(sizes): npt.assert_almost_equal(graphic.sizes.value, sizes) +def test_uniform_size(): + pass + + @pytest.mark.parametrize( "thickness", [None, 0.5, 5.0] ) From 3acabe35a64ec9ae42f1e335a65e04eb52b8aa7d Mon Sep 17 00:00:00 2001 From: Caitlin Date: Fri, 7 Jun 2024 19:03:44 -0400 Subject: [PATCH 114/196] lotta team work --- fastplotlib/graphics/_collection_base.py | 67 ++++++++++++++++++------ fastplotlib/graphics/line_collection.py | 7 +-- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index 50e6ffb19..caa28859e 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -64,7 +64,7 @@ def __init__(self, selection: np.ndarray[Graphic], features: set[str]): """ self._selection = selection - self._features = features + self.features = features @property def graphics(self) -> np.ndarray[Graphic]: @@ -110,24 +110,17 @@ def my_handler(event): """ decorating = not callable(args[0]) - callback = None if decorating else args[0] types = args if decorating else args[1:] - if not all(t in set(PYGFX_EVENTS).union(self._features) for t in types): - raise KeyError( - f"event types must be strings for a valid event from the following:\n" - f"{PYGFX_EVENTS + list(self._features)}" - ) - - def decorator(_callback): - for g in self.graphics: - g.add_event_handler(_callback, types) - return _callback - if decorating: + def decorator(_callback): + for g in self.graphics: + g.add_event_handler(_callback, *types) + return _callback return decorator - return decorator(callback) + for g in self.graphics: + g.add_event_handler(*args) def remove_event_handler(self, callback, *types): for g in self.graphics: @@ -152,6 +145,10 @@ class GraphicCollection(Graphic): child_type: type _indexer: type + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + cls.features = cls.child_type.features + def __init__(self, name: str = None): super().__init__(name) @@ -222,14 +219,52 @@ def remove_graphic(self, graphic: Graphic): self._graphics_changed = True def add_event_handler(self, *args): - raise NotImplementedError("Slice graphic collection to add event handlers") + """ + Register an event handler. + + Parameters + ---------- + callback: callable, the first argument + Event handler, must accept a single event argument + *types: list of strings + A list of event types, ex: "click", "data", "colors", "pointer_down" + + For the available renderer event types, see + https://jupyter-rfb.readthedocs.io/en/stable/events.html + + All feature support events, i.e. ``graphic.features`` will give a set of + all features that are evented + + Can also be used as a decorator. + + Example + ------- + + .. code-block:: py + + def my_handler(event): + print(event) + + graphic.add_event_handler(my_handler, "pointer_up", "pointer_down") + + Decorator usage example: + + .. code-block:: py + + @graphic.add_event_handler("click") + def my_handler(event): + print(event) + """ + + return self[:].add_event_handler(*args) def remove_event_handler(self, callback, *types): - raise NotImplementedError("Slice graphic collection to remove event handlers") + self[:].remove_event_handler(callback, *types) def __getitem__(self, key) -> CollectionIndexer: return self._indexer( selection=self.graphics[key], + features=self.features ) def __del__(self): diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 6c9975c95..e6f4442c8 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -11,7 +11,8 @@ from .selectors import LinearRegionSelector, LinearSelector -class LineSelection(CollectionIndexer): +class LineCollectionProperties: + """Mix-in class for LineCollection properties""" @property def colors(self) -> CollectionFeature: return CollectionFeature(self.graphics, "colors") @@ -79,9 +80,9 @@ def thickness(self, values: np.ndarray | list[float]): g.thickness = v -class LineCollection(GraphicCollection): +class LineCollection(GraphicCollection, LineCollectionProperties): child_type = LineGraphic - _indexer = LineSelection + _indexer = CollectionIndexer def __init__( self, From 072fd910ddb18a174d591ac2222b844d732e1601 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Jun 2024 00:24:17 -0400 Subject: [PATCH 115/196] docstring, small things --- fastplotlib/graphics/scatter.py | 48 +++++++++++++++------------------ 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index f1cdbfa2b..0102229e4 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -4,7 +4,7 @@ import pygfx from ._positions_base import PositionsGraphic -from ._features import PointsSizesFeature, UniformSizes +from ._features import PointsSizesFeature, UniformSize class ScatterGraphic(PositionsGraphic): @@ -16,7 +16,7 @@ def sizes(self) -> PointsSizesFeature | float: if isinstance(self._sizes, PointsSizesFeature): return self._sizes - elif isinstance(self._sizes, UniformSizes): + elif isinstance(self._sizes, UniformSize): return self._sizes.value @sizes.setter @@ -24,7 +24,7 @@ def sizes(self, value): if isinstance(self._sizes, PointsSizesFeature): self._sizes[:] = value - elif isinstance(self._sizes, UniformSizes): + elif isinstance(self._sizes, UniformSize): self._sizes.set_value(self, value) def __init__( @@ -37,7 +37,7 @@ def __init__( cmap_values: np.ndarray = None, isolated_buffer: bool = True, sizes: float | np.ndarray | Iterable[float] = 1, - uniform_sizes: bool = False, + uniform_size: bool = False, **kwargs, ): """ @@ -48,13 +48,17 @@ def __init__( data: array-like Scatter data to plot, 2D must be of shape [n_points, 2], 3D must be of shape [n_points, 3] - sizes: float or iterable of float, optional, default 1.0 - size of the scatter points - 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 scatter point colors, + 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 scatter instead of assigning colors manually, this overrides any argument passed to "colors" @@ -62,30 +66,20 @@ def __init__( cmap_values: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap - alpha: float, optional, default 1.0 - alpha value for the colors + isolated_buffer: bool, default True + whether the buffers should be isolated from the user input array. + Generally always ``True``, ``False`` is for rare advanced use. - z_position: float, optional - z-axis position for placing the graphic + sizes: float or iterable of float, optional, default 1.0 + size of the scatter points + + uniform_size: bool, default False + if True, uses a uniform buffer for the scatter point sizes, + basically saves GPU VRAM when all scatter points are the same size kwargs passed to Graphic - Features - -------- - - **data**: :class:`.ImageDataFeature` - Manages the line [x, y, z] positions data buffer, allows regular and fancy indexing. - - **colors**: :class:`.ColorFeature` - Manages the color buffer, allows regular and fancy indexing. - - **cmap**: :class:`.CmapFeature` - Manages the cmap, wraps :class:`.ColorFeature` to add additional functionality relevant to cmaps. - - **present**: :class:`.PresentFeature` - Control the presence of the Graphic in the scene, set to ``True`` or ``False`` - """ super().__init__( @@ -112,7 +106,7 @@ def __init__( material_kwargs["color_mode"] = "vertex" geo_kwargs["colors"] = self.colors.buffer - if uniform_sizes: + if uniform_size: material_kwargs["size_mode"] = "uniform" material_kwargs["size"] = self.sizes else: From 2972e84208c67dc7d3b66192393e17b4a1016be8 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Jun 2024 00:25:28 -0400 Subject: [PATCH 116/196] rename UniformSizes -> UniformSize --- fastplotlib/graphics/_features/__init__.py | 2 +- .../graphics/_features/_positions_graphics.py | 2 +- tests/test_positions_graphics.py | 40 ++++++++++++++++--- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index d6d1e6465..b9265a1a0 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -1,7 +1,7 @@ from ._positions_graphics import ( VertexColors, UniformColor, - UniformSizes, + UniformSize, Thickness, VertexPositions, PointsSizesFeature, diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index 0b6f4b8f1..987aa213f 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -154,7 +154,7 @@ def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Colo self._call_event_handlers(event) -class UniformSizes(GraphicFeature): +class UniformSize(GraphicFeature): def __init__(self, value: int | float): self._value = float(value) super().__init__() diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index 2e20d75cf..15211b082 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -10,7 +10,7 @@ VertexColors, VertexCmap, UniformColor, - UniformSizes, + UniformSize, PointsSizesFeature, Thickness ) @@ -285,7 +285,7 @@ def test_cmap( @pytest.mark.parametrize( "uniform_color", [True] # none of these will work with a uniform buffer ) -def test_incompatible_args(graphic_type, cmap, colors, uniform_color): +def test_incompatible_color_args(graphic_type, cmap, colors, uniform_color): fig = fpl.Figure() kwargs = dict() @@ -307,7 +307,10 @@ def test_incompatible_args(graphic_type, cmap, colors, uniform_color): @pytest.mark.parametrize( "sizes", [None, 5.0, np.linspace(3, 8, 10, dtype=np.float32)] ) -def test_sizes(sizes): +@pytest.mark.parametrize( + "uniform_size", [None, False] +) +def test_sizes(sizes, uniform_size): fig = fpl.Figure() kwargs = dict() @@ -328,10 +331,37 @@ def test_sizes(sizes): sizes = 1 # default sizes npt.assert_almost_equal(graphic.sizes.value, sizes) + npt.assert_almost_equal(graphic.world_object.geometry.sizes.data, graphic.sizes.value) -def test_uniform_size(): - pass +@pytest.mark.parametrize( + "sizes", [None, 5.0] +) +@pytest.mark.parametrize( + "uniform_size", [True] +) +@pytest.mark.parametrize() +def test_uniform_size(sizes, uniform_size): + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["sizes"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + + assert isinstance(graphic.sizes, (float, int)) + assert isinstance(graphic._sizes, UniformSize) + + if sizes is None: + sizes = 1 # default sizes + + npt.assert_almost_equal(graphic.sizes, sizes) + npt.assert_almost_equal(graphic.world_object.material.size, sizes) @pytest.mark.parametrize( From e07cbe87fe95b0ccbc32257a085f7016e659dfe4 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Jun 2024 00:32:43 -0400 Subject: [PATCH 117/196] update graphic methods mixin --- fastplotlib/layouts/_graphic_methods_mixin.py | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index e92e3aabc..fecc4cc60 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -332,7 +332,7 @@ def add_scatter( cmap_values: numpy.ndarray = None, isolated_buffer: bool = True, sizes: Union[float, numpy.ndarray, Iterable[float]] = 1, - uniform_sizes: bool = False, + uniform_size: bool = False, **kwargs ) -> ScatterGraphic: """ @@ -344,13 +344,17 @@ def add_scatter( data: array-like Scatter data to plot, 2D must be of shape [n_points, 2], 3D must be of shape [n_points, 3] - sizes: float or iterable of float, optional, default 1.0 - size of the scatter points - 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 scatter point colors, + 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 scatter instead of assigning colors manually, this overrides any argument passed to "colors" @@ -358,30 +362,20 @@ def add_scatter( cmap_values: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap - alpha: float, optional, default 1.0 - alpha value for the colors + isolated_buffer: bool, default True + whether the buffers should be isolated from the user input array. + Generally always ``True``, ``False`` is for rare advanced use. - z_position: float, optional - z-axis position for placing the graphic + sizes: float or iterable of float, optional, default 1.0 + size of the scatter points + + uniform_size: bool, default False + if True, uses a uniform buffer for the scatter point sizes, + basically saves GPU VRAM when all scatter points are the same size kwargs passed to Graphic - Features - -------- - - **data**: :class:`.ImageDataFeature` - Manages the line [x, y, z] positions data buffer, allows regular and fancy indexing. - - **colors**: :class:`.ColorFeature` - Manages the color buffer, allows regular and fancy indexing. - - **cmap**: :class:`.CmapFeature` - Manages the cmap, wraps :class:`.ColorFeature` to add additional functionality relevant to cmaps. - - **present**: :class:`.PresentFeature` - Control the presence of the Graphic in the scene, set to ``True`` or ``False`` - """ return self._create_graphic( @@ -394,7 +388,7 @@ def add_scatter( cmap_values, isolated_buffer, sizes, - uniform_sizes, + uniform_size, **kwargs ) From f3a8f95f5a9cd11aad85c123590131d2b9f57df6 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Jun 2024 00:40:18 -0400 Subject: [PATCH 118/196] update graphic methods mixin --- fastplotlib/graphics/scatter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 0102229e4..c32adc02a 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -94,7 +94,6 @@ def __init__( ) n_datapoints = self.data.value.shape[0] - self._sizes = PointsSizesFeature(sizes, n_datapoints=n_datapoints) geo_kwargs = {"positions": self._data.buffer} material_kwargs = {"pick_write": True} @@ -108,9 +107,11 @@ def __init__( if uniform_size: material_kwargs["size_mode"] = "uniform" + self._sizes = UniformSize(sizes) material_kwargs["size"] = self.sizes else: material_kwargs["size_mode"] = "vertex" + self._sizes = PointsSizesFeature(sizes, n_datapoints=n_datapoints) geo_kwargs["sizes"] = self.sizes.buffer world_object = pygfx.Points( From 06408a6097fc8fa78d257f8bf346c964d14c57de Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Jun 2024 00:41:00 -0400 Subject: [PATCH 119/196] bugfix --- tests/test_positions_graphics.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index 15211b082..fabbe72ff 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -285,7 +285,7 @@ def test_cmap( @pytest.mark.parametrize( "uniform_color", [True] # none of these will work with a uniform buffer ) -def test_incompatible_color_args(graphic_type, cmap, colors, uniform_color): +def test_incompatible_cmap_color_args(graphic_type, cmap, colors, uniform_color): fig = fpl.Figure() kwargs = dict() @@ -304,6 +304,32 @@ def test_incompatible_color_args(graphic_type, cmap, colors, uniform_color): graphic = fig[0, 0].add_scatter(data=data, **kwargs) +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize( + "colors", [*generate_color_inputs("multi")] +) +@pytest.mark.parametrize( + "uniform_color", [True] # none of these will work with a uniform buffer +) +def test_incompatible_color_args(graphic_type, colors, uniform_color): + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["colors", "uniform_color"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + if graphic_type == "line": + with pytest.raises(TypeError): + graphic = fig[0, 0].add_line(data=data, **kwargs) + elif graphic_type == "scatter": + with pytest.raises(TypeError): + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + + @pytest.mark.parametrize( "sizes", [None, 5.0, np.linspace(3, 8, 10, dtype=np.float32)] ) @@ -340,12 +366,11 @@ def test_sizes(sizes, uniform_size): @pytest.mark.parametrize( "uniform_size", [True] ) -@pytest.mark.parametrize() def test_uniform_size(sizes, uniform_size): fig = fpl.Figure() kwargs = dict() - for kwarg in ["sizes"]: + for kwarg in ["sizes", "uniform_size"]: if locals()[kwarg] is not None: # add to dict of arguments that will be passed kwargs[kwarg] = locals()[kwarg] From a2f2e6fd1125dca325020e119df0dcdbf79d68ea Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Jun 2024 01:05:45 -0400 Subject: [PATCH 120/196] test data slice for positiosn graphics --- tests/test_positions_graphics.py | 66 ++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index fabbe72ff..d6e8b9b8a 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -19,6 +19,8 @@ generate_positions_spiral_data, generate_color_inputs, MULTI_COLORS_TRUTH, + generate_slice_indices, + assert_pending_uploads, ) @@ -56,8 +58,65 @@ } -def test_data_slice(): - pass +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize( + "slice_method", [generate_slice_indices(i) for i in range(1, 16)] +) # same as slice methods in the buffer tests +@pytest.mark.parametrize("test_axis", ["y", "xy", "xyz"]) +def test_data_slice(graphic_type, slice_method, test_axis): + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + + if graphic_type == "line": + graphic = fig[0, 0].add_line(data=data) + + elif graphic_type == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + s = slice_method["slice"] + indices = slice_method["indices"] + offset = slice_method["offset"] + size = slice_method["size"] + others = slice_method["others"] + + # TODO: placeholder until I make a testing figure where we draw frames only on call + graphic.data.buffer._gfx_pending_uploads.clear() + + match test_axis: + case "y": + graphic.data[s, 1] = -data[s, 1] + npt.assert_almost_equal(graphic.data[s, 1], -data[s, 1]) + npt.assert_almost_equal(graphic.data[indices, 1], -data[indices, 1]) + # make sure other points are not modified + npt.assert_almost_equal( + graphic.data[others, 1], data[others, 1] + ) # other points in same dimension + npt.assert_almost_equal( + graphic.data[:, 2:], data[:, 2:] + ) # dimensions that are not sliced + + case "xy": + graphic.data[s, :-1] = -data[s, :-1] + npt.assert_almost_equal(graphic.data[s, :-1], -data[s, :-1]) + npt.assert_almost_equal(graphic.data[indices, :-1], -data[s, :-1]) + # make sure other points are not modified + npt.assert_almost_equal( + graphic.data[others, :-1], data[others, :-1] + ) # other points in the same dimensions + npt.assert_almost_equal( + graphic.data[:, -1], data[:, -1] + ) # dimensions that are not touched + + case "xyz": + graphic.data[s] = -data[s] + npt.assert_almost_equal(graphic.data[s], -data[s]) + npt.assert_almost_equal(graphic.data[indices], -data[s]) + # make sure other points are not modified + npt.assert_almost_equal(graphic.data[others], data[others]) + + # make sure correct offset and size marked for pending upload + assert_pending_uploads(graphic.data.buffer, offset, size) def test_color_slice(): @@ -140,8 +199,7 @@ def test_positions_graphics_data( graphic_type, data, ): - # tests with multi-lines - + # tests with different ways of passing positions data, x, xy and xyz fig = fpl.Figure() if graphic_type == "line": From 71c06e0855c93279caefa40bff7af7b4987802ad Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Jun 2024 02:35:52 -0400 Subject: [PATCH 121/196] test colors property within buffer tests --- tests/test_colors_buffer_manager.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py index 4d8cbc242..00456d6ec 100644 --- a/tests/test_colors_buffer_manager.py +++ b/tests/test_colors_buffer_manager.py @@ -4,8 +4,9 @@ import pygfx +import fastplotlib as fpl from fastplotlib.graphics._features import VertexColors -from .utils import generate_slice_indices, assert_pending_uploads, generate_color_inputs +from .utils import generate_slice_indices, assert_pending_uploads, generate_color_inputs, generate_positions_spiral_data def make_colors_buffer() -> VertexColors: @@ -107,9 +108,24 @@ def test_tuple(slice_method): @pytest.mark.parametrize( "slice_method", [generate_slice_indices(i) for i in range(1, 16)] ) -def test_slice(color_input, slice_method: dict): +@pytest.mark.parametrize( + "test_graphic", [False, "line", "scatter"] +) +def test_slice(color_input, slice_method: dict, test_graphic: bool): # slicing only first dim - colors = make_colors_buffer() + if test_graphic: + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + if test_graphic == "line": + graphic = fig[0, 0].add_line(data=data) + + elif test_graphic == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + colors = graphic.colors + else: + colors = make_colors_buffer() # TODO: placeholder until I make a testing figure where we draw frames only on call colors.buffer._gfx_pending_uploads.clear() From b5e9f2c4bcaea32a5616c60fa713bb86008e475b Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Jun 2024 02:36:08 -0400 Subject: [PATCH 122/196] cleanup --- tests/test_positions_graphics.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index d6e8b9b8a..a54ee59a9 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -58,6 +58,10 @@ } +# TODO: data slice int +def test_data_slice_int(): + pass + @pytest.mark.parametrize("graphic_type", ["line", "scatter"]) @pytest.mark.parametrize( "slice_method", [generate_slice_indices(i) for i in range(1, 16)] @@ -119,14 +123,6 @@ def test_data_slice(graphic_type, slice_method, test_axis): assert_pending_uploads(graphic.data.buffer, offset, size) -def test_color_slice(): - pass - - -def test_cmap_slice(): - pass - - def test_sizes_slice(): pass From d12bf4d4f6caf340556afa681b1b24a5932d8af1 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Jun 2024 03:04:24 -0400 Subject: [PATCH 123/196] emit user key not parsed key --- .../graphics/_features/_positions_graphics.py | 5 +++- tests/test_colors_buffer_manager.py | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index 987aa213f..0f8bcc1c9 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -63,6 +63,8 @@ def __setitem__( key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], user_value: str | np.ndarray | tuple[float] | list[float] | list[str], ): + user_key = key + if isinstance(key, tuple): # directly setting RGBA values for points, we do no parsing if not isinstance(user_value, (int, float, np.ndarray)): @@ -122,10 +124,11 @@ def __setitem__( return event_info = { - "key": key, + "key": user_key, "value": value, "user_value": user_value, } + event = FeatureEvent("colors", info=event_info) self._call_event_handlers(event) diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py index 00456d6ec..aded53018 100644 --- a/tests/test_colors_buffer_manager.py +++ b/tests/test_colors_buffer_manager.py @@ -5,7 +5,7 @@ import pygfx import fastplotlib as fpl -from fastplotlib.graphics._features import VertexColors +from fastplotlib.graphics._features import VertexColors, FeatureEvent from .utils import generate_slice_indices, assert_pending_uploads, generate_color_inputs, generate_positions_spiral_data @@ -103,6 +103,13 @@ def test_tuple(slice_method): npt.assert_almost_equal(colors[others], others_truth) +EVENT_RETURN_VALUE: FeatureEvent = None + +def event_handler(ev): + global EVENT_RETURN_VALUE + EVENT_RETURN_VALUE = ev + + @pytest.mark.parametrize("color_input", generate_color_inputs("red")) # skip testing with int since that results in shape [1, 4] with np.repeat, int tested in independent unit test @pytest.mark.parametrize( @@ -124,6 +131,9 @@ def test_slice(color_input, slice_method: dict, test_graphic: bool): graphic = fig[0, 0].add_scatter(data=data) colors = graphic.colors + + global EVENT_RETURN_VALUE + graphic.add_event_handler(event_handler, "colors") else: colors = make_colors_buffer() @@ -142,6 +152,19 @@ def test_slice(color_input, slice_method: dict, test_graphic: bool): npt.assert_almost_equal(colors[s], truth) npt.assert_almost_equal(colors[indices], truth) + # check event + if test_graphic: + global EVENT_RETURN_VALUE + + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + if isinstance(s, slice): + assert EVENT_RETURN_VALUE.info["key"] == s + else: + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["key"], s) + # assert EVENT_RETURN_VALUE.info["key"] == s + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], truth) + # make sure correct offset and size marked for pending upload assert_pending_uploads(colors.buffer, offset, size) @@ -152,3 +175,4 @@ def test_slice(color_input, slice_method: dict, test_graphic: bool): # reset colors[:] = (1, 1, 1, 1) npt.assert_almost_equal(colors[:], np.repeat([[1.0, 1.0, 1.0, 1.0]], 10, axis=0)) + From 3baaa957c2dfcf88fb3e45136028a819780bf8a2 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Jun 2024 23:58:20 -0400 Subject: [PATCH 124/196] rename cmap_values to transform --- examples/desktop/line/line_cmap.py | 8 +-- .../graphics/_features/_positions_graphics.py | 38 ++++++------ fastplotlib/graphics/_positions_base.py | 12 ++-- fastplotlib/graphics/line.py | 6 +- fastplotlib/graphics/line_collection.py | 44 +++---------- fastplotlib/graphics/scatter.py | 6 +- fastplotlib/utils/functions.py | 39 ++++++------ tests/test_positions_graphics.py | 61 +++++++++++++------ 8 files changed, 105 insertions(+), 109 deletions(-) diff --git a/examples/desktop/line/line_cmap.py b/examples/desktop/line/line_cmap.py index 7d8e1e7d6..45c878863 100644 --- a/examples/desktop/line/line_cmap.py +++ b/examples/desktop/line/line_cmap.py @@ -21,21 +21,21 @@ ys = np.cos(xs) - 5 cosine = np.dstack([xs, ys])[0] -# cmap_values from an array, so the colors on the sine line will be based on the sine y-values +# cmap_transform from an array, so the colors on the sine line will be based on the sine y-values sine_graphic = fig[0, 0].add_line( data=sine, thickness=10, cmap="plasma", - cmap_values=sine[:, 1] + cmap_transform=sine[:, 1] ) # qualitative colormaps, useful for cluster labels or other types of categorical labels -cmap_values = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 +cmap_transform = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 cosine_graphic = fig[0, 0].add_line( data=cosine, thickness=10, cmap="tab10", - cmap_values=cmap_values + cmap_transform=cmap_transform ) fig.show() diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index 0f8bcc1c9..a2db83906 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -326,38 +326,37 @@ def set_value(self, graphic, value: float): class VertexCmap(BufferManager): """ - Sliceable colormap feature, manages a VertexColors instance and just provides a way to set colormaps. + Sliceable colormap feature, manages a VertexColors instance and + provides a way to set colormaps with arbitrary transforms """ def __init__( self, vertex_colors: VertexColors, cmap_name: str | None, - cmap_values: np.ndarray | None, + transform: np.ndarray | None, alpha: float = 1.0, ): super().__init__(data=vertex_colors.buffer) self._vertex_colors = vertex_colors self._cmap_name = cmap_name - self._cmap_values = cmap_values + self._transform = transform self._alpha = alpha if self._cmap_name is not None: if not isinstance(self._cmap_name, str): raise TypeError - if self._cmap_values is not None: - if isinstance(self._cmap_values, List): - self._cmap_values = np.asarray(self._cmap_values) - if not isinstance(self._cmap_values, np.ndarray): - raise TypeError + + if self._transform is not None: + self._transform = np.asarray(self._transform) n_datapoints = vertex_colors.value.shape[0] colors = parse_cmap_values( n_colors=n_datapoints, cmap_name=self._cmap_name, - cmap_values=self._cmap_values, + transform=self._transform, ) colors[:, -1] = alpha # set vertex colors from cmap @@ -380,7 +379,7 @@ def __setitem__(self, key: slice, cmap_name): n_elements = len(range(start, stop, step)) colors = parse_cmap_values( - n_colors=n_elements, cmap_name=cmap_name, cmap_values=self._cmap_values + n_colors=n_elements, cmap_name=cmap_name, transform=self._transform ) colors[:, -1] = self.alpha @@ -397,36 +396,36 @@ def name(self) -> str: return self._cmap_name @property - def values(self) -> np.ndarray: - return self._cmap_values + def transform(self) -> np.ndarray | None: + return self._transform - @values.setter - def values( + @transform.setter + def transform( self, values: np.ndarray | list[float | int], indices: slice | list | np.ndarray = None, ): if self._cmap_name is None: raise AttributeError( - "cmap is not set, set the cmap before setting the cmap_values" + "cmap name is not set, set the cmap name before setting the transform" ) values = np.asarray(values) colors = parse_cmap_values( - n_colors=self.value.shape[0], cmap_name=self._cmap_name, cmap_values=values + n_colors=self.value.shape[0], cmap_name=self._cmap_name, transform=values ) colors[:, -1] = self.alpha - self._cmap_values = values + self._transform = values if indices is None: indices = slice(None) self._vertex_colors[indices] = colors - self._emit_event("cmap.values", indices, values) + self._emit_event("cmap.transform", indices, values) @property def alpha(self) -> float: @@ -443,3 +442,6 @@ def __len__(self): raise NotImplementedError( "len not implemented for `cmap`, use len(colors) instead" ) + + def __repr__(self): + return f"{self.__class__.__name__} | cmap: {self.name}\ntransform: {self.transform}" diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 3830755b3..a02201139 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -61,7 +61,7 @@ def __init__( uniform_color: bool = False, alpha: float = 1.0, cmap: str | VertexCmap = None, - cmap_values: np.ndarray = None, + cmap_transform: np.ndarray = None, isolated_buffer: bool = True, *args, **kwargs, @@ -71,8 +71,8 @@ def __init__( else: self._data = VertexPositions(data, isolated_buffer=isolated_buffer) - if cmap_values is not None and cmap is None: - raise ValueError("must pass `cmap` if passing `cmap_values`") + if cmap_transform is not None and cmap is None: + raise ValueError("must pass `cmap` if passing `cmap_transform`") if cmap is not None: # if a cmap is specified it overrides colors argument @@ -92,7 +92,7 @@ def __init__( self._cmap = VertexCmap( self._colors, cmap_name=cmap, - cmap_values=cmap_values, + transform=cmap_transform, alpha=alpha, ) elif isinstance(cmap, VertexCmap): @@ -109,7 +109,7 @@ def __init__( self._colors._shared += 1 # blank colormap instance self._cmap = VertexCmap( - self._colors, cmap_name=None, cmap_values=None, alpha=alpha + self._colors, cmap_name=None, transform=None, alpha=alpha ) else: if uniform_color: @@ -127,7 +127,7 @@ def __init__( alpha=alpha, ) self._cmap = VertexCmap( - self._colors, cmap_name=None, cmap_values=None, alpha=alpha + self._colors, cmap_name=None, transform=None, alpha=alpha ) super().__init__(*args, **kwargs) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index fea478e7a..beb9d506d 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -30,7 +30,7 @@ def __init__( uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, - cmap_values: np.ndarray | Iterable = None, + cmap_transform: np.ndarray | Iterable = None, isolated_buffer: bool = True, **kwargs, ): @@ -53,7 +53,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 Iterable of numerical values, optional + cmap_transform: 1D array-like of numerical values, optional if provided, these values are used to map the colors from the cmap alpha: float, optional, default 1.0 @@ -73,7 +73,7 @@ def __init__( uniform_color=uniform_color, alpha=alpha, cmap=cmap, - cmap_values=cmap_values, + cmap_transform=cmap_transform, isolated_buffer=isolated_buffer, **kwargs, ) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index e6f4442c8..d3905ad52 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -92,7 +92,7 @@ def __init__( uniform_colors: bool = False, alpha: float = 1.0, cmap: Sequence[str] | str = None, - cmap_values: np.ndarray | List = None, + cmap_transform: np.ndarray | List = None, name: str = None, metadata: Sequence[Any] | np.ndarray = None, isolated_buffer: bool = True, @@ -127,7 +127,7 @@ def __init__( .. note:: ``cmap`` overrides any arguments passed to ``colors`` - cmap_values: 1D array-like or Iterable of numerical values, optional + 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 @@ -163,7 +163,7 @@ def __init__( f"len(metadata) != len(data)\n" f"{len(metadata)} != {len(data)}" ) - self._cmap_values = cmap_values + self._cmap_transform = cmap_transform self._cmap_str = cmap # cmap takes priority over colors @@ -171,7 +171,7 @@ def __init__( # cmap across lines if isinstance(cmap, str): colors = parse_cmap_values( - n_colors=len(data), cmap_name=cmap, cmap_values=cmap_values + n_colors=len(data), cmap_name=cmap, transform=cmap_transform ) single_color = False cmap = None @@ -267,36 +267,6 @@ def __init__( self.add_graphic(lg) - @property - def cmap(self) -> str: - return self._cmap_str - - @cmap.setter - def cmap(self, cmap: str): - colors = parse_cmap_values( - n_colors=len(self), cmap_name=cmap, cmap_values=self.cmap_values - ) - - for i, g in enumerate(self.graphics): - g.colors = colors[i] - - self._cmap_str = cmap - - @property - def cmap_values(self) -> np.ndarray: - return self._cmap_values - - @cmap_values.setter - def cmap_values(self, values: np.ndarray | Iterable): - colors = parse_cmap_values( - n_colors=len(self), cmap_name=self.cmap, cmap_values=values - ) - - for i, g in enumerate(self.graphics): - g.colors = colors[i] - - self._cmap_values = values - def add_linear_selector( self, selection: int = None, padding: float = 50, **kwargs ) -> LinearSelector: @@ -479,7 +449,7 @@ def __init__( 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, + cmap_transform: np.ndarray | List = None, name: str = None, metadata: Iterable[Any] | np.ndarray = None, separation: float = 10.0, @@ -512,7 +482,7 @@ def __init__( .. note:: ``cmap`` overrides any arguments passed to ``colors`` - cmap_values: 1D array-like or Iterable of numerical values, optional + cmap_transform: 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 @@ -545,7 +515,7 @@ def __init__( colors=colors, alpha=alpha, cmap=cmap, - cmap_values=cmap_values, + cmap_transform=cmap_transform, name=name, metadata=metadata, **kwargs, diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index c32adc02a..c3476885a 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -34,7 +34,7 @@ def __init__( uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, - cmap_values: np.ndarray = None, + cmap_transform: np.ndarray = None, isolated_buffer: bool = True, sizes: float | np.ndarray | Iterable[float] = 1, uniform_size: bool = False, @@ -63,7 +63,7 @@ def __init__( apply a colormap to the scatter instead of assigning colors manually, this overrides any argument passed to "colors" - cmap_values: 1D array-like or list of numerical values, optional + cmap_transform: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap isolated_buffer: bool, default True @@ -88,7 +88,7 @@ def __init__( uniform_color=uniform_color, alpha=alpha, cmap=cmap, - cmap_values=cmap_values, + cmap_transform=cmap_transform, isolated_buffer=isolated_buffer, **kwargs, ) diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index 561863b0c..9e007b563 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -239,7 +239,7 @@ def normalize_min_max(a): def parse_cmap_values( n_colors: int, cmap_name: str, - cmap_values: np.ndarray | list[int | float] = None, + transform: np.ndarray | list[int | float] = None, ) -> np.ndarray: """ @@ -251,28 +251,25 @@ def parse_cmap_values( cmap_name: str colormap name - cmap_values: np.ndarray | List[int | float], optional - cmap values + transform: np.ndarray | List[int | float], optional + cmap transform Returns ------- """ - if cmap_values is None: - # use the cmap values linearly just along the collection indices - # for example, if len(data) = 10 and the cmap is "jet", then it will - # linearly go from blue to red from data[0] to data[-1] + if transform is None: colors = make_colors(n_colors, cmap_name) return colors else: - if not isinstance(cmap_values, np.ndarray): - cmap_values = np.array(cmap_values) + if not isinstance(transform, np.ndarray): + transform = np.array(transform) - # use the values within cmap_values to set the color of the corresponding data - # each individual data[i] has its color based on the "relative cmap_value intensity" - if len(cmap_values) != n_colors: + # use the of the cmap_transform to set the color of the corresponding data + # each individual data[i] has its color based on the transform values + if len(transform) != n_colors: raise ValueError( - f"len(cmap_values) != len(data): {len(cmap_values)} != {n_colors}" + f"len(cmap_values) != len(data): {len(transform)} != {n_colors}" ) colormap = get_cmap(cmap_name) @@ -280,23 +277,23 @@ def parse_cmap_values( n_colors = colormap.shape[0] - 1 if cmap_name in QUALITATIVE_CMAPS: - # check that cmap_values are and within the number of colors `n_colors` + # check that cmap_transform are and within the number of colors `n_colors` # do not scale, use directly - if not np.issubdtype(cmap_values.dtype, np.integer): + if not np.issubdtype(transform.dtype, np.integer): raise TypeError( - f" cmap_values should be used with qualitative colormaps, the dtype you " - f"have passed is {cmap_values.dtype}" + f" `cmap_transform` values should be used with qualitative colormaps, " + f"the dtype you have passed is {transform.dtype}" ) - if max(cmap_values) > n_colors: + if max(transform) > n_colors: raise IndexError( f"You have chosen the qualitative colormap <'{cmap_name}'> which only has " - f"<{n_colors}> colors, which is lower than the max value of your `cmap_values`." + f"<{n_colors}> colors, which is lower than the max value of your `cmap_transform`." f"Choose a cmap with more colors, or a non-quantitative colormap." ) - norm_cmap_values = cmap_values + norm_cmap_values = transform else: # scale between 0 - n_colors so we can just index the colormap as a LUT - norm_cmap_values = (normalize_min_max(cmap_values) * n_colors).astype(int) + norm_cmap_values = (normalize_min_max(transform) * n_colors).astype(int) # use colormap as LUT to map the cmap_values to the colormap index colors = np.vstack([colormap[val] for val in norm_cmap_values]) diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index a54ee59a9..b68b48642 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -12,7 +12,8 @@ UniformColor, UniformSize, PointsSizesFeature, - Thickness + Thickness, + FeatureEvent ) from .utils import ( @@ -58,6 +59,14 @@ } +EVENT_RETURN_VALUE: FeatureEvent = None + + +def event_handler(ev): + global EVENT_RETURN_VALUE + EVENT_RETURN_VALUE = ev + + # TODO: data slice int def test_data_slice_int(): pass @@ -281,9 +290,9 @@ def test_positions_graphic_vertex_colors( @pytest.mark.parametrize("graphic_type", ["line", "scatter"]) @pytest.mark.parametrize("colors", [None, *generate_color_inputs("r")]) @pytest.mark.parametrize("uniform_color", [None, False]) -@pytest.mark.parametrize("cmap", ["jet", "viridis"]) +@pytest.mark.parametrize("cmap", ["jet"]) @pytest.mark.parametrize( - "cmap_values", [None, [3, 5, 2, 1, 0, 6, 9, 7, 4, 8], np.arange(9, -1, -1)] + "transform", [None, [3, 5, 2, 1, 0, 6, 9, 7, 4, 8], np.arange(9, -1, -1)] ) @pytest.mark.parametrize("alpha", [None, 0.5, 0.0]) def test_cmap( @@ -291,13 +300,13 @@ def test_cmap( colors, uniform_color, cmap, - cmap_values, + transform, alpha, ): fig = fpl.Figure() kwargs = dict() - for kwarg in ["cmap", "cmap_values", "colors", "uniform_color", "alpha"]: + for kwarg in ["cmap", "transform", "colors", "uniform_color", "alpha"]: if locals()[kwarg] is not None: # add to dict of arguments that will be passed kwargs[kwarg] = locals()[kwarg] @@ -315,9 +324,10 @@ def test_cmap( truth = TRUTH_CMAPS[cmap].copy() truth[:, -1] = alpha - # permute if cmap_values is provided - if cmap_values is not None: - truth = truth[cmap_values] + # permute if transform is provided + if transform is not None: + truth = truth[transform] + npt.assert_almost_equal(graphic.cmap.transform, transform) assert isinstance(graphic._cmap, VertexCmap) @@ -330,6 +340,30 @@ def test_cmap( npt.assert_almost_equal(graphic.cmap.value, truth) npt.assert_almost_equal(graphic.colors.value, truth) + # test changing cmap but not transform + graphic.cmap = "viridis" + truth = TRUTH_CMAPS["viridis"].copy() + truth[:, -1] = alpha + + if transform is not None: + truth = truth[transform] + + assert graphic.cmap.name == "viridis" + npt.assert_almost_equal(graphic.cmap.value, truth) + npt.assert_almost_equal(graphic.colors.value, truth) + + # test changing transform + transform = np.random.rand(10) + graphic.cmap.transform = transform + npt.assert_almost_equal(graphic.cmap.transform, transform) + + truth = TRUTH_CMAPS["viridis"].copy() + truth = truth[transform.argsort()] + truth[:, -1] = alpha + + npt.assert_almost_equal(graphic.cmap.value, truth) + npt.assert_almost_equal(graphic.colors.value, truth) + @pytest.mark.parametrize("graphic_type", ["line", "scatter"]) @pytest.mark.parametrize("cmap", ["jet"]) @@ -446,7 +480,7 @@ def test_uniform_size(sizes, uniform_size): @pytest.mark.parametrize( "thickness", [None, 0.5, 5.0] ) -def test_thicnkess(thickness): +def test_thickness(thickness): fig = fpl.Figure() kwargs = dict() @@ -465,17 +499,10 @@ def test_thicnkess(thickness): assert isinstance(graphic._thickness, Thickness) assert graphic.thickness == thickness + assert graphic.world_object.material.thickness == thickness if thickness == 0.5: assert isinstance(graphic.world_object.material, pygfx.LineThinMaterial) else: assert isinstance(graphic.world_object.material, pygfx.LineMaterial) - - -def test_line_feature_events(): - pass - - -def test_scatter_feature_events(): - pass From 255183bdc5519fc4d808893220a4a5e50fdbae4b Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Jun 2024 23:59:02 -0400 Subject: [PATCH 125/196] cmap_values -> cmap_transform in mixin --- fastplotlib/layouts/_graphic_methods_mixin.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index fecc4cc60..489c8d4f6 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -106,7 +106,7 @@ def add_line_collection( uniform_colors: bool = False, alpha: float = 1.0, cmap: Union[Sequence[str], str] = None, - cmap_values: Union[numpy.ndarray, List] = None, + cmap_transform: Union[numpy.ndarray, List] = None, name: str = None, metadata: Union[Sequence[Any], numpy.ndarray] = None, isolated_buffer: bool = True, @@ -142,7 +142,7 @@ def add_line_collection( .. note:: ``cmap`` overrides any arguments passed to ``colors`` - cmap_values: 1D array-like or Iterable of numerical values, optional + 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 @@ -172,7 +172,7 @@ def add_line_collection( uniform_colors, alpha, cmap, - cmap_values, + cmap_transform, name, metadata, isolated_buffer, @@ -187,7 +187,7 @@ def add_line( uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, - cmap_values: Union[numpy.ndarray, Iterable] = None, + cmap_transform: Union[numpy.ndarray, Iterable] = None, isolated_buffer: bool = True, **kwargs ) -> LineGraphic: @@ -211,7 +211,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 Iterable of numerical values, optional + cmap_transform: 1D array-like of numerical values, optional if provided, these values are used to map the colors from the cmap alpha: float, optional, default 1.0 @@ -233,7 +233,7 @@ def add_line( uniform_color, alpha, cmap, - cmap_values, + cmap_transform, isolated_buffer, **kwargs ) @@ -245,7 +245,7 @@ def add_line_stack( 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, + cmap_transform: Union[numpy.ndarray, List] = None, name: str = None, metadata: Union[Iterable[Any], numpy.ndarray] = None, separation: float = 10.0, @@ -279,7 +279,7 @@ def add_line_stack( .. note:: ``cmap`` overrides any arguments passed to ``colors`` - cmap_values: 1D array-like or Iterable of numerical values, optional + cmap_transform: 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 @@ -314,7 +314,7 @@ def add_line_stack( colors, alpha, cmap, - cmap_values, + cmap_transform, name, metadata, separation, @@ -329,7 +329,7 @@ def add_scatter( uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, - cmap_values: numpy.ndarray = None, + cmap_transform: numpy.ndarray = None, isolated_buffer: bool = True, sizes: Union[float, numpy.ndarray, Iterable[float]] = 1, uniform_size: bool = False, @@ -359,7 +359,7 @@ def add_scatter( apply a colormap to the scatter instead of assigning colors manually, this overrides any argument passed to "colors" - cmap_values: 1D array-like or list of numerical values, optional + cmap_transform: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap isolated_buffer: bool, default True @@ -385,7 +385,7 @@ def add_scatter( uniform_color, alpha, cmap, - cmap_values, + cmap_transform, isolated_buffer, sizes, uniform_size, From 7392628d8b3b6199603e64be5d3bb0006abd10d7 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 00:03:04 -0400 Subject: [PATCH 126/196] test graphics in vertex data buffer manager tests --- tests/test_positions_data_buffer_manager.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/test_positions_data_buffer_manager.py b/tests/test_positions_data_buffer_manager.py index fc0be9b89..27de019f2 100644 --- a/tests/test_positions_data_buffer_manager.py +++ b/tests/test_positions_data_buffer_manager.py @@ -2,6 +2,7 @@ from numpy import testing as npt import pytest +import fastplotlib as fpl from fastplotlib.graphics._features import VertexPositions from .utils import ( generate_slice_indices, @@ -62,20 +63,34 @@ def test_int(): npt.assert_almost_equal(points[indices], data[indices]) +@pytest.mark.parametrize("graphic_type", [None, "line", "scatter"]) @pytest.mark.parametrize( "slice_method", [generate_slice_indices(i) for i in range(1, 16)] ) # int tested separately @pytest.mark.parametrize("test_axis", ["y", "xy", "xyz"]) -def test_slice(slice_method: dict, test_axis: str): +def test_slice(graphic_type, slice_method: dict, test_axis: str): data = generate_positions_spiral_data("xyz") + if graphic_type is not None: + fig = fpl.Figure() + + if graphic_type == "line": + graphic = fig[0, 0].add_line(data=data) + + elif graphic_type == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + points = graphic.data + + else: + points = VertexPositions(data) + s = slice_method["slice"] indices = slice_method["indices"] offset = slice_method["offset"] size = slice_method["size"] others = slice_method["others"] - points = VertexPositions(data) # TODO: placeholder until I make a testing figure where we draw frames only on call points.buffer._gfx_pending_uploads.clear() From 612adc5340f6a6b07494a19dd52a71276261c5f8 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 00:20:08 -0400 Subject: [PATCH 127/196] cmap transform tests --- tests/test_colors_buffer_manager.py | 2 ++ tests/test_positions_graphics.py | 34 +++++++++++++++++------------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py index aded53018..1a11e9c83 100644 --- a/tests/test_colors_buffer_manager.py +++ b/tests/test_colors_buffer_manager.py @@ -105,6 +105,7 @@ def test_tuple(slice_method): EVENT_RETURN_VALUE: FeatureEvent = None + def event_handler(ev): global EVENT_RETURN_VALUE EVENT_RETURN_VALUE = ev @@ -156,6 +157,7 @@ def test_slice(color_input, slice_method: dict, test_graphic: bool): if test_graphic: global EVENT_RETURN_VALUE + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target is graphic.world_object if isinstance(s, slice): diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index b68b48642..77ebf9cc6 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -292,7 +292,7 @@ def test_positions_graphic_vertex_colors( @pytest.mark.parametrize("uniform_color", [None, False]) @pytest.mark.parametrize("cmap", ["jet"]) @pytest.mark.parametrize( - "transform", [None, [3, 5, 2, 1, 0, 6, 9, 7, 4, 8], np.arange(9, -1, -1)] + "cmap_transform", [None]#, [3, 5, 2, 1, 0, 6, 9, 7, 4, 8], np.arange(9, -1, -1)] ) @pytest.mark.parametrize("alpha", [None, 0.5, 0.0]) def test_cmap( @@ -300,13 +300,13 @@ def test_cmap( colors, uniform_color, cmap, - transform, + cmap_transform, alpha, ): fig = fpl.Figure() kwargs = dict() - for kwarg in ["cmap", "transform", "colors", "uniform_color", "alpha"]: + for kwarg in ["cmap", "cmap_transform", "colors", "uniform_color", "alpha"]: if locals()[kwarg] is not None: # add to dict of arguments that will be passed kwargs[kwarg] = locals()[kwarg] @@ -325,9 +325,9 @@ def test_cmap( truth[:, -1] = alpha # permute if transform is provided - if transform is not None: - truth = truth[transform] - npt.assert_almost_equal(graphic.cmap.transform, transform) + if cmap_transform is not None: + truth = truth[cmap_transform] + npt.assert_almost_equal(graphic.cmap.transform, cmap_transform) assert isinstance(graphic._cmap, VertexCmap) @@ -345,21 +345,27 @@ def test_cmap( truth = TRUTH_CMAPS["viridis"].copy() truth[:, -1] = alpha - if transform is not None: - truth = truth[transform] + if cmap_transform is not None: + truth = truth[cmap_transform] assert graphic.cmap.name == "viridis" npt.assert_almost_equal(graphic.cmap.value, truth) npt.assert_almost_equal(graphic.colors.value, truth) # test changing transform - transform = np.random.rand(10) - graphic.cmap.transform = transform - npt.assert_almost_equal(graphic.cmap.transform, transform) + cmap_transform = np.random.rand(10) - truth = TRUTH_CMAPS["viridis"].copy() - truth = truth[transform.argsort()] - truth[:, -1] = alpha + # cmap transform is internally normalized between 0 - 1 + cmap_transform_norm = cmap_transform.copy() + cmap_transform_norm -= cmap_transform.min() + cmap_transform_norm /= cmap_transform_norm.max() + cmap_transform_norm *= 255 + + truth = fpl.utils.get_cmap("viridis", alpha=alpha) + truth = np.vstack([truth[val] for val in cmap_transform_norm.astype(int)]) + + graphic.cmap.transform = cmap_transform + npt.assert_almost_equal(graphic.cmap.transform, cmap_transform) npt.assert_almost_equal(graphic.cmap.value, truth) npt.assert_almost_equal(graphic.colors.value, truth) From 9a27cb7ce1af3e21851449857bd2f31f92fed7d0 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 00:31:43 -0400 Subject: [PATCH 128/196] make Graphic._features private, add Graphic.events property --- fastplotlib/graphics/_base.py | 23 +++++++++++++++---- fastplotlib/graphics/_collection_base.py | 2 +- fastplotlib/graphics/image.py | 4 +--- fastplotlib/graphics/line.py | 2 +- fastplotlib/graphics/scatter.py | 2 +- .../graphics/selectors/_base_selector.py | 2 +- fastplotlib/graphics/text.py | 1 + 7 files changed, 24 insertions(+), 12 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 6608b6ade..f8edc43ec 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -42,7 +42,12 @@ class Graphic: - features = {} + _features = {} + + @property + def events(self) -> tuple[str]: + """events supported by this graphic""" + return (*tuple(self._features), *PYGFX_EVENTS) @property def name(self) -> str | None: @@ -108,8 +113,8 @@ def __init_subclass__(cls, **kwargs): ) # set of all features - cls.features = { - *cls.features, + cls._features = { + *cls._features, "name", "offset", "rotation", @@ -243,6 +248,14 @@ def my_handler(event): callback = None if decorating else args[0] types = args if decorating else args[1:] + unsupported_events = [t for t in types if t not in self.events] + + if len(unsupported_events) > 0: + raise TypeError( + f"unsupported events passed: {unsupported_events} for {self.__class__.__name__}\n" + f"`graphic.events` will return a tuple of supported events" + ) + def decorator(_callback): _callback_injector = partial( self._handle_event, _callback @@ -252,7 +265,7 @@ def decorator(_callback): # add to our record self._event_handlers[t].add(_callback) - if t in self.features: + if t in self._features: # fpl feature event feature = getattr(self, f"_{t}") feature.add_event_handler(_callback_injector) @@ -276,7 +289,7 @@ def _handle_event(self, callback, event: pygfx.Event): if self.block_events: return - if event.type in self.features: + if event.type in self._features: # for feature events event._target = self.world_object diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index caa28859e..c1986de10 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -147,7 +147,7 @@ class GraphicCollection(Graphic): def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) - cls.features = cls.child_type.features + cls._features = cls.child_type._features def __init__(self, name: str = None): super().__init__(name) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index ee6bd2c70..94ea3d668 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -1,8 +1,6 @@ from typing import * import weakref -from numpy.typing import NDArray - import pygfx from ..utils import quick_min_max @@ -73,7 +71,7 @@ def chunk_index(self) -> tuple[int, int]: class ImageGraphic(Graphic): - features = {"data", "cmap", "vmin", "vmax"} + _features = {"data", "cmap", "vmin", "vmax"} @property def data(self) -> TextureArray: diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index beb9d506d..40394dbd6 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -11,7 +11,7 @@ class LineGraphic(PositionsGraphic): - features = {"data", "colors", "cmap", "thickness"} + _features = {"data", "colors", "cmap", "thickness"} @property def thickness(self) -> float: diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index c3476885a..a6fba9121 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -8,7 +8,7 @@ class ScatterGraphic(PositionsGraphic): - features = {"data", "sizes", "colors", "cmap"} + _features = {"data", "sizes", "colors", "cmap"} @property def sizes(self) -> PointsSizesFeature | float: diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 672b54cd1..0fc48058d 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -35,7 +35,7 @@ class MoveInfo: # Selector base class class BaseSelector(Graphic): - features = {"selection"} + _features = {"selection"} @property def axis(self) -> str: diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index cbc622255..1434cc15a 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -12,6 +12,7 @@ class TextGraphic(Graphic): + _features = {"text", "font_size", "face_color", "outline_color", "outline_thickness"} def __init__( self, text: str, From fbfca0bfd72018db33baa0d7c6220cbb18fca01f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 00:58:25 -0400 Subject: [PATCH 129/196] color events tests --- tests/test_colors_buffer_manager.py | 99 +++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py index 1a11e9c83..c914e37c5 100644 --- a/tests/test_colors_buffer_manager.py +++ b/tests/test_colors_buffer_manager.py @@ -14,6 +14,14 @@ def make_colors_buffer() -> VertexColors: return colors +EVENT_RETURN_VALUE: FeatureEvent = None + + +def event_handler(ev): + global EVENT_RETURN_VALUE + EVENT_RETURN_VALUE = ev + + @pytest.mark.parametrize( "color_input", [ @@ -28,9 +36,27 @@ def test_create_buffer(color_input): npt.assert_almost_equal(colors[:], truth) -def test_int(): +@pytest.mark.parametrize( + "test_graphic", [False, "line", "scatter"] +) +def test_int(test_graphic): # setting single points - colors = make_colors_buffer() + if test_graphic: + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + if test_graphic == "line": + graphic = fig[0, 0].add_line(data=data) + + elif test_graphic == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + colors = graphic.colors + global EVENT_RETURN_VALUE + graphic.add_event_handler(event_handler, "colors") + else: + colors = make_colors_buffer() + # TODO: placeholder until I make a testing figure where we draw frames only on call colors.buffer._gfx_pending_uploads.clear() @@ -38,6 +64,15 @@ def test_int(): npt.assert_almost_equal(colors[3], [1.0, 0.0, 0.0, 1.0]) assert colors.buffer._gfx_pending_uploads[-1] == (3, 1) + if test_graphic: + # test event + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + assert EVENT_RETURN_VALUE.info["key"] == 3 + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], np.array([[1, 0, 0, 1]])) + assert EVENT_RETURN_VALUE.info["user_value"] == "r" + colors[6] = [0.0, 1.0, 1.0, 1.0] npt.assert_almost_equal(colors[6], [0.0, 1.0, 1.0, 1.0]) @@ -51,12 +86,29 @@ def test_int(): npt.assert_almost_equal(colors[2], [1.0, 0.0, 1.0, 0.5]) +@pytest.mark.parametrize( + "test_graphic", [False, "line", "scatter"] +) @pytest.mark.parametrize( "slice_method", [generate_slice_indices(i) for i in range(0, 16)] ) -def test_tuple(slice_method): +def test_tuple(test_graphic, slice_method): # setting entire array manually - colors = make_colors_buffer() + if test_graphic: + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + if test_graphic == "line": + graphic = fig[0, 0].add_line(data=data) + + elif test_graphic == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + colors = graphic.colors + global EVENT_RETURN_VALUE + graphic.add_event_handler(event_handler, "colors") + else: + colors = make_colors_buffer() s = slice_method["slice"] indices = slice_method["indices"] @@ -67,13 +119,36 @@ def test_tuple(slice_method): truth = np.repeat([[0.5, 0.5, 0.5, 0.5]], repeats=len(indices), axis=0) npt.assert_almost_equal(colors[indices], truth) + if test_graphic: + # test event + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + assert EVENT_RETURN_VALUE.info["key"] == (s, slice(None)) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], truth) + assert EVENT_RETURN_VALUE.info["user_value"] == 0.5 + # check others are not modified others_truth = np.repeat([[1.0, 1.0, 1.0, 1.0]], repeats=len(others), axis=0) npt.assert_almost_equal(colors[others], others_truth) # reset - colors[:] = (1, 1, 1, 1) - npt.assert_almost_equal(colors[:], np.repeat([[1.0, 1.0, 1.0, 1.0]], 10, axis=0)) + if test_graphic: + # test setter + graphic.colors = "w" + else: + colors[:] = [1, 1, 1, 1] + truth = np.repeat([[1.0, 1.0, 1.0, 1.0]], 10, axis=0) + npt.assert_almost_equal(colors[:], truth) + + if test_graphic: + # test event + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + assert EVENT_RETURN_VALUE.info["key"] == slice(None) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], truth) + assert EVENT_RETURN_VALUE.info["user_value"] == "w" # set just R values colors[s, 0] = 0.5 @@ -103,14 +178,6 @@ def test_tuple(slice_method): npt.assert_almost_equal(colors[others], others_truth) -EVENT_RETURN_VALUE: FeatureEvent = None - - -def event_handler(ev): - global EVENT_RETURN_VALUE - EVENT_RETURN_VALUE = ev - - @pytest.mark.parametrize("color_input", generate_color_inputs("red")) # skip testing with int since that results in shape [1, 4] with np.repeat, int tested in independent unit test @pytest.mark.parametrize( @@ -166,6 +233,10 @@ def test_slice(color_input, slice_method: dict, test_graphic: bool): npt.assert_almost_equal(EVENT_RETURN_VALUE.info["key"], s) # assert EVENT_RETURN_VALUE.info["key"] == s npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], truth) + if isinstance(color_input, str): + assert EVENT_RETURN_VALUE.info["user_value"] == color_input + else: + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["user_value"], color_input) # make sure correct offset and size marked for pending upload assert_pending_uploads(colors.buffer, offset, size) From 7c3520f704f57b8c742ad19a1040f721d25ed7b7 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 01:02:53 -0400 Subject: [PATCH 130/196] move data slice test to just buffer tests --- tests/test_positions_graphics.py | 74 +++----------------------------- 1 file changed, 7 insertions(+), 67 deletions(-) diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index 77ebf9cc6..d7a0ea0ff 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -67,71 +67,6 @@ def event_handler(ev): EVENT_RETURN_VALUE = ev -# TODO: data slice int -def test_data_slice_int(): - pass - -@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) -@pytest.mark.parametrize( - "slice_method", [generate_slice_indices(i) for i in range(1, 16)] -) # same as slice methods in the buffer tests -@pytest.mark.parametrize("test_axis", ["y", "xy", "xyz"]) -def test_data_slice(graphic_type, slice_method, test_axis): - fig = fpl.Figure() - - data = generate_positions_spiral_data("xyz") - - if graphic_type == "line": - graphic = fig[0, 0].add_line(data=data) - - elif graphic_type == "scatter": - graphic = fig[0, 0].add_scatter(data=data) - - s = slice_method["slice"] - indices = slice_method["indices"] - offset = slice_method["offset"] - size = slice_method["size"] - others = slice_method["others"] - - # TODO: placeholder until I make a testing figure where we draw frames only on call - graphic.data.buffer._gfx_pending_uploads.clear() - - match test_axis: - case "y": - graphic.data[s, 1] = -data[s, 1] - npt.assert_almost_equal(graphic.data[s, 1], -data[s, 1]) - npt.assert_almost_equal(graphic.data[indices, 1], -data[indices, 1]) - # make sure other points are not modified - npt.assert_almost_equal( - graphic.data[others, 1], data[others, 1] - ) # other points in same dimension - npt.assert_almost_equal( - graphic.data[:, 2:], data[:, 2:] - ) # dimensions that are not sliced - - case "xy": - graphic.data[s, :-1] = -data[s, :-1] - npt.assert_almost_equal(graphic.data[s, :-1], -data[s, :-1]) - npt.assert_almost_equal(graphic.data[indices, :-1], -data[s, :-1]) - # make sure other points are not modified - npt.assert_almost_equal( - graphic.data[others, :-1], data[others, :-1] - ) # other points in the same dimensions - npt.assert_almost_equal( - graphic.data[:, -1], data[:, -1] - ) # dimensions that are not touched - - case "xyz": - graphic.data[s] = -data[s] - npt.assert_almost_equal(graphic.data[s], -data[s]) - npt.assert_almost_equal(graphic.data[indices], -data[s]) - # make sure other points are not modified - npt.assert_almost_equal(graphic.data[others], data[others]) - - # make sure correct offset and size marked for pending upload - assert_pending_uploads(graphic.data.buffer, offset, size) - - def test_sizes_slice(): pass @@ -244,6 +179,7 @@ def test_positions_graphic_vertex_colors( uniform_color, alpha, ): + # test different ways of passing vertex colors fig = fpl.Figure() kwargs = dict() @@ -292,7 +228,7 @@ def test_positions_graphic_vertex_colors( @pytest.mark.parametrize("uniform_color", [None, False]) @pytest.mark.parametrize("cmap", ["jet"]) @pytest.mark.parametrize( - "cmap_transform", [None]#, [3, 5, 2, 1, 0, 6, 9, 7, 4, 8], np.arange(9, -1, -1)] + "cmap_transform", [None, [3, 5, 2, 1, 0, 6, 9, 7, 4, 8], np.arange(9, -1, -1)] ) @pytest.mark.parametrize("alpha", [None, 0.5, 0.0]) def test_cmap( @@ -300,9 +236,10 @@ def test_cmap( colors, uniform_color, cmap, - cmap_transform, + cmap_transform, alpha, ): + # test different ways of passing cmap args fig = fpl.Figure() kwargs = dict() @@ -380,6 +317,7 @@ def test_cmap( "uniform_color", [True] # none of these will work with a uniform buffer ) def test_incompatible_cmap_color_args(graphic_type, cmap, colors, uniform_color): + # test incompatible cmap args fig = fpl.Figure() kwargs = dict() @@ -406,6 +344,7 @@ def test_incompatible_cmap_color_args(graphic_type, cmap, colors, uniform_color) "uniform_color", [True] # none of these will work with a uniform buffer ) def test_incompatible_color_args(graphic_type, colors, uniform_color): + # test incompatible color args fig = fpl.Figure() kwargs = dict() @@ -431,6 +370,7 @@ def test_incompatible_color_args(graphic_type, colors, uniform_color): "uniform_size", [None, False] ) def test_sizes(sizes, uniform_size): + # test scatter sizes fig = fpl.Figure() kwargs = dict() From c127fe73d19fa9aed3dabe1d5fc9729b6c63b35b Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 01:20:49 -0400 Subject: [PATCH 131/196] data events tests --- tests/test_positions_data_buffer_manager.py | 99 ++++++++++++++++++--- 1 file changed, 88 insertions(+), 11 deletions(-) diff --git a/tests/test_positions_data_buffer_manager.py b/tests/test_positions_data_buffer_manager.py index 27de019f2..948a67e54 100644 --- a/tests/test_positions_data_buffer_manager.py +++ b/tests/test_positions_data_buffer_manager.py @@ -3,7 +3,7 @@ import pytest import fastplotlib as fpl -from fastplotlib.graphics._features import VertexPositions +from fastplotlib.graphics._features import VertexPositions, FeatureEvent from .utils import ( generate_slice_indices, assert_pending_uploads, @@ -11,6 +11,14 @@ ) +EVENT_RETURN_VALUE: FeatureEvent = None + + +def event_handler(ev): + global EVENT_RETURN_VALUE + EVENT_RETURN_VALUE = ev + + @pytest.mark.parametrize( "data", [generate_positions_spiral_data(v) for v in ["y", "xy", "xyz"]] ) @@ -34,11 +42,25 @@ def test_create_buffer(data): # test 3D spiral npt.assert_almost_equal(points_data[:], generate_positions_spiral_data("xyz")) +@pytest.mark.parametrize("test_graphic", [False, "line", "scatter"]) +def test_int(test_graphic): + # test setting single points -def test_int(): data = generate_positions_spiral_data("xyz") - # test setting single points - points = VertexPositions(data) + if test_graphic: + fig = fpl.Figure() + + if test_graphic == "line": + graphic = fig[0, 0].add_line(data=data) + + elif test_graphic == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + points = graphic.data + global EVENT_RETURN_VALUE + graphic.add_event_handler(event_handler, "data") + else: + points = VertexPositions(data) # set all x, y, z points, create a kink in the spiral points[2] = 1.0 @@ -48,10 +70,29 @@ def test_int(): indices.pop(2) npt.assert_almost_equal(points[indices], data[indices]) + # check event + if test_graphic: + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + assert EVENT_RETURN_VALUE.info["key"] == 2 + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], 1.0) + # reset - points = data + if test_graphic: + graphic.data = data + else: + points[:] = data npt.assert_almost_equal(points[:], data) + # check event + if test_graphic: + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + assert EVENT_RETURN_VALUE.info["key"] == slice(None) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], data) + # just set y value points[3, 1] = 1.0 npt.assert_almost_equal(points[3, 1], 1.0) @@ -63,25 +104,26 @@ def test_int(): npt.assert_almost_equal(points[indices], data[indices]) -@pytest.mark.parametrize("graphic_type", [None, "line", "scatter"]) +@pytest.mark.parametrize("test_graphic", [False, "line", "scatter"]) @pytest.mark.parametrize( "slice_method", [generate_slice_indices(i) for i in range(1, 16)] ) # int tested separately @pytest.mark.parametrize("test_axis", ["y", "xy", "xyz"]) -def test_slice(graphic_type, slice_method: dict, test_axis: str): +def test_slice(test_graphic, slice_method: dict, test_axis: str): data = generate_positions_spiral_data("xyz") - if graphic_type is not None: + if test_graphic: fig = fpl.Figure() - if graphic_type == "line": + if test_graphic == "line": graphic = fig[0, 0].add_line(data=data) - elif graphic_type == "scatter": + elif test_graphic == "scatter": graphic = fig[0, 0].add_scatter(data=data) points = graphic.data - + global EVENT_RETURN_VALUE + graphic.add_event_handler(event_handler, "data") else: points = VertexPositions(data) @@ -107,6 +149,18 @@ def test_slice(graphic_type, slice_method: dict, test_axis: str): points[:, 2:], data[:, 2:] ) # dimensions that are not sliced + # check event + if test_graphic: + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + if isinstance(s, slice): + assert EVENT_RETURN_VALUE.info["key"] == (s, 1) + else: + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["key"][0], s) + assert EVENT_RETURN_VALUE.info["key"][1] == 1 + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], -data[s, 1]) + case "xy": points[s, :-1] = -data[s, :-1] npt.assert_almost_equal(points[s, :-1], -data[s, :-1]) @@ -119,6 +173,18 @@ def test_slice(graphic_type, slice_method: dict, test_axis: str): points[:, -1], data[:, -1] ) # dimensions that are not touched + # check event + if test_graphic: + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + if isinstance(s, slice): + assert EVENT_RETURN_VALUE.info["key"] == (s, slice(None, -1, None)) + else: + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["key"][0], s) + assert EVENT_RETURN_VALUE.info["key"][1] == slice(None, -1, None) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], -data[s, :-1]) + case "xyz": points[s] = -data[s] npt.assert_almost_equal(points[s], -data[s]) @@ -126,5 +192,16 @@ def test_slice(graphic_type, slice_method: dict, test_axis: str): # make sure other points are not modified npt.assert_almost_equal(points[others], data[others]) + # check event + if test_graphic: + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + if isinstance(s, slice): + assert EVENT_RETURN_VALUE.info["key"] == s + else: + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["key"], s) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], -data[s]) + # make sure correct offset and size marked for pending upload assert_pending_uploads(points.buffer, offset, size) From 18f7035e7568b0407d2c1cbd670842be94d374e2 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 01:21:34 -0400 Subject: [PATCH 132/196] cleanup --- tests/test_colors_buffer_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py index c914e37c5..116dcc56b 100644 --- a/tests/test_colors_buffer_manager.py +++ b/tests/test_colors_buffer_manager.py @@ -231,7 +231,6 @@ def test_slice(color_input, slice_method: dict, test_graphic: bool): assert EVENT_RETURN_VALUE.info["key"] == s else: npt.assert_almost_equal(EVENT_RETURN_VALUE.info["key"], s) - # assert EVENT_RETURN_VALUE.info["key"] == s npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], truth) if isinstance(color_input, str): assert EVENT_RETURN_VALUE.info["user_value"] == color_input From 96065b7d8c0978c507df275a07ca34defaef4b0a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 01:26:58 -0400 Subject: [PATCH 133/196] cleanup, remove old histogram graphic --- fastplotlib/graphics/histogram.py | 114 ------------------------------ 1 file changed, 114 deletions(-) delete mode 100644 fastplotlib/graphics/histogram.py diff --git a/fastplotlib/graphics/histogram.py b/fastplotlib/graphics/histogram.py deleted file mode 100644 index b78be39d3..000000000 --- a/fastplotlib/graphics/histogram.py +++ /dev/null @@ -1,114 +0,0 @@ -from warnings import warn -from typing import Union, Dict - -import numpy as np - -import pygfx - -from ._base import Graphic - - -class _HistogramBin(pygfx.Mesh): - def __int__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bin_center: float = None - self.frequency: Union[int, float] = None - - -class HistogramGraphic(Graphic): - def __init__( - self, - data: np.ndarray = None, - bins: Union[int, str] = "auto", - pre_computed: Dict[str, np.ndarray] = None, - colors: np.ndarray = "w", - draw_scale_factor: float = 100.0, - draw_bin_width_scale: float = 1.0, - **kwargs, - ): - """ - Create a Histogram Graphic - - Parameters - ---------- - data: np.ndarray or None, optional - data to create a histogram from, can be ``None`` if pre-computed values are provided to ``pre_computed`` - - bins: int or str, default is "auto", optional - this is directly just passed to ``numpy.histogram`` - - pre_computed: dict in the form {"hist": vals, "bin_edges" : vals}, optional - pre-computed histogram values - - colors: np.ndarray, optional - - draw_scale_factor: float, default ``100.0``, optional - scale the drawing of the entire Graphic - - draw_bin_width_scale: float, default ``1.0`` - scale the drawing of the bin widths - - kwargs - passed to Graphic - """ - - if pre_computed is None: - self.hist, self.bin_edges = np.histogram(data, bins) - else: - if not set(pre_computed.keys()) == {"hist", "bin_edges"}: - raise ValueError( - "argument to `pre_computed` must be a `dict` with keys 'hist' and 'bin_edges'" - ) - if not all(isinstance(v, np.ndarray) for v in pre_computed.values()): - raise ValueError( - "argument to `pre_computed` must be a `dict` where the values are numpy.ndarray" - ) - self.hist, self.bin_edges = pre_computed["hist"], pre_computed["bin_edges"] - - self.bin_interval = (self.bin_edges[1] - self.bin_edges[0]) / 2 - self.bin_centers = (self.bin_edges + self.bin_interval)[:-1] - - # scale between 0 - draw_scale_factor - scaled_bin_edges = ( - (self.bin_edges - self.bin_edges.min()) / (np.ptp(self.bin_edges)) - ) * draw_scale_factor - - bin_interval_scaled = scaled_bin_edges[1] / 2 - # get the centers of the bins from the edges - x_positions_bins = (scaled_bin_edges + bin_interval_scaled)[:-1].astype( - np.float32 - ) - - n_bins = x_positions_bins.shape[0] - bin_width = (draw_scale_factor / n_bins) * draw_bin_width_scale - - self.hist = self.hist.astype(np.float32) - - for bad_val in [np.nan, np.inf, -np.inf]: - if bad_val in self.hist: - warn( - f"Problematic value <{bad_val}> found in histogram, replacing with zero" - ) - self.hist[self.hist == bad_val] = 0 - - data = np.vstack([x_positions_bins, self.hist]) - - super().__init__(data=data, colors=colors, n_colors=n_bins, **kwargs) - - self._world_object: pygfx.Group = pygfx.Group() - - for x_val, y_val, bin_center in zip( - x_positions_bins, self.hist, self.bin_centers - ): - geometry = pygfx.plane_geometry( - width=bin_width, - height=y_val, - ) - - material = pygfx.MeshBasicMaterial() - hist_bin_graphic = _HistogramBin(geometry, material) - hist_bin_graphic.position.set(x_val, (y_val) / 2, 0) - hist_bin_graphic.bin_center = bin_center - hist_bin_graphic.frequency = y_val - - self.world_object.add(hist_bin_graphic) From 444eed06af6ae253c391b1e6031981b36a4321a5 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 02:03:45 -0400 Subject: [PATCH 134/196] text changes and tests --- fastplotlib/graphics/_features/_text.py | 14 ++-- fastplotlib/graphics/text.py | 10 +-- tests/test_text_graphic.py | 91 +++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 tests/test_text_graphic.py diff --git a/fastplotlib/graphics/_features/_text.py b/fastplotlib/graphics/_features/_text.py index ab9c18f77..baa2734d5 100644 --- a/fastplotlib/graphics/_features/_text.py +++ b/fastplotlib/graphics/_features/_text.py @@ -33,7 +33,7 @@ def value(self) -> float | int: def set_value(self, graphic, value: float | int): graphic.world_object.geometry.font_size = value - self._value = value + self._value = graphic.world_object.geometry.font_size event = FeatureEvent(type="font_size", info={"value": value}) self._call_event_handlers(event) @@ -51,7 +51,7 @@ def value(self) -> pygfx.Color: def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float]): value = pygfx.Color(value) graphic.world_object.material.color = value - self._value = value + self._value = graphic.world_object.material.color event = FeatureEvent(type="face_color", info={"value": value}) self._call_event_handlers(event) @@ -69,24 +69,24 @@ def value(self) -> pygfx.Color: def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float]): value = pygfx.Color(value) graphic.world_object.material.outline_color = value - self._value = value + self._value = graphic.world_object.material.outline_color event = FeatureEvent(type="outline_color", info={"value": value}) self._call_event_handlers(event) class TextOutlineThickness(GraphicFeature): - def __init__(self, value: float | int): + def __init__(self, value: float): self._value = value super().__init__() @property - def value(self) -> float | int: + def value(self) -> float: return self._value - def set_value(self, graphic, value: float | int): + def set_value(self, graphic, value: float): graphic.world_object.material.outline_thickness = value - self._value = value + self._value = graphic.world_object.material.outline_thickness event = FeatureEvent(type="outline_thickness", info={"value": value}) self._call_event_handlers(event) diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index 1434cc15a..6500e4623 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -19,7 +19,7 @@ def __init__( font_size: float | int = 14, face_color: str | np.ndarray | list[float] | tuple[float] = "w", outline_color: str | np.ndarray | list[float] | tuple[float] = "w", - outline_thickness: float | int = 0, + outline_thickness: float = 0.0, screen_space: bool = True, offset: tuple[float] = (0, 0, 0), anchor: str = "middle-center", @@ -42,8 +42,8 @@ def __init__( outline_color: str or array, default "w" str or RGBA array to set the outline color of the text - outline_thickness: float | int, default 0 - text outline thickness + outline_thickness: float, default 0 + relative outline thickness, value between 0.0 - 0.5 screen_space: bool = True if True, text size is in screen space, if False the text size is in data space @@ -118,12 +118,12 @@ def face_color(self, color: str | np.ndarray | list[float] | tuple[float]): self._face_color.set_value(self, color) @property - def outline_thickness(self) -> float | int: + def outline_thickness(self) -> float: """text outline thickness""" return self._outline_thickness.value @outline_thickness.setter - def outline_thickness(self, thickness: float | int): + def outline_thickness(self, thickness: float): self._outline_thickness.set_value(self, thickness) @property diff --git a/tests/test_text_graphic.py b/tests/test_text_graphic.py new file mode 100644 index 000000000..fb7566fec --- /dev/null +++ b/tests/test_text_graphic.py @@ -0,0 +1,91 @@ +from numpy import testing as npt + +import fastplotlib as fpl +from fastplotlib.graphics._features import FeatureEvent, TextData, FontSize, TextFaceColor, TextOutlineColor, TextOutlineThickness + +import pygfx + + +def test_create_graphic(): + fig = fpl.Figure() + data = "lorem ipsum" + text = fig[0, 0].add_text(data) + + assert isinstance(text, fpl.TextGraphic) + + assert isinstance(text._text, TextData) + assert text.text == data + + assert text.font_size == 14 + assert isinstance(text._font_size, FontSize) + assert text.world_object.geometry.font_size == 14 + + assert text.face_color == pygfx.Color("w") + assert isinstance(text._face_color, TextFaceColor) + assert text.world_object.material.color == pygfx.Color("w") + + assert text.outline_color == pygfx.Color("w") + assert isinstance(text._outline_color, TextOutlineColor) + assert text.world_object.material.outline_color == pygfx.Color("w") + + assert text.outline_thickness == 0 + assert isinstance(text._outline_thickness, TextOutlineThickness) + assert text.world_object.material.outline_thickness == 0 + + +EVENT_RETURN_VALUE: FeatureEvent = None + + +def event_handler(ev): + global EVENT_RETURN_VALUE + EVENT_RETURN_VALUE = ev + + +def check_event( + graphic, + feature, + value +): + global EVENT_RETURN_VALUE + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.type == feature + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target == graphic.world_object + if isinstance(EVENT_RETURN_VALUE.info["value"], float): + # floating point error + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], value) + else: + assert EVENT_RETURN_VALUE.info["value"] == value + + +def test_text_changes_events(): + fig = fpl.Figure() + data = "lorem ipsum" + text = fig[0, 0].add_text(data) + + text.add_event_handler(event_handler, "text", "font_size", "face_color", "outline_color", "outline_thickness") + + text.text = "bah" + assert text.text == "bah" + # TODO: seems like there isn't a way in pygfx to get the current text as a str? + check_event(graphic=text, feature="text", value="bah") + + text.font_size = 10.0 + assert text.font_size == 10.0 + assert text.world_object.geometry.font_size == 10 + check_event(text, "font_size", 10) + + text.face_color = "r" + assert text.face_color == pygfx.Color("r") + assert text.world_object.material.color == pygfx.Color("r") + check_event(text, "face_color", pygfx.Color("r")) + + text.outline_color = "b" + assert text.outline_color == pygfx.Color("b") + assert text.world_object.material.outline_color == pygfx.Color("b") + check_event(text, "outline_color", pygfx.Color("b")) + + text.outline_thickness = 0.3 + npt.assert_almost_equal(text.outline_thickness, 0.3) + npt.assert_almost_equal(text.world_object.material.outline_thickness, 0.3) + check_event(text, "outline_thickness", 0.3) From 44d7286f06c5c73939f1aae0782dca67738ac0af Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 02:27:05 -0400 Subject: [PATCH 135/196] texture array tests with graphic --- tests/test_texture_array.py | 77 ++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/tests/test_texture_array.py b/tests/test_texture_array.py index 9cb71e253..b3b7a7a7b 100644 --- a/tests/test_texture_array.py +++ b/tests/test_texture_array.py @@ -1,9 +1,12 @@ import numpy as np from numpy import testing as npt +import pytest import pygfx +import fastplotlib as fpl from fastplotlib.graphics._features import TextureArray, WGPU_MAX_TEXTURE_SIZE +from fastplotlib.graphics.image import _ImageTile def make_data(n_rows: int, n_cols: int) -> np.ndarray: @@ -74,11 +77,30 @@ def check_set_slice(data, ta, row_slice, col_slice): npt.assert_almost_equal(ta[:, col_slice.stop :], data[:, col_slice.stop :]) -def test_small_texture(): +def make_image_graphic(data) -> fpl.ImageGraphic: + fig = fpl.Figure() + return fig[0, 0].add_image(data) + + +def check_image_graphic(texture_array, graphic): + # make sure each ImageTile has the right texture + for (texture, chunk_index, data_slice), img in zip(texture_array, graphic.world_object.children): + assert isinstance(img, _ImageTile) + assert img.geometry.grid is texture + assert img.world.x == data_slice[1].start + assert img.world.y == data_slice[0].start + + +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_small_texture(test_graphic): # tests TextureArray with dims that requires only 1 texture data = make_data(1_000, 1_000) - ta = TextureArray(data) + if test_graphic: + graphic = make_image_graphic(data) + ta = graphic.data + else: + ta = TextureArray(data) check_texture_array( data=data, @@ -91,14 +113,22 @@ def test_small_texture(): col_indices_values=np.array([0]), ) + if test_graphic: + check_image_graphic(ta, graphic) + check_set_slice(data, ta, slice(50, 200), slice(600, 800)) -def test_texture_at_limit(): +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_texture_at_limit(test_graphic): # tests TextureArray with data that is 8192 x 8192 data = make_data(WGPU_MAX_TEXTURE_SIZE, WGPU_MAX_TEXTURE_SIZE) - ta = TextureArray(data) + if test_graphic: + graphic = make_image_graphic(data) + ta = graphic.data + else: + ta = TextureArray(data) check_texture_array( data, @@ -111,13 +141,21 @@ def test_texture_at_limit(): col_indices_values=np.array([0]), ) + if test_graphic: + check_image_graphic(ta, graphic) + check_set_slice(data, ta, slice(5000, 8000), slice(2000, 3000)) -def test_wide(): +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_wide(test_graphic): data = make_data(10_000, 20_000) - ta = TextureArray(data) + if test_graphic: + graphic = make_image_graphic(data) + ta = graphic.data + else: + ta = TextureArray(data) check_texture_array( data, @@ -130,13 +168,21 @@ def test_wide(): col_indices_values=np.array([0, 8192, 16384]), ) + if test_graphic: + check_image_graphic(ta, graphic) + check_set_slice(data, ta, slice(6_000, 9_000), slice(12_000, 18_000)) -def test_tall(): +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_tall(test_graphic): data = make_data(20_000, 10_000) - ta = TextureArray(data) + if test_graphic: + graphic = make_image_graphic(data) + ta = graphic.data + else: + ta = TextureArray(data) check_texture_array( data, @@ -149,13 +195,21 @@ def test_tall(): col_indices_values=np.array([0, 8192]), ) + if test_graphic: + check_image_graphic(ta, graphic) + check_set_slice(data, ta, slice(12_000, 18_000), slice(6_000, 9_000)) -def test_square(): +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_square(test_graphic): data = make_data(20_000, 20_000) - ta = TextureArray(data) + if test_graphic: + graphic = make_image_graphic(data) + ta = graphic.data + else: + ta = TextureArray(data) check_texture_array( data, @@ -168,4 +222,7 @@ def test_square(): col_indices_values=np.array([0, 8192, 16384]), ) + if test_graphic: + check_image_graphic(ta, graphic) + check_set_slice(data, ta, slice(12_000, 18_000), slice(16_000, 19_000)) From 080d8ae072659cd9c2617d19d45f0b96a1c6ed81 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 02:49:45 -0400 Subject: [PATCH 136/196] image graphic tests --- tests/test_image_graphic.py | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/test_image_graphic.py b/tests/test_image_graphic.py index d30ad76b0..51a20e17c 100644 --- a/tests/test_image_graphic.py +++ b/tests/test_image_graphic.py @@ -3,6 +3,7 @@ import imageio.v3 as iio import fastplotlib as fpl +from fastplotlib.graphics._features import FeatureEvent from fastplotlib.utils import make_colors GRAY_IMAGE = iio.imread("imageio:camera.png") @@ -15,6 +16,31 @@ # new screenshot tests too for these when in graphics +EVENT_RETURN_VALUE: FeatureEvent = None + + +def event_handler(ev): + global EVENT_RETURN_VALUE + EVENT_RETURN_VALUE = ev + + +def check_event( + graphic, + feature, + value +): + global EVENT_RETURN_VALUE + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.type == feature + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target == graphic.world_object + if isinstance(EVENT_RETURN_VALUE.info["value"], float): + # floating point error + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], value) + else: + assert EVENT_RETURN_VALUE.info["value"] == value + + def check_set_slice( data: np.ndarray, image_graphic: fpl.ImageGraphic, @@ -33,16 +59,31 @@ def check_set_slice( ) npt.assert_almost_equal(data_values[:, col_slice.stop :], data[:, col_slice.stop :]) + global EVENT_RETURN_VALUE + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.type == "data" + assert EVENT_RETURN_VALUE.graphic == image_graphic + assert EVENT_RETURN_VALUE.target == image_graphic.world_object + assert EVENT_RETURN_VALUE.info["key"] == (row_slice, col_slice) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], 1) + def test_gray(): fig = fpl.Figure() ig = fig[0, 0].add_image(GRAY_IMAGE) assert isinstance(ig, fpl.ImageGraphic) + ig.add_event_handler(event_handler, "data", "cmap", "vmin", "vmax", "interpolation", "cmap_interpolation") + npt.assert_almost_equal(ig.data.value, GRAY_IMAGE) ig.cmap = "viridis" assert ig.cmap == "viridis" + check_event( + graphic=ig, + feature="cmap", + value="viridis" + ) new_colors = make_colors(256, "viridis") for child in ig.world_object.children: @@ -63,6 +104,11 @@ def test_gray(): assert ig.interpolation == "linear" for child in ig.world_object.children: assert child.material.interpolation == "linear" + check_event( + graphic=ig, + feature="interpolation", + value="linear" + ) assert ig.cmap_interpolation == "linear" for child in ig.world_object.children: @@ -72,6 +118,11 @@ def test_gray(): assert ig.cmap_interpolation == "nearest" for child in ig.world_object.children: assert child.material.map_interpolation == "nearest" + check_event( + graphic=ig, + feature="cmap_interpolation", + value="nearest" + ) npt.assert_almost_equal(ig.vmin, GRAY_IMAGE.min()) npt.assert_almost_equal(ig.vmax, GRAY_IMAGE.max()) @@ -80,14 +131,30 @@ def test_gray(): assert ig.vmin == 50 for child in ig.world_object.children: assert child.material.clim == (50, ig.vmax) + check_event( + graphic=ig, + feature="vmin", + value=50 + ) ig.vmax = 100 assert ig.vmax == 100 for child in ig.world_object.children: assert child.material.clim == (ig.vmin, 100) + check_event( + graphic=ig, + feature="vmax", + value=100 + ) + + # test reset + ig.reset_vmin_vmax() + npt.assert_almost_equal(ig.vmin, GRAY_IMAGE.min()) + npt.assert_almost_equal(ig.vmax, GRAY_IMAGE.max()) check_set_slice(GRAY_IMAGE, ig, slice(100, 200), slice(200, 300)) + # test setting all values ig.data = 1 npt.assert_almost_equal(ig.data.value, 1) @@ -98,6 +165,8 @@ def test_rgb(): ig = fig[0, 0].add_image(RGB_IMAGE) assert isinstance(ig, fpl.ImageGraphic) + ig.add_event_handler(event_handler, "data") + npt.assert_almost_equal(ig.data.value, RGB_IMAGE) assert ig.interpolation == "nearest" @@ -122,6 +191,11 @@ def test_rgb(): for child in ig.world_object.children: assert child.material.clim == (ig.vmin, 100) + # test reset + ig.reset_vmin_vmax() + npt.assert_almost_equal(ig.vmin, RGB_IMAGE.min()) + npt.assert_almost_equal(ig.vmax, RGB_IMAGE.max()) + check_set_slice(RGB_IMAGE, ig, slice(100, 200), slice(200, 300)) @@ -146,3 +220,4 @@ def test_rgba(): # check that fancy indexing works npt.assert_almost_equal(ig.data.value, rgba) + From 51db9ddddeb4de86d314173bcd30ecfacab49f29 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 02:52:01 -0400 Subject: [PATCH 137/196] update image features --- fastplotlib/graphics/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 94ea3d668..92b6d4d71 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -71,7 +71,7 @@ def chunk_index(self) -> tuple[int, int]: class ImageGraphic(Graphic): - _features = {"data", "cmap", "vmin", "vmax"} + _features = {"data", "cmap", "vmin", "vmax", "interpolation", "cmap_interpolation"} @property def data(self) -> TextureArray: From 1d3c111265188599b1e24706f9fd135236b7ada2 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 02:53:05 -0400 Subject: [PATCH 138/196] black --- fastplotlib/graphics/_collection_base.py | 7 ++- fastplotlib/graphics/line.py | 5 ++- fastplotlib/graphics/line_collection.py | 1 + fastplotlib/graphics/text.py | 9 +++- tests/test_colors_buffer_manager.py | 24 +++++------ tests/test_image_graphic.py | 48 +++++++-------------- tests/test_positions_data_buffer_manager.py | 1 + tests/test_positions_graphics.py | 30 +++++-------- tests/test_text_graphic.py | 24 ++++++++--- tests/test_texture_array.py | 4 +- 10 files changed, 74 insertions(+), 79 deletions(-) diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index c1986de10..da7a7d7eb 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -113,10 +113,12 @@ def my_handler(event): types = args if decorating else args[1:] if decorating: + def decorator(_callback): for g in self.graphics: g.add_event_handler(_callback, *types) return _callback + return decorator for g in self.graphics: @@ -262,10 +264,7 @@ def remove_event_handler(self, callback, *types): self[:].remove_event_handler(callback, *types) def __getitem__(self, key) -> CollectionIndexer: - return self._indexer( - selection=self.graphics[key], - features=self.features - ) + return self._indexer(selection=self.graphics[key], features=self.features) def __del__(self): self.world_object.clear() diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 40394dbd6..918a4a8ef 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -88,7 +88,10 @@ def __init__( if uniform_color: geometry = pygfx.Geometry(positions=self._data.buffer) material = MaterialCls( - thickness=self.thickness, color_mode="uniform", color=self.colors, pick_write=True + thickness=self.thickness, + color_mode="uniform", + color=self.colors, + pick_write=True, ) else: material = MaterialCls( diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index d3905ad52..adc1589ae 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -13,6 +13,7 @@ class LineCollectionProperties: """Mix-in class for LineCollection properties""" + @property def colors(self) -> CollectionFeature: return CollectionFeature(self.graphics, "colors") diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index 6500e4623..fcee6129b 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -12,7 +12,14 @@ class TextGraphic(Graphic): - _features = {"text", "font_size", "face_color", "outline_color", "outline_thickness"} + _features = { + "text", + "font_size", + "face_color", + "outline_color", + "outline_thickness", + } + def __init__( self, text: str, diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py index 116dcc56b..252c6e5c3 100644 --- a/tests/test_colors_buffer_manager.py +++ b/tests/test_colors_buffer_manager.py @@ -6,7 +6,12 @@ import fastplotlib as fpl from fastplotlib.graphics._features import VertexColors, FeatureEvent -from .utils import generate_slice_indices, assert_pending_uploads, generate_color_inputs, generate_positions_spiral_data +from .utils import ( + generate_slice_indices, + assert_pending_uploads, + generate_color_inputs, + generate_positions_spiral_data, +) def make_colors_buffer() -> VertexColors: @@ -36,9 +41,7 @@ def test_create_buffer(color_input): npt.assert_almost_equal(colors[:], truth) -@pytest.mark.parametrize( - "test_graphic", [False, "line", "scatter"] -) +@pytest.mark.parametrize("test_graphic", [False, "line", "scatter"]) def test_int(test_graphic): # setting single points if test_graphic: @@ -70,7 +73,9 @@ def test_int(test_graphic): assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target is graphic.world_object assert EVENT_RETURN_VALUE.info["key"] == 3 - npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], np.array([[1, 0, 0, 1]])) + npt.assert_almost_equal( + EVENT_RETURN_VALUE.info["value"], np.array([[1, 0, 0, 1]]) + ) assert EVENT_RETURN_VALUE.info["user_value"] == "r" colors[6] = [0.0, 1.0, 1.0, 1.0] @@ -86,9 +91,7 @@ def test_int(test_graphic): npt.assert_almost_equal(colors[2], [1.0, 0.0, 1.0, 0.5]) -@pytest.mark.parametrize( - "test_graphic", [False, "line", "scatter"] -) +@pytest.mark.parametrize("test_graphic", [False, "line", "scatter"]) @pytest.mark.parametrize( "slice_method", [generate_slice_indices(i) for i in range(0, 16)] ) @@ -183,9 +186,7 @@ def test_tuple(test_graphic, slice_method): @pytest.mark.parametrize( "slice_method", [generate_slice_indices(i) for i in range(1, 16)] ) -@pytest.mark.parametrize( - "test_graphic", [False, "line", "scatter"] -) +@pytest.mark.parametrize("test_graphic", [False, "line", "scatter"]) def test_slice(color_input, slice_method: dict, test_graphic: bool): # slicing only first dim if test_graphic: @@ -247,4 +248,3 @@ def test_slice(color_input, slice_method: dict, test_graphic: bool): # reset colors[:] = (1, 1, 1, 1) npt.assert_almost_equal(colors[:], np.repeat([[1.0, 1.0, 1.0, 1.0]], 10, axis=0)) - diff --git a/tests/test_image_graphic.py b/tests/test_image_graphic.py index 51a20e17c..9f89e8aa8 100644 --- a/tests/test_image_graphic.py +++ b/tests/test_image_graphic.py @@ -24,11 +24,7 @@ def event_handler(ev): EVENT_RETURN_VALUE = ev -def check_event( - graphic, - feature, - value -): +def check_event(graphic, feature, value): global EVENT_RETURN_VALUE assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) assert EVENT_RETURN_VALUE.type == feature @@ -73,17 +69,21 @@ def test_gray(): ig = fig[0, 0].add_image(GRAY_IMAGE) assert isinstance(ig, fpl.ImageGraphic) - ig.add_event_handler(event_handler, "data", "cmap", "vmin", "vmax", "interpolation", "cmap_interpolation") + ig.add_event_handler( + event_handler, + "data", + "cmap", + "vmin", + "vmax", + "interpolation", + "cmap_interpolation", + ) npt.assert_almost_equal(ig.data.value, GRAY_IMAGE) ig.cmap = "viridis" assert ig.cmap == "viridis" - check_event( - graphic=ig, - feature="cmap", - value="viridis" - ) + check_event(graphic=ig, feature="cmap", value="viridis") new_colors = make_colors(256, "viridis") for child in ig.world_object.children: @@ -104,11 +104,7 @@ def test_gray(): assert ig.interpolation == "linear" for child in ig.world_object.children: assert child.material.interpolation == "linear" - check_event( - graphic=ig, - feature="interpolation", - value="linear" - ) + check_event(graphic=ig, feature="interpolation", value="linear") assert ig.cmap_interpolation == "linear" for child in ig.world_object.children: @@ -118,11 +114,7 @@ def test_gray(): assert ig.cmap_interpolation == "nearest" for child in ig.world_object.children: assert child.material.map_interpolation == "nearest" - check_event( - graphic=ig, - feature="cmap_interpolation", - value="nearest" - ) + check_event(graphic=ig, feature="cmap_interpolation", value="nearest") npt.assert_almost_equal(ig.vmin, GRAY_IMAGE.min()) npt.assert_almost_equal(ig.vmax, GRAY_IMAGE.max()) @@ -131,21 +123,13 @@ def test_gray(): assert ig.vmin == 50 for child in ig.world_object.children: assert child.material.clim == (50, ig.vmax) - check_event( - graphic=ig, - feature="vmin", - value=50 - ) + check_event(graphic=ig, feature="vmin", value=50) ig.vmax = 100 assert ig.vmax == 100 for child in ig.world_object.children: assert child.material.clim == (ig.vmin, 100) - check_event( - graphic=ig, - feature="vmax", - value=100 - ) + check_event(graphic=ig, feature="vmax", value=100) # test reset ig.reset_vmin_vmax() @@ -154,7 +138,6 @@ def test_gray(): check_set_slice(GRAY_IMAGE, ig, slice(100, 200), slice(200, 300)) - # test setting all values ig.data = 1 npt.assert_almost_equal(ig.data.value, 1) @@ -220,4 +203,3 @@ def test_rgba(): # check that fancy indexing works npt.assert_almost_equal(ig.data.value, rgba) - diff --git a/tests/test_positions_data_buffer_manager.py b/tests/test_positions_data_buffer_manager.py index 948a67e54..de9d179d8 100644 --- a/tests/test_positions_data_buffer_manager.py +++ b/tests/test_positions_data_buffer_manager.py @@ -42,6 +42,7 @@ def test_create_buffer(data): # test 3D spiral npt.assert_almost_equal(points_data[:], generate_positions_spiral_data("xyz")) + @pytest.mark.parametrize("test_graphic", [False, "line", "scatter"]) def test_int(test_graphic): # test setting single points diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index d7a0ea0ff..68f8cbbe9 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -13,7 +13,7 @@ UniformSize, PointsSizesFeature, Thickness, - FeatureEvent + FeatureEvent, ) from .utils import ( @@ -337,9 +337,7 @@ def test_incompatible_cmap_color_args(graphic_type, cmap, colors, uniform_color) @pytest.mark.parametrize("graphic_type", ["line", "scatter"]) -@pytest.mark.parametrize( - "colors", [*generate_color_inputs("multi")] -) +@pytest.mark.parametrize("colors", [*generate_color_inputs("multi")]) @pytest.mark.parametrize( "uniform_color", [True] # none of these will work with a uniform buffer ) @@ -363,12 +361,8 @@ def test_incompatible_color_args(graphic_type, colors, uniform_color): graphic = fig[0, 0].add_scatter(data=data, **kwargs) -@pytest.mark.parametrize( - "sizes", [None, 5.0, np.linspace(3, 8, 10, dtype=np.float32)] -) -@pytest.mark.parametrize( - "uniform_size", [None, False] -) +@pytest.mark.parametrize("sizes", [None, 5.0, np.linspace(3, 8, 10, dtype=np.float32)]) +@pytest.mark.parametrize("uniform_size", [None, False]) def test_sizes(sizes, uniform_size): # test scatter sizes fig = fpl.Figure() @@ -391,15 +385,13 @@ def test_sizes(sizes, uniform_size): sizes = 1 # default sizes npt.assert_almost_equal(graphic.sizes.value, sizes) - npt.assert_almost_equal(graphic.world_object.geometry.sizes.data, graphic.sizes.value) + npt.assert_almost_equal( + graphic.world_object.geometry.sizes.data, graphic.sizes.value + ) -@pytest.mark.parametrize( - "sizes", [None, 5.0] -) -@pytest.mark.parametrize( - "uniform_size", [True] -) +@pytest.mark.parametrize("sizes", [None, 5.0]) +@pytest.mark.parametrize("uniform_size", [True]) def test_uniform_size(sizes, uniform_size): fig = fpl.Figure() @@ -423,9 +415,7 @@ def test_uniform_size(sizes, uniform_size): npt.assert_almost_equal(graphic.world_object.material.size, sizes) -@pytest.mark.parametrize( - "thickness", [None, 0.5, 5.0] -) +@pytest.mark.parametrize("thickness", [None, 0.5, 5.0]) def test_thickness(thickness): fig = fpl.Figure() diff --git a/tests/test_text_graphic.py b/tests/test_text_graphic.py index fb7566fec..a13dfe690 100644 --- a/tests/test_text_graphic.py +++ b/tests/test_text_graphic.py @@ -1,7 +1,14 @@ from numpy import testing as npt import fastplotlib as fpl -from fastplotlib.graphics._features import FeatureEvent, TextData, FontSize, TextFaceColor, TextOutlineColor, TextOutlineThickness +from fastplotlib.graphics._features import ( + FeatureEvent, + TextData, + FontSize, + TextFaceColor, + TextOutlineColor, + TextOutlineThickness, +) import pygfx @@ -41,11 +48,7 @@ def event_handler(ev): EVENT_RETURN_VALUE = ev -def check_event( - graphic, - feature, - value -): +def check_event(graphic, feature, value): global EVENT_RETURN_VALUE assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) assert EVENT_RETURN_VALUE.type == feature @@ -63,7 +66,14 @@ def test_text_changes_events(): data = "lorem ipsum" text = fig[0, 0].add_text(data) - text.add_event_handler(event_handler, "text", "font_size", "face_color", "outline_color", "outline_thickness") + text.add_event_handler( + event_handler, + "text", + "font_size", + "face_color", + "outline_color", + "outline_thickness", + ) text.text = "bah" assert text.text == "bah" diff --git a/tests/test_texture_array.py b/tests/test_texture_array.py index b3b7a7a7b..5aecf49a5 100644 --- a/tests/test_texture_array.py +++ b/tests/test_texture_array.py @@ -84,7 +84,9 @@ def make_image_graphic(data) -> fpl.ImageGraphic: def check_image_graphic(texture_array, graphic): # make sure each ImageTile has the right texture - for (texture, chunk_index, data_slice), img in zip(texture_array, graphic.world_object.children): + for (texture, chunk_index, data_slice), img in zip( + texture_array, graphic.world_object.children + ): assert isinstance(img, _ImageTile) assert img.geometry.grid is texture assert img.world.x == data_slice[1].start From 252b8bb7f7f603dcb33f86a5591697fc257bd936 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 03:12:05 -0400 Subject: [PATCH 139/196] append data and world xy for graphic pointer events --- fastplotlib/graphics/_base.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index f8edc43ec..e7dc6acf0 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -257,9 +257,9 @@ def my_handler(event): ) def decorator(_callback): - _callback_injector = partial( + _callback_wrapper = partial( self._handle_event, _callback - ) # adds graphic instance as attribute + ) # adds graphic instance as attribute and other things for t in types: # add to our record @@ -268,13 +268,13 @@ def decorator(_callback): if t in self._features: # fpl feature event feature = getattr(self, f"_{t}") - feature.add_event_handler(_callback_injector) + feature.add_event_handler(_callback_wrapper) else: # wrap pygfx event - self.world_object._event_handlers[t].add(_callback_injector) + self.world_object._event_handlers[t].add(_callback_wrapper) # keep track of the partial too - self._event_handler_wrappers[t].add((_callback, _callback_injector)) + self._event_handler_wrappers[t].add((_callback, _callback_wrapper)) return _callback if decorating: @@ -293,6 +293,18 @@ def _handle_event(self, callback, event: pygfx.Event): # for feature events event._target = self.world_object + if isinstance(event, pygfx.PointerEvent): + # map from screen to world space and data space + world_xy = self._plot_area.map_screen_to_world(event) + + # subtract offset to map to data + data_xy = world_xy - self.offset + + # append attributes + event.x_world, event.y_world = world_xy[:2] + event.x_data, event.y_data = data_xy[:2] + + with log_exception(f"Error during handling {event.type} event"): callback(event) From 197a64fc76f4b0c623d94c4b80bab9dfe48fa562 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 03:13:10 -0400 Subject: [PATCH 140/196] black --- fastplotlib/graphics/_base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index e7dc6acf0..eed0a71d2 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -304,7 +304,6 @@ def _handle_event(self, callback, event: pygfx.Event): event.x_world, event.y_world = world_xy[:2] event.x_data, event.y_data = data_xy[:2] - with log_exception(f"Error during handling {event.type} event"): callback(event) From b0d20f67cbf1375cdf57097815b4b22a9f9b6984 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 03:16:38 -0400 Subject: [PATCH 141/196] move constructor to top --- fastplotlib/graphics/_base.py | 118 ++++++++++++++++---------------- fastplotlib/graphics/image.py | 108 ++++++++++++++--------------- fastplotlib/graphics/line.py | 18 ++--- fastplotlib/graphics/scatter.py | 34 ++++----- 4 files changed, 139 insertions(+), 139 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index eed0a71d2..6370fe720 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -44,65 +44,6 @@ class Graphic: _features = {} - @property - def events(self) -> tuple[str]: - """events supported by this graphic""" - return (*tuple(self._features), *PYGFX_EVENTS) - - @property - def name(self) -> str | None: - """Graphic name""" - return self._name.value - - @name.setter - def name(self, value: str): - self._name.set_value(self, value) - - @property - def offset(self) -> np.ndarray: - """Offset position of the graphic, array: [x, y, z]""" - return self._offset.value - - @offset.setter - def offset(self, value: np.ndarray | list | tuple): - self._offset.set_value(self, value) - - @property - def rotation(self) -> np.ndarray: - """Orientation of the graphic as a quaternion""" - return self._rotation.value - - @rotation.setter - def rotation(self, value: np.ndarray | list | tuple): - self._rotation.set_value(self, value) - - @property - def visible(self) -> bool: - """Whether the graphic is visible""" - return self._visible.value - - @visible.setter - def visible(self, value: bool): - self._visible.set_value(self, value) - - @property - def deleted(self) -> bool: - """used to emit an event after the graphic is deleted""" - return self._deleted.value - - @deleted.setter - def deleted(self, value: bool): - self._deleted.set_value(self, value) - - @property - def block_events(self) -> bool: - """Used to block events for a graphic and prevent recursion.""" - return self._block_events - - @block_events.setter - def block_events(self, value: bool): - self._block_events = value - def __init_subclass__(cls, **kwargs): # set the type of the graphic in lower case like "image", "line_collection", etc. cls.type = ( @@ -173,6 +114,65 @@ def __init__( self._visible = Visible(visible) self._block_events = False + @property + def events(self) -> tuple[str]: + """events supported by this graphic""" + return (*tuple(self._features), *PYGFX_EVENTS) + + @property + def name(self) -> str | None: + """Graphic name""" + return self._name.value + + @name.setter + def name(self, value: str): + self._name.set_value(self, value) + + @property + def offset(self) -> np.ndarray: + """Offset position of the graphic, array: [x, y, z]""" + return self._offset.value + + @offset.setter + def offset(self, value: np.ndarray | list | tuple): + self._offset.set_value(self, value) + + @property + def rotation(self) -> np.ndarray: + """Orientation of the graphic as a quaternion""" + return self._rotation.value + + @rotation.setter + def rotation(self, value: np.ndarray | list | tuple): + self._rotation.set_value(self, value) + + @property + def visible(self) -> bool: + """Whether the graphic is visible""" + return self._visible.value + + @visible.setter + def visible(self, value: bool): + self._visible.set_value(self, value) + + @property + def deleted(self) -> bool: + """used to emit an event after the graphic is deleted""" + return self._deleted.value + + @deleted.setter + def deleted(self, value: bool): + self._deleted.set_value(self, value) + + @property + def block_events(self) -> bool: + """Used to block events for a graphic and prevent recursion.""" + return self._block_events + + @block_events.setter + def block_events(self, value: bool): + self._block_events = value + @property def world_object(self) -> pygfx.WorldObject: """Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly.""" diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 92b6d4d71..c5829e993 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -73,60 +73,6 @@ def chunk_index(self) -> tuple[int, int]: class ImageGraphic(Graphic): _features = {"data", "cmap", "vmin", "vmax", "interpolation", "cmap_interpolation"} - @property - def data(self) -> TextureArray: - """Get or set the image data""" - return self._data - - @data.setter - def data(self, data): - self._data[:] = data - - @property - def cmap(self) -> str: - """colormap name""" - return self._cmap.value - - @cmap.setter - def cmap(self, name: str): - self._cmap.set_value(self, name) - - @property - def vmin(self) -> float: - """lower contrast limit""" - return self._vmin.value - - @vmin.setter - def vmin(self, value: float): - self._vmin.set_value(self, value) - - @property - def vmax(self) -> float: - """upper contrast limit""" - return self._vmax.value - - @vmax.setter - def vmax(self, value: float): - self._vmax.set_value(self, value) - - @property - def interpolation(self) -> str: - """image data interpolation method""" - return self._interpolation.value - - @interpolation.setter - def interpolation(self, value: str): - self._interpolation.set_value(self, value) - - @property - def cmap_interpolation(self) -> str: - """cmap interpolation method""" - return self._cmap_interpolation.value - - @cmap_interpolation.setter - def cmap_interpolation(self, value: str): - self._cmap_interpolation.set_value(self, value) - def __init__( self, data: Any, @@ -243,6 +189,60 @@ def __init__( self._set_world_object(world_object) + @property + def data(self) -> TextureArray: + """Get or set the image data""" + return self._data + + @data.setter + def data(self, data): + self._data[:] = data + + @property + def cmap(self) -> str: + """colormap name""" + return self._cmap.value + + @cmap.setter + def cmap(self, name: str): + self._cmap.set_value(self, name) + + @property + def vmin(self) -> float: + """lower contrast limit""" + return self._vmin.value + + @vmin.setter + def vmin(self, value: float): + self._vmin.set_value(self, value) + + @property + def vmax(self) -> float: + """upper contrast limit""" + return self._vmax.value + + @vmax.setter + def vmax(self, value: float): + self._vmax.set_value(self, value) + + @property + def interpolation(self) -> str: + """image data interpolation method""" + return self._interpolation.value + + @interpolation.setter + def interpolation(self, value: str): + self._interpolation.set_value(self, value) + + @property + def cmap_interpolation(self) -> str: + """cmap interpolation method""" + return self._cmap_interpolation.value + + @cmap_interpolation.setter + def cmap_interpolation(self, value: str): + self._cmap_interpolation.set_value(self, value) + def reset_vmin_vmax(self): """ Reset the vmin, vmax by estimating it from the data diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 918a4a8ef..8c9ad2072 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -13,15 +13,6 @@ class LineGraphic(PositionsGraphic): _features = {"data", "colors", "cmap", "thickness"} - @property - def thickness(self) -> float: - """Graphic name""" - return self._thickness.value - - @thickness.setter - def thickness(self, value: float): - self._thickness.set_value(self, value) - def __init__( self, data: Any, @@ -105,6 +96,15 @@ def __init__( self._set_world_object(world_object) + @property + def thickness(self) -> float: + """Graphic name""" + return self._thickness.value + + @thickness.setter + def thickness(self, value: float): + self._thickness.set_value(self, value) + def add_linear_selector( self, selection: float = None, padding: float = 0.0, axis: str = "x", **kwargs ) -> LinearSelector: diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index a6fba9121..39d815c95 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -10,23 +10,6 @@ class ScatterGraphic(PositionsGraphic): _features = {"data", "sizes", "colors", "cmap"} - @property - def sizes(self) -> PointsSizesFeature | float: - """Get or set the scatter point size(s)""" - if isinstance(self._sizes, PointsSizesFeature): - return self._sizes - - elif isinstance(self._sizes, UniformSize): - return self._sizes.value - - @sizes.setter - def sizes(self, value): - if isinstance(self._sizes, PointsSizesFeature): - self._sizes[:] = value - - elif isinstance(self._sizes, UniformSize): - self._sizes.set_value(self, value) - def __init__( self, data: Any, @@ -120,3 +103,20 @@ def __init__( ) self._set_world_object(world_object) + + @property + def sizes(self) -> PointsSizesFeature | float: + """Get or set the scatter point size(s)""" + if isinstance(self._sizes, PointsSizesFeature): + return self._sizes + + elif isinstance(self._sizes, UniformSize): + return self._sizes.value + + @sizes.setter + def sizes(self, value): + if isinstance(self._sizes, PointsSizesFeature): + self._sizes[:] = value + + elif isinstance(self._sizes, UniformSize): + self._sizes.set_value(self, value) From 9f92ab712c871add28db7c8d76b8925692fe1e78 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 03:29:07 -0400 Subject: [PATCH 142/196] example tests for wide and square hm --- examples/desktop/heatmap/square.py | 32 ++++++++++++++++++++++++++++++ examples/desktop/heatmap/wide.py | 32 ++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 examples/desktop/heatmap/square.py create mode 100644 examples/desktop/heatmap/wide.py diff --git a/examples/desktop/heatmap/square.py b/examples/desktop/heatmap/square.py new file mode 100644 index 000000000..6eb4be3ff --- /dev/null +++ b/examples/desktop/heatmap/square.py @@ -0,0 +1,32 @@ +""" +Square heatmap test +=================== +""" + +# test_example = true + +import fastplotlib as fpl +import numpy as np + + +fig = fpl.Figure() + +xs = np.linspace(0, 1_000, 20_000) + +sine = np.sin(np.sqrt(xs)) + +data = np.vstack([sine * i for i in range(20_000)]) + +# plot the image data +img = fig[0, 0].add_image(data=data, name="heatmap") + +fig.show() + +fig.canvas.set_logical_size(500, 500) + +fig[0, 0].auto_scale(maintain_aspect=True) + + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/heatmap/wide.py b/examples/desktop/heatmap/wide.py new file mode 100644 index 000000000..2be3383d1 --- /dev/null +++ b/examples/desktop/heatmap/wide.py @@ -0,0 +1,32 @@ +""" +Wide heatmap test +================= +""" + +# test_example = true + +import fastplotlib as fpl +import numpy as np + + +fig = fpl.Figure() + +xs = np.linspace(0, 1_000, 20_000) + +sine = np.sin(np.sqrt(xs)) + +data = np.vstack([sine * i for i in range(10_000)]) + +# plot the image data +img = fig[0, 0].add_image(data=data, name="heatmap") + +fig.show() + +fig.canvas.set_logical_size(500, 500) + +fig[0, 0].auto_scale(maintain_aspect=True) + + +if __name__ == "__main__": + print(__doc__) + fpl.run() From 613cc6815d0c566de9367e6bfd725541624b8beb Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 05:14:17 -0400 Subject: [PATCH 143/196] update ci --- .github/workflows/black.yml | 16 ++++++++++++++-- .github/workflows/ci.yml | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index bcb2d2b33..bec47fdc5 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -1,12 +1,24 @@ name: Lint -on: [push, pull_request] +on: + push: + branches: + - main + pull_request: + branches: + - main + types: + - opened + - reopened + - synchronize + - ready_for_review jobs: lint: runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} steps: - uses: actions/checkout@v4 - uses: psf/black@stable with: - src: "./fastplotlib" \ No newline at end of file + src: "./fastplotlib" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7db694d01..9adb67f77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: docs-build: name: Docs runs-on: bigmem + if: ${{ !github.event.pull_request.draft }} strategy: fail-fast: false steps: From 12a00601fbd4fc488a4863fe076fe3e77017f0fa Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 05:18:55 -0400 Subject: [PATCH 144/196] fix message --- fastplotlib/utils/functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index 9e007b563..73752ba5e 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -91,8 +91,8 @@ def make_colors(n_colors: int, cmap: str, alpha: float = 1.0) -> np.ndarray: max_colors = cmap.shape[0] if n_colors > cmap.shape[0]: raise ValueError( - f"You have requested <{n_colors}> but only <{max_colors} existing for the " - f"chosen cmap: <{cmap}>" + f"You have requested <{n_colors}> colors but only <{max_colors}> exist for the " + f"chosen cmap: <{name}>" ) return cmap[:n_colors] From 49ffefda2563b94b423a11009b6fc9426f77913a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 06:04:55 -0400 Subject: [PATCH 145/196] update examples --- .../heatmap/{square.py => heatmap_square.py} | 10 ++-- .../heatmap/{wide.py => heatmap_wide.py} | 10 ++-- examples/desktop/line/line_cmap.py | 4 +- examples/desktop/line/line_colorslice.py | 55 ++++++++++++++----- examples/desktop/scatter/scatter_cmap.py | 6 +- .../desktop/scatter/scatter_colorslice.py | 7 ++- examples/desktop/scatter/scatter_present.py | 38 ------------- .../desktop/screenshots/line_colorslice.png | 4 +- .../screenshots/line_present_scaling.png | 3 - .../desktop/screenshots/scatter_present.png | 3 - 10 files changed, 65 insertions(+), 75 deletions(-) rename examples/desktop/heatmap/{square.py => heatmap_square.py} (75%) rename examples/desktop/heatmap/{wide.py => heatmap_wide.py} (75%) delete mode 100644 examples/desktop/scatter/scatter_present.py delete mode 100644 examples/desktop/screenshots/line_present_scaling.png delete mode 100644 examples/desktop/screenshots/scatter_present.png diff --git a/examples/desktop/heatmap/square.py b/examples/desktop/heatmap/heatmap_square.py similarity index 75% rename from examples/desktop/heatmap/square.py rename to examples/desktop/heatmap/heatmap_square.py index 6eb4be3ff..002af81f9 100644 --- a/examples/desktop/heatmap/square.py +++ b/examples/desktop/heatmap/heatmap_square.py @@ -1,6 +1,7 @@ """ -Square heatmap test -=================== +Square Heatmap +============== +square heatmap test """ # test_example = true @@ -22,10 +23,9 @@ fig.show() -fig.canvas.set_logical_size(500, 500) - -fig[0, 0].auto_scale(maintain_aspect=True) +fig.canvas.set_logical_size(1500, 1500) +fig[0, 0].auto_scale() if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/heatmap/wide.py b/examples/desktop/heatmap/heatmap_wide.py similarity index 75% rename from examples/desktop/heatmap/wide.py rename to examples/desktop/heatmap/heatmap_wide.py index 2be3383d1..f1080b522 100644 --- a/examples/desktop/heatmap/wide.py +++ b/examples/desktop/heatmap/heatmap_wide.py @@ -1,6 +1,7 @@ """ -Wide heatmap test -================= +Wide Heatmap +============ +Wide example """ # test_example = true @@ -22,10 +23,9 @@ fig.show() -fig.canvas.set_logical_size(500, 500) - -fig[0, 0].auto_scale(maintain_aspect=True) +fig.canvas.set_logical_size(1500, 1500) +fig[0, 0].auto_scale() if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line/line_cmap.py b/examples/desktop/line/line_cmap.py index 45c878863..f18ceb201 100644 --- a/examples/desktop/line/line_cmap.py +++ b/examples/desktop/line/line_cmap.py @@ -30,12 +30,12 @@ ) # qualitative colormaps, useful for cluster labels or other types of categorical labels -cmap_transform = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 +labels = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 cosine_graphic = fig[0, 0].add_line( data=cosine, thickness=10, cmap="tab10", - cmap_transform=cmap_transform + cmap_transform=labels ) fig.show() diff --git a/examples/desktop/line/line_colorslice.py b/examples/desktop/line/line_colorslice.py index 25a6329ae..28b877793 100644 --- a/examples/desktop/line/line_colorslice.py +++ b/examples/desktop/line/line_colorslice.py @@ -1,6 +1,6 @@ """ Line Plot -============ +========= Example showing color slicing with cosine, sine, sinc lines. """ @@ -15,25 +15,48 @@ xs = np.linspace(-10, 10, 100) # sine wave ys = np.sin(xs) -sine = np.dstack([xs, ys])[0] +sine = np.column_stack([xs, ys]) # cosine wave -ys = np.cos(xs) + 5 -cosine = np.dstack([xs, ys])[0] +ys = np.cos(xs) +cosine = np.column_stack([xs, ys]) # sinc function a = 0.5 -ys = np.sinc(xs) * 3 + 8 -sinc = np.dstack([xs, ys])[0] +ys = np.sinc(xs) * 3 +sinc = np.column_stack([xs, ys]) -sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") +sine_graphic = fig[0, 0].add_line( + data=sine, + thickness=5, + colors="magenta" +) # you can also use colormaps for lines! -cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") +cosine_graphic = fig[0, 0].add_line( + data=cosine, + thickness=12, + cmap="autumn", + offset=(0, 3, 0) # places the graphic at a y-axis offset of 3, offsets don't affect data +) # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) +sinc_graphic = fig[0, 0].add_line( + data=sinc, + thickness=5, + colors=colors, + offset=(0, 6, 0) +) + +zeros = np.zeros(xs.size) +zeros_data = np.column_stack([xs, zeros]) +zeros_graphic = fig[0, 0].add_line( + data=zeros_data, + thickness=8, + colors="w", + offset=(0, 10, 0) +) fig.show() @@ -42,10 +65,6 @@ cosine_graphic.colors[90:] = "red" cosine_graphic.colors[60] = "w" -# indexing to assign colormaps to entire lines or segments -sinc_graphic.cmap[10:50] = "gray" -sine_graphic.cmap = "seismic" - # more complex indexing, set the blue value directly from an array cosine_graphic.colors[65:90, 0] = np.linspace(0, 1, 90-65) @@ -53,8 +72,14 @@ key = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 67, 19]) sinc_graphic.colors[key] = "Red" -#key2 = np.array([True, False, True, False, True, True, True, True]) -#cosine_graphic.colors[key2] = "Green" +# boolean fancy indexing +zeros_graphic.colors[xs < -5] = "green" + +# assign colormap to an entire line +sine_graphic.cmap = "seismic" +# or to segments of a line +zeros_graphic.cmap[50:75] = "jet" +zeros_graphic.cmap[75:] = "viridis" fig.canvas.set_logical_size(800, 800) diff --git a/examples/desktop/scatter/scatter_cmap.py b/examples/desktop/scatter/scatter_cmap.py index f1bba98c3..58c43c0ea 100644 --- a/examples/desktop/scatter/scatter_cmap.py +++ b/examples/desktop/scatter/scatter_cmap.py @@ -21,7 +21,11 @@ agg.fit_predict(data) scatter_graphic = fig[0, 0].add_scatter( - data=data[:, :-1], sizes=15, alpha=0.7, cmap="Set1", cmap_values=agg.labels_ + data=data[:, :-1], # use only xy data + sizes=15, + alpha=0.7, + cmap="Set1", + cmap_transform=agg.labels_ # use the labels as a transform to map colors from the colormap ) fig.show() diff --git a/examples/desktop/scatter/scatter_colorslice.py b/examples/desktop/scatter/scatter_colorslice.py index 43f405b06..60433b5f5 100644 --- a/examples/desktop/scatter/scatter_colorslice.py +++ b/examples/desktop/scatter/scatter_colorslice.py @@ -19,7 +19,12 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter_graphic = fig[0, 0].add_scatter( + data=data[:, :-1], + sizes=6, + alpha=0.7, + colors=colors # use colors from the list of strings +) fig.show() diff --git a/examples/desktop/scatter/scatter_present.py b/examples/desktop/scatter/scatter_present.py deleted file mode 100644 index 5da4610bd..000000000 --- a/examples/desktop/scatter/scatter_present.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Scatter Plot -============ -Example showing present feature for scatter plot. -""" - -# test_example = true - -import fastplotlib as fpl -import numpy as np -from pathlib import Path - - -fig = fpl.Figure() - -data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") -data = np.load(data_path) - -n_points = 50 -colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points - -scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) - -colors = ["red"] * n_points + ["white"] * n_points + ["blue"] * n_points -scatter_graphic2 = fig[0, 0].add_scatter(data=data[:, 1:], sizes=6, alpha=0.7, colors=colors) - -fig.show() - -fig.canvas.set_logical_size(800, 800) - -fig[0, 0].auto_scale() - -scatter_graphic.present = False - - -if __name__ == "__main__": - print(__doc__) - fpl.run() diff --git a/examples/desktop/screenshots/line_colorslice.png b/examples/desktop/screenshots/line_colorslice.png index 3d04c473f..789265530 100644 --- a/examples/desktop/screenshots/line_colorslice.png +++ b/examples/desktop/screenshots/line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa941eaf5b940b4eebab89ed836cbd092e16b4758abafa3722c296db65c0c4b5 -size 33233 +oid sha256:25e87f566a667c98b54d4acdf115d16b486e047242b9ce8b141e5724b9d0a46a +size 33191 diff --git a/examples/desktop/screenshots/line_present_scaling.png b/examples/desktop/screenshots/line_present_scaling.png deleted file mode 100644 index ba7142106..000000000 --- a/examples/desktop/screenshots/line_present_scaling.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:06f7dd45eb495fecfcf46478c6430a658640ceb2855c4797bc184cf4134571e3 -size 20180 diff --git a/examples/desktop/screenshots/scatter_present.png b/examples/desktop/screenshots/scatter_present.png deleted file mode 100644 index 08bc610b3..000000000 --- a/examples/desktop/screenshots/scatter_present.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd072918f21ed0ce4ea4e1f4499ec1ff66d867cfdc0ecd6b3ed8092141cd348e -size 14195 From 01e037179988f4f0c118e85596f045716cc70f86 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 06:21:56 -0400 Subject: [PATCH 146/196] bugfix, docstrings --- fastplotlib/graphics/_features/_base.py | 17 +++++++++++------ .../graphics/_features/_positions_graphics.py | 7 ++++--- tests/test_positions_graphics.py | 10 ++++++---- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 17ad8a1ea..baa079779 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -76,9 +76,11 @@ def __init__(self, **kwargs): @property def value(self) -> Any: + """Graphic Feature value, must be implemented in subclass""" raise NotImplemented def set_value(self, graphic, value: float): + """Graphic Feature value setter, must be implemented in subclass""" raise NotImplementedError def block_events(self, val: bool): @@ -97,10 +99,10 @@ def add_event_handler(self, handler: callable): """ Add an event handler. All added event handlers are called when this feature changes. - The ``handler`` can optionally accept a :class:`.FeatureEvent` as the first and only argument. - The ``FeatureEvent`` only has 2 attributes, ``type`` which denotes the type of event - as a ``str`` in the form of "", such as "color". And ``pick_info`` which contains - information about the event and Graphic that triggered it. + Used by `Graphic` classes to add to their event handlers, not meant for users. Users + add handlers to Graphic instances only. + + The ``handler`` must accept a :class:`.FeatureEvent` as the first and only argument. Parameters ---------- @@ -174,6 +176,7 @@ def __init__( elif buffer_type == "buffer": self._buffer = pygfx.Buffer(bdata) elif buffer_type == "texture": + # TODO: placeholder, not currently used since TextureArray is used specifically for Image graphics self._buffer = pygfx.Texture(bdata, dim=texture_dim) else: raise ValueError( @@ -185,7 +188,8 @@ def __init__( self._shared: int = 0 @property - def value(self) -> NDArray: + def value(self) -> np.ndarray: + """numpy array object representing the data managed by this buffer""" return self.buffer.data def set_value(self, graphic, value): @@ -194,6 +198,7 @@ def set_value(self, graphic, value): @property def buffer(self) -> pygfx.Buffer | pygfx.Texture: + """managed buffer""" return self._buffer @property @@ -220,7 +225,7 @@ def _parse_offset_size( upper_bound: int, ): """ - parse offset and size for one dimension + parse offset and size for first, i.e. n_datapoints, dimension """ if isinstance(key, int): # simplest case diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index a2db83906..e109e7bab 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -136,6 +136,7 @@ def __len__(self): return len(self.buffer.data) +# Manages uniform color for line or scatter material class UniformColor(GraphicFeature): def __init__( self, value: str | np.ndarray | tuple | list | pygfx.Color, alpha: float = 1.0 @@ -157,6 +158,7 @@ def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Colo self._call_event_handlers(event) +# manages uniform size for scatter material class UniformSize(GraphicFeature): def __init__(self, value: int | float): self._value = float(value) @@ -166,9 +168,8 @@ def __init__(self, value: int | float): def value(self) -> float: return self._value - def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Color): - value = pygfx.Color(value) - graphic.world_object.material.size = value + def set_value(self, graphic, value: float | int): + graphic.world_object.material.size = float(value) self._value = value event = FeatureEvent(type="sizes", info={"value": value}) diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index 68f8cbbe9..d9c3a4871 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -71,10 +71,6 @@ def test_sizes_slice(): pass -def test_change_thickness(): - pass - - @pytest.mark.parametrize("graphic_type", ["line", "scatter"]) @pytest.mark.parametrize("colors", [None, *generate_color_inputs("b")]) @pytest.mark.parametrize("uniform_color", [True, False]) @@ -414,6 +410,12 @@ def test_uniform_size(sizes, uniform_size): npt.assert_almost_equal(graphic.sizes, sizes) npt.assert_almost_equal(graphic.world_object.material.size, sizes) + # test changing size + graphic.sizes = 10.0 + assert isinstance(graphic.sizes, float) + assert isinstance(graphic._sizes, UniformSize) + assert graphic.sizes == 10.0 + @pytest.mark.parametrize("thickness", [None, 0.5, 5.0]) def test_thickness(thickness): From 6ad1802cbf298c5796ea14002656e29654c7e87f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 06:39:50 -0400 Subject: [PATCH 147/196] docstrings, exception messages --- fastplotlib/graphics/_features/_base.py | 9 ++++++--- .../graphics/_features/_positions_graphics.py | 14 +++++++++++--- fastplotlib/graphics/_positions_base.py | 4 ++-- fastplotlib/graphics/image.py | 12 ------------ fastplotlib/graphics/line.py | 15 ++++++++------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index baa079779..9b256b697 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -260,7 +260,7 @@ def _parse_offset_size( key = np.array(key) if not key.ndim == 1: - raise TypeError(key) + raise TypeError(f"can only use 1D arrays for fancy indexing, you have passed a data with: {key.ndim} dimensions") if key.dtype == bool: # convert bool mask to integer indices @@ -268,7 +268,7 @@ def _parse_offset_size( if not np.issubdtype(key.dtype, np.integer): # fancy indexing doesn't make sense with non-integer types - raise TypeError(key) + raise TypeError(f"can only using integer or booleans arrays for fancy indexing, your array is of type: {key.dtype}") if key.size < 1: # nothing to update @@ -286,7 +286,10 @@ def _parse_offset_size( size = np.ptp(key) + 1 else: - raise TypeError(key) + raise TypeError( + f"invalid key for indexing buffer: {key}\n" + f"valid ways to index buffers are using integers, slices, or fancy indexing with integers or bool" + ) return offset, size diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index e109e7bab..920995fd7 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -100,7 +100,10 @@ def __setitem__( if key.dtype == bool: # make sure len is same if not key.size == self.buffer.data.shape[0]: - raise IndexError + raise IndexError( + f"Length of array for fancy indexing must match number of datapoints.\n" + f"There are {len(self.buffer.data.shape[0])} datapoints and you have passed {key.size} indices" + ) n_colors = np.count_nonzero(key) elif np.issubdtype(key.dtype, np.integer): @@ -114,7 +117,10 @@ def __setitem__( value = parse_colors(user_value, n_colors) else: - raise TypeError + raise TypeError( + f"invalid key for setting colors, you may set colors using integer indices, slices, or " + f"fancy indexing using an array of integers or bool" + ) self.buffer.data[key] = value @@ -347,7 +353,9 @@ def __init__( if self._cmap_name is not None: if not isinstance(self._cmap_name, str): - raise TypeError + raise TypeError( + f"cmap name must be of type , you have passed: {self._cmap_name} of type: {type(self._cmap_name)}" + ) if self._transform is not None: self._transform = np.asarray(self._transform) diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index a02201139..723bd6c8a 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -44,7 +44,7 @@ def colors(self, value: str | np.ndarray | tuple[float] | list[float] | list[str @property def cmap(self) -> VertexCmap: - """Control cmap""" + """Control the cmap, cmap transform, or cmap alpha""" return self._cmap @cmap.setter @@ -100,7 +100,7 @@ def __init__( self._cmap = cmap self._colors = cmap._vertex_colors else: - raise TypeError + raise TypeError("`cmap` argument must be a cmap name or an existing `VertexCmap` instance") else: # no cmap given if isinstance(colors, VertexColors): diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index c5829e993..d6576c12d 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -116,18 +116,6 @@ def __init__( kwargs: additional keyword arguments passed to Graphic - Features - -------- - - **data**: :class:`.HeatmapDataFeature` - Manages the data buffer displayed in the HeatmapGraphic - - **cmap**: :class:`.HeatmapCmapFeature` - Manages the colormap - - **present**: :class:`.PresentFeature` - Control the presence of the Graphic in the scene - """ super().__init__(**kwargs) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 8c9ad2072..7393ac91f 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -40,6 +40,13 @@ def __init__( 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" @@ -47,12 +54,6 @@ def __init__( cmap_transform: 1D array-like of numerical values, optional if provided, these values are used to map the colors from the cmap - alpha: float, optional, default 1.0 - alpha value for the colors - - z_position: float, optional - z-axis position for placing the graphic - **kwargs passed to Graphic @@ -98,7 +99,7 @@ def __init__( @property def thickness(self) -> float: - """Graphic name""" + """line thickness""" return self._thickness.value @thickness.setter From 44fc96101b1a217d59fb3b84e3bffabbc2aa8b04 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 06:47:09 -0400 Subject: [PATCH 148/196] update api docs --- .../api/graphic_features/CmapFeature.rst | 36 --------------- .../api/graphic_features/ColorFeature.rst | 34 -------------- docs/source/api/graphic_features/Deleted.rst | 2 + .../api/graphic_features/FeatureEvent.rst | 29 ------------ docs/source/api/graphic_features/FontSize.rst | 35 +++++++++++++++ .../api/graphic_features/GraphicFeature.rst | 33 -------------- .../GraphicFeatureIndexable.rst | 34 -------------- .../graphic_features/HeatmapCmapFeature.rst | 37 ---------------- .../graphic_features/HeatmapDataFeature.rst | 35 --------------- .../source/api/graphic_features/ImageCmap.rst | 35 +++++++++++++++ .../api/graphic_features/ImageCmapFeature.rst | 37 ---------------- .../ImageCmapInterpolation.rst | 35 +++++++++++++++ .../api/graphic_features/ImageDataFeature.rst | 35 --------------- .../graphic_features/ImageInterpolation.rst | 35 +++++++++++++++ .../source/api/graphic_features/ImageVmax.rst | 35 +++++++++++++++ .../source/api/graphic_features/ImageVmin.rst | 35 +++++++++++++++ .../LinearRegionSelectionFeature.rst | 2 + .../LinearSelectionFeature.rst | 2 + docs/source/api/graphic_features/Name.rst | 35 +++++++++++++++ docs/source/api/graphic_features/Offset.rst | 35 +++++++++++++++ .../graphic_features/PointsDataFeature.rst | 34 -------------- .../graphic_features/PointsSizesFeature.rst | 3 ++ .../api/graphic_features/PresentFeature.rst | 33 -------------- docs/source/api/graphic_features/Rotation.rst | 35 +++++++++++++++ docs/source/api/graphic_features/TextData.rst | 35 +++++++++++++++ .../api/graphic_features/TextFaceColor.rst | 35 +++++++++++++++ .../api/graphic_features/TextOutlineColor.rst | 35 +++++++++++++++ .../graphic_features/TextOutlineThickness.rst | 35 +++++++++++++++ .../api/graphic_features/TextureArray.rst | 39 ++++++++++++++++ .../source/api/graphic_features/Thickness.rst | 35 +++++++++++++++ .../api/graphic_features/ThicknessFeature.rst | 33 -------------- .../api/graphic_features/UniformColor.rst | 35 +++++++++++++++ .../api/graphic_features/UniformSize.rst | 35 +++++++++++++++ .../api/graphic_features/VertexCmap.rst | 40 +++++++++++++++++ .../api/graphic_features/VertexColors.rst | 37 ++++++++++++++++ .../api/graphic_features/VertexPositions.rst | 37 ++++++++++++++++ docs/source/api/graphic_features/Visible.rst | 35 +++++++++++++++ docs/source/api/graphic_features/index.rst | 34 ++++++++------ .../to_gpu_supported_dtype.rst | 29 ------------ docs/source/api/graphics/HeatmapGraphic.rst | 44 ------------------- docs/source/api/graphics/ImageGraphic.rst | 24 ++++++---- docs/source/api/graphics/LineCollection.rst | 22 ++++++---- docs/source/api/graphics/LineGraphic.rst | 21 +++++---- docs/source/api/graphics/LineStack.rst | 22 ++++++---- docs/source/api/graphics/ScatterGraphic.rst | 18 +++++--- docs/source/api/graphics/TextGraphic.rst | 18 +++++--- docs/source/api/graphics/index.rst | 5 +-- docs/source/api/layouts/subplot.rst | 1 - .../api/selectors/LinearRegionSelector.rst | 17 ++++--- docs/source/api/selectors/LinearSelector.rst | 17 ++++--- docs/source/api/selectors/PolygonSelector.rst | 43 ------------------ docs/source/api/selectors/Synchronizer.rst | 33 -------------- docs/source/api/selectors/index.rst | 2 - 53 files changed, 883 insertions(+), 634 deletions(-) delete mode 100644 docs/source/api/graphic_features/CmapFeature.rst delete mode 100644 docs/source/api/graphic_features/ColorFeature.rst delete mode 100644 docs/source/api/graphic_features/FeatureEvent.rst create mode 100644 docs/source/api/graphic_features/FontSize.rst delete mode 100644 docs/source/api/graphic_features/GraphicFeature.rst delete mode 100644 docs/source/api/graphic_features/GraphicFeatureIndexable.rst delete mode 100644 docs/source/api/graphic_features/HeatmapCmapFeature.rst delete mode 100644 docs/source/api/graphic_features/HeatmapDataFeature.rst create mode 100644 docs/source/api/graphic_features/ImageCmap.rst delete mode 100644 docs/source/api/graphic_features/ImageCmapFeature.rst create mode 100644 docs/source/api/graphic_features/ImageCmapInterpolation.rst delete mode 100644 docs/source/api/graphic_features/ImageDataFeature.rst create mode 100644 docs/source/api/graphic_features/ImageInterpolation.rst create mode 100644 docs/source/api/graphic_features/ImageVmax.rst create mode 100644 docs/source/api/graphic_features/ImageVmin.rst create mode 100644 docs/source/api/graphic_features/Name.rst create mode 100644 docs/source/api/graphic_features/Offset.rst delete mode 100644 docs/source/api/graphic_features/PointsDataFeature.rst delete mode 100644 docs/source/api/graphic_features/PresentFeature.rst create mode 100644 docs/source/api/graphic_features/Rotation.rst create mode 100644 docs/source/api/graphic_features/TextData.rst create mode 100644 docs/source/api/graphic_features/TextFaceColor.rst create mode 100644 docs/source/api/graphic_features/TextOutlineColor.rst create mode 100644 docs/source/api/graphic_features/TextOutlineThickness.rst create mode 100644 docs/source/api/graphic_features/TextureArray.rst create mode 100644 docs/source/api/graphic_features/Thickness.rst delete mode 100644 docs/source/api/graphic_features/ThicknessFeature.rst create mode 100644 docs/source/api/graphic_features/UniformColor.rst create mode 100644 docs/source/api/graphic_features/UniformSize.rst create mode 100644 docs/source/api/graphic_features/VertexCmap.rst create mode 100644 docs/source/api/graphic_features/VertexColors.rst create mode 100644 docs/source/api/graphic_features/VertexPositions.rst create mode 100644 docs/source/api/graphic_features/Visible.rst delete mode 100644 docs/source/api/graphic_features/to_gpu_supported_dtype.rst delete mode 100644 docs/source/api/graphics/HeatmapGraphic.rst delete mode 100644 docs/source/api/selectors/PolygonSelector.rst delete mode 100644 docs/source/api/selectors/Synchronizer.rst diff --git a/docs/source/api/graphic_features/CmapFeature.rst b/docs/source/api/graphic_features/CmapFeature.rst deleted file mode 100644 index 7cc2f681f..000000000 --- a/docs/source/api/graphic_features/CmapFeature.rst +++ /dev/null @@ -1,36 +0,0 @@ -.. _api.CmapFeature: - -CmapFeature -*********** - -=========== -CmapFeature -=========== -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: CmapFeature_api - - CmapFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: CmapFeature_api - - CmapFeature.buffer - CmapFeature.name - CmapFeature.values - -Methods -~~~~~~~ -.. autosummary:: - :toctree: CmapFeature_api - - CmapFeature.add_event_handler - CmapFeature.block_events - CmapFeature.clear_event_handlers - CmapFeature.remove_event_handler - diff --git a/docs/source/api/graphic_features/ColorFeature.rst b/docs/source/api/graphic_features/ColorFeature.rst deleted file mode 100644 index 3ed84cd70..000000000 --- a/docs/source/api/graphic_features/ColorFeature.rst +++ /dev/null @@ -1,34 +0,0 @@ -.. _api.ColorFeature: - -ColorFeature -************ - -============ -ColorFeature -============ -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: ColorFeature_api - - ColorFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: ColorFeature_api - - ColorFeature.buffer - -Methods -~~~~~~~ -.. autosummary:: - :toctree: ColorFeature_api - - ColorFeature.add_event_handler - ColorFeature.block_events - ColorFeature.clear_event_handlers - ColorFeature.remove_event_handler - diff --git a/docs/source/api/graphic_features/Deleted.rst b/docs/source/api/graphic_features/Deleted.rst index 998e94588..09131c4a7 100644 --- a/docs/source/api/graphic_features/Deleted.rst +++ b/docs/source/api/graphic_features/Deleted.rst @@ -20,6 +20,7 @@ Properties .. autosummary:: :toctree: Deleted_api + Deleted.value Methods ~~~~~~~ @@ -30,4 +31,5 @@ Methods Deleted.block_events Deleted.clear_event_handlers Deleted.remove_event_handler + Deleted.set_value diff --git a/docs/source/api/graphic_features/FeatureEvent.rst b/docs/source/api/graphic_features/FeatureEvent.rst deleted file mode 100644 index f22ee3ef4..000000000 --- a/docs/source/api/graphic_features/FeatureEvent.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. _api.FeatureEvent: - -FeatureEvent -************ - -============ -FeatureEvent -============ -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: FeatureEvent_api - - FeatureEvent - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: FeatureEvent_api - - -Methods -~~~~~~~ -.. autosummary:: - :toctree: FeatureEvent_api - - diff --git a/docs/source/api/graphic_features/FontSize.rst b/docs/source/api/graphic_features/FontSize.rst new file mode 100644 index 000000000..4b8df9826 --- /dev/null +++ b/docs/source/api/graphic_features/FontSize.rst @@ -0,0 +1,35 @@ +.. _api.FontSize: + +FontSize +******** + +======== +FontSize +======== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: FontSize_api + + FontSize + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: FontSize_api + + FontSize.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: FontSize_api + + FontSize.add_event_handler + FontSize.block_events + FontSize.clear_event_handlers + FontSize.remove_event_handler + FontSize.set_value + diff --git a/docs/source/api/graphic_features/GraphicFeature.rst b/docs/source/api/graphic_features/GraphicFeature.rst deleted file mode 100644 index 7abc3e6b2..000000000 --- a/docs/source/api/graphic_features/GraphicFeature.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. _api.GraphicFeature: - -GraphicFeature -************** - -============== -GraphicFeature -============== -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: GraphicFeature_api - - GraphicFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: GraphicFeature_api - - -Methods -~~~~~~~ -.. autosummary:: - :toctree: GraphicFeature_api - - GraphicFeature.add_event_handler - GraphicFeature.block_events - GraphicFeature.clear_event_handlers - GraphicFeature.remove_event_handler - diff --git a/docs/source/api/graphic_features/GraphicFeatureIndexable.rst b/docs/source/api/graphic_features/GraphicFeatureIndexable.rst deleted file mode 100644 index 7bd1383bc..000000000 --- a/docs/source/api/graphic_features/GraphicFeatureIndexable.rst +++ /dev/null @@ -1,34 +0,0 @@ -.. _api.GraphicFeatureIndexable: - -GraphicFeatureIndexable -*********************** - -======================= -GraphicFeatureIndexable -======================= -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: GraphicFeatureIndexable_api - - GraphicFeatureIndexable - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: GraphicFeatureIndexable_api - - GraphicFeatureIndexable.buffer - -Methods -~~~~~~~ -.. autosummary:: - :toctree: GraphicFeatureIndexable_api - - GraphicFeatureIndexable.add_event_handler - GraphicFeatureIndexable.block_events - GraphicFeatureIndexable.clear_event_handlers - GraphicFeatureIndexable.remove_event_handler - diff --git a/docs/source/api/graphic_features/HeatmapCmapFeature.rst b/docs/source/api/graphic_features/HeatmapCmapFeature.rst deleted file mode 100644 index bac43c9b9..000000000 --- a/docs/source/api/graphic_features/HeatmapCmapFeature.rst +++ /dev/null @@ -1,37 +0,0 @@ -.. _api.HeatmapCmapFeature: - -HeatmapCmapFeature -****************** - -================== -HeatmapCmapFeature -================== -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: HeatmapCmapFeature_api - - HeatmapCmapFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: HeatmapCmapFeature_api - - HeatmapCmapFeature.name - HeatmapCmapFeature.vmax - HeatmapCmapFeature.vmin - -Methods -~~~~~~~ -.. autosummary:: - :toctree: HeatmapCmapFeature_api - - HeatmapCmapFeature.add_event_handler - HeatmapCmapFeature.block_events - HeatmapCmapFeature.clear_event_handlers - HeatmapCmapFeature.remove_event_handler - HeatmapCmapFeature.reset_vmin_vmax - diff --git a/docs/source/api/graphic_features/HeatmapDataFeature.rst b/docs/source/api/graphic_features/HeatmapDataFeature.rst deleted file mode 100644 index 029f0e199..000000000 --- a/docs/source/api/graphic_features/HeatmapDataFeature.rst +++ /dev/null @@ -1,35 +0,0 @@ -.. _api.HeatmapDataFeature: - -HeatmapDataFeature -****************** - -================== -HeatmapDataFeature -================== -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: HeatmapDataFeature_api - - HeatmapDataFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: HeatmapDataFeature_api - - HeatmapDataFeature.buffer - -Methods -~~~~~~~ -.. autosummary:: - :toctree: HeatmapDataFeature_api - - HeatmapDataFeature.add_event_handler - HeatmapDataFeature.block_events - HeatmapDataFeature.clear_event_handlers - HeatmapDataFeature.remove_event_handler - HeatmapDataFeature.update_gpu - diff --git a/docs/source/api/graphic_features/ImageCmap.rst b/docs/source/api/graphic_features/ImageCmap.rst new file mode 100644 index 000000000..23d16a4a2 --- /dev/null +++ b/docs/source/api/graphic_features/ImageCmap.rst @@ -0,0 +1,35 @@ +.. _api.ImageCmap: + +ImageCmap +********* + +========= +ImageCmap +========= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageCmap_api + + ImageCmap + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageCmap_api + + ImageCmap.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageCmap_api + + ImageCmap.add_event_handler + ImageCmap.block_events + ImageCmap.clear_event_handlers + ImageCmap.remove_event_handler + ImageCmap.set_value + diff --git a/docs/source/api/graphic_features/ImageCmapFeature.rst b/docs/source/api/graphic_features/ImageCmapFeature.rst deleted file mode 100644 index ae65744c7..000000000 --- a/docs/source/api/graphic_features/ImageCmapFeature.rst +++ /dev/null @@ -1,37 +0,0 @@ -.. _api.ImageCmapFeature: - -ImageCmapFeature -**************** - -================ -ImageCmapFeature -================ -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: ImageCmapFeature_api - - ImageCmapFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: ImageCmapFeature_api - - ImageCmapFeature.name - ImageCmapFeature.vmax - ImageCmapFeature.vmin - -Methods -~~~~~~~ -.. autosummary:: - :toctree: ImageCmapFeature_api - - ImageCmapFeature.add_event_handler - ImageCmapFeature.block_events - ImageCmapFeature.clear_event_handlers - ImageCmapFeature.remove_event_handler - ImageCmapFeature.reset_vmin_vmax - diff --git a/docs/source/api/graphic_features/ImageCmapInterpolation.rst b/docs/source/api/graphic_features/ImageCmapInterpolation.rst new file mode 100644 index 000000000..7e04ec788 --- /dev/null +++ b/docs/source/api/graphic_features/ImageCmapInterpolation.rst @@ -0,0 +1,35 @@ +.. _api.ImageCmapInterpolation: + +ImageCmapInterpolation +********************** + +====================== +ImageCmapInterpolation +====================== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageCmapInterpolation_api + + ImageCmapInterpolation + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageCmapInterpolation_api + + ImageCmapInterpolation.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageCmapInterpolation_api + + ImageCmapInterpolation.add_event_handler + ImageCmapInterpolation.block_events + ImageCmapInterpolation.clear_event_handlers + ImageCmapInterpolation.remove_event_handler + ImageCmapInterpolation.set_value + diff --git a/docs/source/api/graphic_features/ImageDataFeature.rst b/docs/source/api/graphic_features/ImageDataFeature.rst deleted file mode 100644 index 35fe74cf7..000000000 --- a/docs/source/api/graphic_features/ImageDataFeature.rst +++ /dev/null @@ -1,35 +0,0 @@ -.. _api.ImageDataFeature: - -ImageDataFeature -**************** - -================ -ImageDataFeature -================ -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: ImageDataFeature_api - - ImageDataFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: ImageDataFeature_api - - ImageDataFeature.buffer - -Methods -~~~~~~~ -.. autosummary:: - :toctree: ImageDataFeature_api - - ImageDataFeature.add_event_handler - ImageDataFeature.block_events - ImageDataFeature.clear_event_handlers - ImageDataFeature.remove_event_handler - ImageDataFeature.update_gpu - diff --git a/docs/source/api/graphic_features/ImageInterpolation.rst b/docs/source/api/graphic_features/ImageInterpolation.rst new file mode 100644 index 000000000..866e76333 --- /dev/null +++ b/docs/source/api/graphic_features/ImageInterpolation.rst @@ -0,0 +1,35 @@ +.. _api.ImageInterpolation: + +ImageInterpolation +****************** + +================== +ImageInterpolation +================== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageInterpolation_api + + ImageInterpolation + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageInterpolation_api + + ImageInterpolation.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageInterpolation_api + + ImageInterpolation.add_event_handler + ImageInterpolation.block_events + ImageInterpolation.clear_event_handlers + ImageInterpolation.remove_event_handler + ImageInterpolation.set_value + diff --git a/docs/source/api/graphic_features/ImageVmax.rst b/docs/source/api/graphic_features/ImageVmax.rst new file mode 100644 index 000000000..b7dfe7e2d --- /dev/null +++ b/docs/source/api/graphic_features/ImageVmax.rst @@ -0,0 +1,35 @@ +.. _api.ImageVmax: + +ImageVmax +********* + +========= +ImageVmax +========= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageVmax_api + + ImageVmax + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageVmax_api + + ImageVmax.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageVmax_api + + ImageVmax.add_event_handler + ImageVmax.block_events + ImageVmax.clear_event_handlers + ImageVmax.remove_event_handler + ImageVmax.set_value + diff --git a/docs/source/api/graphic_features/ImageVmin.rst b/docs/source/api/graphic_features/ImageVmin.rst new file mode 100644 index 000000000..0d4634894 --- /dev/null +++ b/docs/source/api/graphic_features/ImageVmin.rst @@ -0,0 +1,35 @@ +.. _api.ImageVmin: + +ImageVmin +********* + +========= +ImageVmin +========= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageVmin_api + + ImageVmin + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageVmin_api + + ImageVmin.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageVmin_api + + ImageVmin.add_event_handler + ImageVmin.block_events + ImageVmin.clear_event_handlers + ImageVmin.remove_event_handler + ImageVmin.set_value + diff --git a/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst b/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst index a15825530..b8958c86b 100644 --- a/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst +++ b/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst @@ -21,6 +21,7 @@ Properties :toctree: LinearRegionSelectionFeature_api LinearRegionSelectionFeature.axis + LinearRegionSelectionFeature.value Methods ~~~~~~~ @@ -31,4 +32,5 @@ Methods LinearRegionSelectionFeature.block_events LinearRegionSelectionFeature.clear_event_handlers LinearRegionSelectionFeature.remove_event_handler + LinearRegionSelectionFeature.set_value diff --git a/docs/source/api/graphic_features/LinearSelectionFeature.rst b/docs/source/api/graphic_features/LinearSelectionFeature.rst index aeb1ca66b..ad7b8645a 100644 --- a/docs/source/api/graphic_features/LinearSelectionFeature.rst +++ b/docs/source/api/graphic_features/LinearSelectionFeature.rst @@ -20,6 +20,7 @@ Properties .. autosummary:: :toctree: LinearSelectionFeature_api + LinearSelectionFeature.value Methods ~~~~~~~ @@ -30,4 +31,5 @@ Methods LinearSelectionFeature.block_events LinearSelectionFeature.clear_event_handlers LinearSelectionFeature.remove_event_handler + LinearSelectionFeature.set_value diff --git a/docs/source/api/graphic_features/Name.rst b/docs/source/api/graphic_features/Name.rst new file mode 100644 index 000000000..288fcfc22 --- /dev/null +++ b/docs/source/api/graphic_features/Name.rst @@ -0,0 +1,35 @@ +.. _api.Name: + +Name +**** + +==== +Name +==== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Name_api + + Name + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Name_api + + Name.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Name_api + + Name.add_event_handler + Name.block_events + Name.clear_event_handlers + Name.remove_event_handler + Name.set_value + diff --git a/docs/source/api/graphic_features/Offset.rst b/docs/source/api/graphic_features/Offset.rst new file mode 100644 index 000000000..683aaf763 --- /dev/null +++ b/docs/source/api/graphic_features/Offset.rst @@ -0,0 +1,35 @@ +.. _api.Offset: + +Offset +****** + +====== +Offset +====== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Offset_api + + Offset + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Offset_api + + Offset.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Offset_api + + Offset.add_event_handler + Offset.block_events + Offset.clear_event_handlers + Offset.remove_event_handler + Offset.set_value + diff --git a/docs/source/api/graphic_features/PointsDataFeature.rst b/docs/source/api/graphic_features/PointsDataFeature.rst deleted file mode 100644 index 078b1c535..000000000 --- a/docs/source/api/graphic_features/PointsDataFeature.rst +++ /dev/null @@ -1,34 +0,0 @@ -.. _api.PointsDataFeature: - -PointsDataFeature -***************** - -================= -PointsDataFeature -================= -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: PointsDataFeature_api - - PointsDataFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: PointsDataFeature_api - - PointsDataFeature.buffer - -Methods -~~~~~~~ -.. autosummary:: - :toctree: PointsDataFeature_api - - PointsDataFeature.add_event_handler - PointsDataFeature.block_events - PointsDataFeature.clear_event_handlers - PointsDataFeature.remove_event_handler - diff --git a/docs/source/api/graphic_features/PointsSizesFeature.rst b/docs/source/api/graphic_features/PointsSizesFeature.rst index 7915cb09d..3dcc4eeb2 100644 --- a/docs/source/api/graphic_features/PointsSizesFeature.rst +++ b/docs/source/api/graphic_features/PointsSizesFeature.rst @@ -21,6 +21,8 @@ Properties :toctree: PointsSizesFeature_api PointsSizesFeature.buffer + PointsSizesFeature.shared + PointsSizesFeature.value Methods ~~~~~~~ @@ -31,4 +33,5 @@ Methods PointsSizesFeature.block_events PointsSizesFeature.clear_event_handlers PointsSizesFeature.remove_event_handler + PointsSizesFeature.set_value diff --git a/docs/source/api/graphic_features/PresentFeature.rst b/docs/source/api/graphic_features/PresentFeature.rst deleted file mode 100644 index 1ddbf1ec4..000000000 --- a/docs/source/api/graphic_features/PresentFeature.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. _api.PresentFeature: - -PresentFeature -************** - -============== -PresentFeature -============== -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: PresentFeature_api - - PresentFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: PresentFeature_api - - -Methods -~~~~~~~ -.. autosummary:: - :toctree: PresentFeature_api - - PresentFeature.add_event_handler - PresentFeature.block_events - PresentFeature.clear_event_handlers - PresentFeature.remove_event_handler - diff --git a/docs/source/api/graphic_features/Rotation.rst b/docs/source/api/graphic_features/Rotation.rst new file mode 100644 index 000000000..f8963b0fd --- /dev/null +++ b/docs/source/api/graphic_features/Rotation.rst @@ -0,0 +1,35 @@ +.. _api.Rotation: + +Rotation +******** + +======== +Rotation +======== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Rotation_api + + Rotation + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Rotation_api + + Rotation.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Rotation_api + + Rotation.add_event_handler + Rotation.block_events + Rotation.clear_event_handlers + Rotation.remove_event_handler + Rotation.set_value + diff --git a/docs/source/api/graphic_features/TextData.rst b/docs/source/api/graphic_features/TextData.rst new file mode 100644 index 000000000..1c27b6e48 --- /dev/null +++ b/docs/source/api/graphic_features/TextData.rst @@ -0,0 +1,35 @@ +.. _api.TextData: + +TextData +******** + +======== +TextData +======== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextData_api + + TextData + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextData_api + + TextData.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextData_api + + TextData.add_event_handler + TextData.block_events + TextData.clear_event_handlers + TextData.remove_event_handler + TextData.set_value + diff --git a/docs/source/api/graphic_features/TextFaceColor.rst b/docs/source/api/graphic_features/TextFaceColor.rst new file mode 100644 index 000000000..5dae54192 --- /dev/null +++ b/docs/source/api/graphic_features/TextFaceColor.rst @@ -0,0 +1,35 @@ +.. _api.TextFaceColor: + +TextFaceColor +************* + +============= +TextFaceColor +============= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextFaceColor_api + + TextFaceColor + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextFaceColor_api + + TextFaceColor.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextFaceColor_api + + TextFaceColor.add_event_handler + TextFaceColor.block_events + TextFaceColor.clear_event_handlers + TextFaceColor.remove_event_handler + TextFaceColor.set_value + diff --git a/docs/source/api/graphic_features/TextOutlineColor.rst b/docs/source/api/graphic_features/TextOutlineColor.rst new file mode 100644 index 000000000..f7831b0df --- /dev/null +++ b/docs/source/api/graphic_features/TextOutlineColor.rst @@ -0,0 +1,35 @@ +.. _api.TextOutlineColor: + +TextOutlineColor +**************** + +================ +TextOutlineColor +================ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextOutlineColor_api + + TextOutlineColor + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextOutlineColor_api + + TextOutlineColor.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextOutlineColor_api + + TextOutlineColor.add_event_handler + TextOutlineColor.block_events + TextOutlineColor.clear_event_handlers + TextOutlineColor.remove_event_handler + TextOutlineColor.set_value + diff --git a/docs/source/api/graphic_features/TextOutlineThickness.rst b/docs/source/api/graphic_features/TextOutlineThickness.rst new file mode 100644 index 000000000..75d485781 --- /dev/null +++ b/docs/source/api/graphic_features/TextOutlineThickness.rst @@ -0,0 +1,35 @@ +.. _api.TextOutlineThickness: + +TextOutlineThickness +******************** + +==================== +TextOutlineThickness +==================== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextOutlineThickness_api + + TextOutlineThickness + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextOutlineThickness_api + + TextOutlineThickness.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextOutlineThickness_api + + TextOutlineThickness.add_event_handler + TextOutlineThickness.block_events + TextOutlineThickness.clear_event_handlers + TextOutlineThickness.remove_event_handler + TextOutlineThickness.set_value + diff --git a/docs/source/api/graphic_features/TextureArray.rst b/docs/source/api/graphic_features/TextureArray.rst new file mode 100644 index 000000000..79707c453 --- /dev/null +++ b/docs/source/api/graphic_features/TextureArray.rst @@ -0,0 +1,39 @@ +.. _api.TextureArray: + +TextureArray +************ + +============ +TextureArray +============ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextureArray_api + + TextureArray + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextureArray_api + + TextureArray.buffer + TextureArray.col_indices + TextureArray.row_indices + TextureArray.shared + TextureArray.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextureArray_api + + TextureArray.add_event_handler + TextureArray.block_events + TextureArray.clear_event_handlers + TextureArray.remove_event_handler + TextureArray.set_value + diff --git a/docs/source/api/graphic_features/Thickness.rst b/docs/source/api/graphic_features/Thickness.rst new file mode 100644 index 000000000..061f96fe8 --- /dev/null +++ b/docs/source/api/graphic_features/Thickness.rst @@ -0,0 +1,35 @@ +.. _api.Thickness: + +Thickness +********* + +========= +Thickness +========= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Thickness_api + + Thickness + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Thickness_api + + Thickness.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Thickness_api + + Thickness.add_event_handler + Thickness.block_events + Thickness.clear_event_handlers + Thickness.remove_event_handler + Thickness.set_value + diff --git a/docs/source/api/graphic_features/ThicknessFeature.rst b/docs/source/api/graphic_features/ThicknessFeature.rst deleted file mode 100644 index 80219a2cd..000000000 --- a/docs/source/api/graphic_features/ThicknessFeature.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. _api.ThicknessFeature: - -ThicknessFeature -**************** - -================ -ThicknessFeature -================ -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: ThicknessFeature_api - - ThicknessFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: ThicknessFeature_api - - -Methods -~~~~~~~ -.. autosummary:: - :toctree: ThicknessFeature_api - - ThicknessFeature.add_event_handler - ThicknessFeature.block_events - ThicknessFeature.clear_event_handlers - ThicknessFeature.remove_event_handler - diff --git a/docs/source/api/graphic_features/UniformColor.rst b/docs/source/api/graphic_features/UniformColor.rst new file mode 100644 index 000000000..7370589b7 --- /dev/null +++ b/docs/source/api/graphic_features/UniformColor.rst @@ -0,0 +1,35 @@ +.. _api.UniformColor: + +UniformColor +************ + +============ +UniformColor +============ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: UniformColor_api + + UniformColor + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: UniformColor_api + + UniformColor.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: UniformColor_api + + UniformColor.add_event_handler + UniformColor.block_events + UniformColor.clear_event_handlers + UniformColor.remove_event_handler + UniformColor.set_value + diff --git a/docs/source/api/graphic_features/UniformSize.rst b/docs/source/api/graphic_features/UniformSize.rst new file mode 100644 index 000000000..e342d6a70 --- /dev/null +++ b/docs/source/api/graphic_features/UniformSize.rst @@ -0,0 +1,35 @@ +.. _api.UniformSize: + +UniformSize +*********** + +=========== +UniformSize +=========== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: UniformSize_api + + UniformSize + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: UniformSize_api + + UniformSize.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: UniformSize_api + + UniformSize.add_event_handler + UniformSize.block_events + UniformSize.clear_event_handlers + UniformSize.remove_event_handler + UniformSize.set_value + diff --git a/docs/source/api/graphic_features/VertexCmap.rst b/docs/source/api/graphic_features/VertexCmap.rst new file mode 100644 index 000000000..a3311d6e6 --- /dev/null +++ b/docs/source/api/graphic_features/VertexCmap.rst @@ -0,0 +1,40 @@ +.. _api.VertexCmap: + +VertexCmap +********** + +========== +VertexCmap +========== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VertexCmap_api + + VertexCmap + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VertexCmap_api + + VertexCmap.alpha + VertexCmap.buffer + VertexCmap.name + VertexCmap.shared + VertexCmap.transform + VertexCmap.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VertexCmap_api + + VertexCmap.add_event_handler + VertexCmap.block_events + VertexCmap.clear_event_handlers + VertexCmap.remove_event_handler + VertexCmap.set_value + diff --git a/docs/source/api/graphic_features/VertexColors.rst b/docs/source/api/graphic_features/VertexColors.rst new file mode 100644 index 000000000..3c2089a78 --- /dev/null +++ b/docs/source/api/graphic_features/VertexColors.rst @@ -0,0 +1,37 @@ +.. _api.VertexColors: + +VertexColors +************ + +============ +VertexColors +============ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VertexColors_api + + VertexColors + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VertexColors_api + + VertexColors.buffer + VertexColors.shared + VertexColors.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VertexColors_api + + VertexColors.add_event_handler + VertexColors.block_events + VertexColors.clear_event_handlers + VertexColors.remove_event_handler + VertexColors.set_value + diff --git a/docs/source/api/graphic_features/VertexPositions.rst b/docs/source/api/graphic_features/VertexPositions.rst new file mode 100644 index 000000000..9669ab6d5 --- /dev/null +++ b/docs/source/api/graphic_features/VertexPositions.rst @@ -0,0 +1,37 @@ +.. _api.VertexPositions: + +VertexPositions +*************** + +=============== +VertexPositions +=============== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VertexPositions_api + + VertexPositions + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VertexPositions_api + + VertexPositions.buffer + VertexPositions.shared + VertexPositions.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VertexPositions_api + + VertexPositions.add_event_handler + VertexPositions.block_events + VertexPositions.clear_event_handlers + VertexPositions.remove_event_handler + VertexPositions.set_value + diff --git a/docs/source/api/graphic_features/Visible.rst b/docs/source/api/graphic_features/Visible.rst new file mode 100644 index 000000000..957b4433a --- /dev/null +++ b/docs/source/api/graphic_features/Visible.rst @@ -0,0 +1,35 @@ +.. _api.Visible: + +Visible +******* + +======= +Visible +======= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Visible_api + + Visible + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Visible_api + + Visible.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Visible_api + + Visible.add_event_handler + Visible.block_events + Visible.clear_event_handlers + Visible.remove_event_handler + Visible.set_value + diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index 06e3119e5..87504ea8a 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -4,20 +4,28 @@ Graphic Features .. toctree:: :maxdepth: 1 - ColorFeature - CmapFeature - ImageCmapFeature - HeatmapCmapFeature - PointsDataFeature + VertexColors + UniformColor + UniformSize + Thickness + VertexPositions PointsSizesFeature - ImageDataFeature - HeatmapDataFeature - PresentFeature - ThicknessFeature - GraphicFeature - GraphicFeatureIndexable - FeatureEvent - to_gpu_supported_dtype + VertexCmap + TextureArray + ImageCmap + ImageVmin + ImageVmax + ImageInterpolation + ImageCmapInterpolation + TextData + FontSize + TextFaceColor + TextOutlineColor + TextOutlineThickness LinearSelectionFeature LinearRegionSelectionFeature + Name + Offset + Rotation + Visible Deleted diff --git a/docs/source/api/graphic_features/to_gpu_supported_dtype.rst b/docs/source/api/graphic_features/to_gpu_supported_dtype.rst deleted file mode 100644 index 984a76157..000000000 --- a/docs/source/api/graphic_features/to_gpu_supported_dtype.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. _api.to_gpu_supported_dtype: - -to_gpu_supported_dtype -********************** - -====================== -to_gpu_supported_dtype -====================== -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: to_gpu_supported_dtype_api - - to_gpu_supported_dtype - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: to_gpu_supported_dtype_api - - -Methods -~~~~~~~ -.. autosummary:: - :toctree: to_gpu_supported_dtype_api - - diff --git a/docs/source/api/graphics/HeatmapGraphic.rst b/docs/source/api/graphics/HeatmapGraphic.rst deleted file mode 100644 index ffa86eb16..000000000 --- a/docs/source/api/graphics/HeatmapGraphic.rst +++ /dev/null @@ -1,44 +0,0 @@ -.. _api.HeatmapGraphic: - -HeatmapGraphic -************** - -============== -HeatmapGraphic -============== -.. currentmodule:: fastplotlib - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: HeatmapGraphic_api - - HeatmapGraphic - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: HeatmapGraphic_api - - HeatmapGraphic.children - HeatmapGraphic.name - HeatmapGraphic.position - HeatmapGraphic.position_x - HeatmapGraphic.position_y - HeatmapGraphic.position_z - HeatmapGraphic.rotation - HeatmapGraphic.visible - HeatmapGraphic.world_object - -Methods -~~~~~~~ -.. autosummary:: - :toctree: HeatmapGraphic_api - - HeatmapGraphic.add_linear_region_selector - HeatmapGraphic.add_linear_selector - HeatmapGraphic.link - HeatmapGraphic.reset_feature - HeatmapGraphic.rotate - HeatmapGraphic.set_feature - diff --git a/docs/source/api/graphics/ImageGraphic.rst b/docs/source/api/graphics/ImageGraphic.rst index 00b27340d..639a02cd1 100644 --- a/docs/source/api/graphics/ImageGraphic.rst +++ b/docs/source/api/graphics/ImageGraphic.rst @@ -20,14 +20,20 @@ Properties .. autosummary:: :toctree: ImageGraphic_api - ImageGraphic.children + ImageGraphic.block_events + ImageGraphic.cmap + ImageGraphic.cmap_interpolation + ImageGraphic.data + ImageGraphic.deleted + ImageGraphic.event_handlers + ImageGraphic.events + ImageGraphic.interpolation ImageGraphic.name - ImageGraphic.position - ImageGraphic.position_x - ImageGraphic.position_y - ImageGraphic.position_z + ImageGraphic.offset ImageGraphic.rotation ImageGraphic.visible + ImageGraphic.vmax + ImageGraphic.vmin ImageGraphic.world_object Methods @@ -35,10 +41,12 @@ Methods .. autosummary:: :toctree: ImageGraphic_api + ImageGraphic.add_event_handler ImageGraphic.add_linear_region_selector ImageGraphic.add_linear_selector - ImageGraphic.link - ImageGraphic.reset_feature + ImageGraphic.attach_feature + ImageGraphic.detach_feature + ImageGraphic.remove_event_handler + ImageGraphic.reset_vmin_vmax ImageGraphic.rotate - ImageGraphic.set_feature diff --git a/docs/source/api/graphics/LineCollection.rst b/docs/source/api/graphics/LineCollection.rst index 8d10d8376..6399320ac 100644 --- a/docs/source/api/graphics/LineCollection.rst +++ b/docs/source/api/graphics/LineCollection.rst @@ -20,16 +20,18 @@ Properties .. autosummary:: :toctree: LineCollection_api - LineCollection.children + LineCollection.block_events LineCollection.cmap - LineCollection.cmap_values + LineCollection.colors + LineCollection.data + LineCollection.deleted + LineCollection.event_handlers + LineCollection.events LineCollection.graphics LineCollection.name - LineCollection.position - LineCollection.position_x - LineCollection.position_y - LineCollection.position_z + LineCollection.offset LineCollection.rotation + LineCollection.thickness LineCollection.visible LineCollection.world_object @@ -38,12 +40,14 @@ Methods .. autosummary:: :toctree: LineCollection_api + LineCollection.add_event_handler LineCollection.add_graphic LineCollection.add_linear_region_selector LineCollection.add_linear_selector - LineCollection.link + LineCollection.attach_feature + LineCollection.child_type + LineCollection.detach_feature + LineCollection.remove_event_handler LineCollection.remove_graphic - LineCollection.reset_feature LineCollection.rotate - LineCollection.set_feature diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst index 8b6fedf22..3f3c22207 100644 --- a/docs/source/api/graphics/LineGraphic.rst +++ b/docs/source/api/graphics/LineGraphic.rst @@ -20,13 +20,17 @@ Properties .. autosummary:: :toctree: LineGraphic_api - LineGraphic.children + LineGraphic.block_events + LineGraphic.cmap + LineGraphic.colors + LineGraphic.data + LineGraphic.deleted + LineGraphic.event_handlers + LineGraphic.events LineGraphic.name - LineGraphic.position - LineGraphic.position_x - LineGraphic.position_y - LineGraphic.position_z + LineGraphic.offset LineGraphic.rotation + LineGraphic.thickness LineGraphic.visible LineGraphic.world_object @@ -35,10 +39,11 @@ Methods .. autosummary:: :toctree: LineGraphic_api + LineGraphic.add_event_handler LineGraphic.add_linear_region_selector LineGraphic.add_linear_selector - LineGraphic.link - LineGraphic.reset_feature + LineGraphic.attach_feature + LineGraphic.detach_feature + LineGraphic.remove_event_handler LineGraphic.rotate - LineGraphic.set_feature diff --git a/docs/source/api/graphics/LineStack.rst b/docs/source/api/graphics/LineStack.rst index a39db46f8..ac670c5b2 100644 --- a/docs/source/api/graphics/LineStack.rst +++ b/docs/source/api/graphics/LineStack.rst @@ -20,16 +20,18 @@ Properties .. autosummary:: :toctree: LineStack_api - LineStack.children + LineStack.block_events LineStack.cmap - LineStack.cmap_values + LineStack.colors + LineStack.data + LineStack.deleted + LineStack.event_handlers + LineStack.events LineStack.graphics LineStack.name - LineStack.position - LineStack.position_x - LineStack.position_y - LineStack.position_z + LineStack.offset LineStack.rotation + LineStack.thickness LineStack.visible LineStack.world_object @@ -38,12 +40,14 @@ Methods .. autosummary:: :toctree: LineStack_api + LineStack.add_event_handler LineStack.add_graphic LineStack.add_linear_region_selector LineStack.add_linear_selector - LineStack.link + LineStack.attach_feature + LineStack.child_type + LineStack.detach_feature + LineStack.remove_event_handler LineStack.remove_graphic - LineStack.reset_feature LineStack.rotate - LineStack.set_feature diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst index 44d87d008..9174f2b5c 100644 --- a/docs/source/api/graphics/ScatterGraphic.rst +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -20,13 +20,17 @@ Properties .. autosummary:: :toctree: ScatterGraphic_api - ScatterGraphic.children + ScatterGraphic.block_events + ScatterGraphic.cmap + ScatterGraphic.colors + ScatterGraphic.data + ScatterGraphic.deleted + ScatterGraphic.event_handlers + ScatterGraphic.events ScatterGraphic.name - ScatterGraphic.position - ScatterGraphic.position_x - ScatterGraphic.position_y - ScatterGraphic.position_z + ScatterGraphic.offset ScatterGraphic.rotation + ScatterGraphic.sizes ScatterGraphic.visible ScatterGraphic.world_object @@ -35,5 +39,9 @@ Methods .. autosummary:: :toctree: ScatterGraphic_api + ScatterGraphic.add_event_handler + ScatterGraphic.attach_feature + ScatterGraphic.detach_feature + ScatterGraphic.remove_event_handler ScatterGraphic.rotate diff --git a/docs/source/api/graphics/TextGraphic.rst b/docs/source/api/graphics/TextGraphic.rst index 23425cf41..3a52890ef 100644 --- a/docs/source/api/graphics/TextGraphic.rst +++ b/docs/source/api/graphics/TextGraphic.rst @@ -20,18 +20,18 @@ Properties .. autosummary:: :toctree: TextGraphic_api - TextGraphic.children + TextGraphic.block_events + TextGraphic.deleted + TextGraphic.event_handlers + TextGraphic.events TextGraphic.face_color + TextGraphic.font_size TextGraphic.name + TextGraphic.offset TextGraphic.outline_color - TextGraphic.outline_size - TextGraphic.position - TextGraphic.position_x - TextGraphic.position_y - TextGraphic.position_z + TextGraphic.outline_thickness TextGraphic.rotation TextGraphic.text - TextGraphic.text_size TextGraphic.visible TextGraphic.world_object @@ -40,5 +40,9 @@ Methods .. autosummary:: :toctree: TextGraphic_api + TextGraphic.add_event_handler + TextGraphic.attach_feature + TextGraphic.detach_feature + TextGraphic.remove_event_handler TextGraphic.rotate diff --git a/docs/source/api/graphics/index.rst b/docs/source/api/graphics/index.rst index 611ee5833..b64ac53c0 100644 --- a/docs/source/api/graphics/index.rst +++ b/docs/source/api/graphics/index.rst @@ -4,10 +4,9 @@ Graphics .. toctree:: :maxdepth: 1 + LineGraphic ImageGraphic ScatterGraphic - LineGraphic - HeatmapGraphic + TextGraphic LineCollection LineStack - TextGraphic diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index 91884557a..61f5da307 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -42,7 +42,6 @@ Methods Subplot.add_animations Subplot.add_graphic - Subplot.add_heatmap Subplot.add_image Subplot.add_line Subplot.add_line_collection diff --git a/docs/source/api/selectors/LinearRegionSelector.rst b/docs/source/api/selectors/LinearRegionSelector.rst index 1b59e80c9..5840964a6 100644 --- a/docs/source/api/selectors/LinearRegionSelector.rst +++ b/docs/source/api/selectors/LinearRegionSelector.rst @@ -20,14 +20,17 @@ Properties .. autosummary:: :toctree: LinearRegionSelector_api - LinearRegionSelector.children + LinearRegionSelector.axis + LinearRegionSelector.block_events + LinearRegionSelector.deleted + LinearRegionSelector.event_handlers + LinearRegionSelector.events LinearRegionSelector.limits LinearRegionSelector.name - LinearRegionSelector.position - LinearRegionSelector.position_x - LinearRegionSelector.position_y - LinearRegionSelector.position_z + LinearRegionSelector.offset + LinearRegionSelector.parent LinearRegionSelector.rotation + LinearRegionSelector.selection LinearRegionSelector.visible LinearRegionSelector.world_object @@ -36,10 +39,14 @@ Methods .. autosummary:: :toctree: LinearRegionSelector_api + LinearRegionSelector.add_event_handler LinearRegionSelector.add_ipywidget_handler + LinearRegionSelector.attach_feature + LinearRegionSelector.detach_feature LinearRegionSelector.get_selected_data LinearRegionSelector.get_selected_index LinearRegionSelector.get_selected_indices LinearRegionSelector.make_ipywidget_slider + LinearRegionSelector.remove_event_handler LinearRegionSelector.rotate diff --git a/docs/source/api/selectors/LinearSelector.rst b/docs/source/api/selectors/LinearSelector.rst index 3278559d0..d3623b5e3 100644 --- a/docs/source/api/selectors/LinearSelector.rst +++ b/docs/source/api/selectors/LinearSelector.rst @@ -20,14 +20,17 @@ Properties .. autosummary:: :toctree: LinearSelector_api - LinearSelector.children + LinearSelector.axis + LinearSelector.block_events + LinearSelector.deleted + LinearSelector.event_handlers + LinearSelector.events LinearSelector.limits LinearSelector.name - LinearSelector.position - LinearSelector.position_x - LinearSelector.position_y - LinearSelector.position_z + LinearSelector.offset + LinearSelector.parent LinearSelector.rotation + LinearSelector.selection LinearSelector.visible LinearSelector.world_object @@ -36,10 +39,14 @@ Methods .. autosummary:: :toctree: LinearSelector_api + LinearSelector.add_event_handler LinearSelector.add_ipywidget_handler + LinearSelector.attach_feature + LinearSelector.detach_feature LinearSelector.get_selected_data LinearSelector.get_selected_index LinearSelector.get_selected_indices LinearSelector.make_ipywidget_slider + LinearSelector.remove_event_handler LinearSelector.rotate diff --git a/docs/source/api/selectors/PolygonSelector.rst b/docs/source/api/selectors/PolygonSelector.rst deleted file mode 100644 index 8de87ec74..000000000 --- a/docs/source/api/selectors/PolygonSelector.rst +++ /dev/null @@ -1,43 +0,0 @@ -.. _api.PolygonSelector: - -PolygonSelector -*************** - -=============== -PolygonSelector -=============== -.. currentmodule:: fastplotlib - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: PolygonSelector_api - - PolygonSelector - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: PolygonSelector_api - - PolygonSelector.children - PolygonSelector.name - PolygonSelector.position - PolygonSelector.position_x - PolygonSelector.position_y - PolygonSelector.position_z - PolygonSelector.rotation - PolygonSelector.visible - PolygonSelector.world_object - -Methods -~~~~~~~ -.. autosummary:: - :toctree: PolygonSelector_api - - PolygonSelector.get_selected_data - PolygonSelector.get_selected_index - PolygonSelector.get_selected_indices - PolygonSelector.get_vertices - PolygonSelector.rotate - diff --git a/docs/source/api/selectors/Synchronizer.rst b/docs/source/api/selectors/Synchronizer.rst deleted file mode 100644 index 2b28fe351..000000000 --- a/docs/source/api/selectors/Synchronizer.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. _api.Synchronizer: - -Synchronizer -************ - -============ -Synchronizer -============ -.. currentmodule:: fastplotlib - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: Synchronizer_api - - Synchronizer - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: Synchronizer_api - - Synchronizer.selectors - -Methods -~~~~~~~ -.. autosummary:: - :toctree: Synchronizer_api - - Synchronizer.add - Synchronizer.clear - Synchronizer.remove - diff --git a/docs/source/api/selectors/index.rst b/docs/source/api/selectors/index.rst index 01c040728..ffa4054db 100644 --- a/docs/source/api/selectors/index.rst +++ b/docs/source/api/selectors/index.rst @@ -6,5 +6,3 @@ Selectors LinearSelector LinearRegionSelector - PolygonSelector - Synchronizer From 4f1f4007466d5c118efdf120668286dd88d8e1fc Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 06:47:41 -0400 Subject: [PATCH 149/196] apparently we use __all__ in graphics to generate api docs --- fastplotlib/graphics/__init__.py | 10 ++++++++ fastplotlib/graphics/_features/__init__.py | 29 ++++++++++++++++++++++ fastplotlib/graphics/selectors/__init__.py | 6 +++++ 3 files changed, 45 insertions(+) diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py index bb3cd8854..40293fc67 100644 --- a/fastplotlib/graphics/__init__.py +++ b/fastplotlib/graphics/__init__.py @@ -3,3 +3,13 @@ from .image import ImageGraphic from .text import TextGraphic from .line_collection import LineCollection, LineStack + + +__all__ = [ + "LineGraphic", + "ImageGraphic", + "ScatterGraphic", + "TextGraphic", + "LineCollection", + "LineStack" +] diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index b9265a1a0..cf1d0af02 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -33,3 +33,32 @@ from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature from ._common import Name, Offset, Rotation, Visible, Deleted + + +__all__ =[ + "VertexColors", + "UniformColor", + "UniformSize", + "Thickness", + "VertexPositions", + "PointsSizesFeature", + "VertexCmap", + "TextureArray", + "ImageCmap", + "ImageVmin", + "ImageVmax", + "ImageInterpolation", + "ImageCmapInterpolation", + "TextData", + "FontSize", + "TextFaceColor", + "TextOutlineColor", + "TextOutlineThickness", + "LinearSelectionFeature", + "LinearRegionSelectionFeature", + "Name", + "Offset", + "Rotation", + "Visible", + "Deleted", +] \ No newline at end of file diff --git a/fastplotlib/graphics/selectors/__init__.py b/fastplotlib/graphics/selectors/__init__.py index 6f081448e..af2585437 100644 --- a/fastplotlib/graphics/selectors/__init__.py +++ b/fastplotlib/graphics/selectors/__init__.py @@ -1,3 +1,9 @@ from ._linear import LinearSelector from ._linear_region import LinearRegionSelector from ._polygon import PolygonSelector + + +__all__ = [ + "LinearSelector", + "LinearRegionSelector" +] \ No newline at end of file From 82ec4b48635efa6bbd4a755359d7a6dc497ff1e9 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 06:55:02 -0400 Subject: [PATCH 150/196] remove nbsphinx --- docs/source/conf.py | 1 - docs/source/quickstart.ipynb | 1673 ---------------------------------- setup.py | 1 - 3 files changed, 1675 deletions(-) delete mode 100644 docs/source/quickstart.ipynb diff --git a/docs/source/conf.py b/docs/source/conf.py index f681a8101..16d6ed7d1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,7 +23,6 @@ "sphinx.ext.viewcode", "sphinx_copybutton", "sphinx_design", - "nbsphinx", ] autosummary_generate = True diff --git a/docs/source/quickstart.ipynb b/docs/source/quickstart.ipynb deleted file mode 100644 index 6a892399e..000000000 --- a/docs/source/quickstart.ipynb +++ /dev/null @@ -1,1673 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "93740a09-9111-4777-ad57-173e9b80a2f0", - "metadata": { - "tags": [] - }, - "source": [ - "# Quick Start Guide 🚀\n", - "\n", - "This notebook goes through the basic components of the `fastplotlib` API, images, image updates, line plots, scatter plots, and grid plots.\n", - "\n", - "**NOTE: This quick start guide in the docs is NOT interactive. Download the examples from the repo and try them on your own computer. You can run the desktop examples directly if you have `glfw` installed, or try the notebook demos:** https://github.com/kushalkolar/fastplotlib/tree/master/examples\n", - "\n", - "It will not be possible to have live demos on the docs until someone can figure out how to get [pygfx](https://github.com/pygfx/pygfx) to work with `wgpu` in the browser, perhaps through [pyodide](https://github.com/pyodide/pyodide) or something :D." - ] - }, - { - "cell_type": "markdown", - "id": "5d21c330-89cd-49ab-9069-4e3652d4286b", - "metadata": {}, - "source": [ - "**The example images are from `imageio` so you will need to install it for this example notebook. But `imageio` is not required to use `fasptlotlib`**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "07f064bb-025a-4794-9b05-243810edaf60", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "!pip install imageio" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5f842366-bd39-47de-ad00-723b2be707e4", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import imageio.v3 as iio" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fb57c3d3-f20d-4d88-9e7a-04b9309bc637", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import fastplotlib as fpl\n", - "from ipywidgets import VBox, HBox, IntSlider\n", - "import numpy as np" - ] - }, - { - "cell_type": "markdown", - "id": "a9b386ac-9218-4f8f-97b3-f29b4201ef55", - "metadata": {}, - "source": [ - "## Images" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "237823b7-e2c0-4e2f-9ee8-e3fc2b4453c4", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# create a `Figure` instance\n", - "fig = fpl.Figure()\n", - "\n", - "# get a grayscale image\n", - "data = iio.imread(\"imageio:camera.png\")\n", - "\n", - "# plot the image data\n", - "image_graphic = fig[0, 0].add_image(data=data, name=\"sample-image\")\n", - "\n", - "# show the plot\n", - "fig.show()" - ] - }, - { - "cell_type": "markdown", - "id": "be5b408f-dd91-4e36-807a-8c22c8d7d216", - "metadata": {}, - "source": [ - "**In live notebooks or desktop applications, you can use the handle on the bottom right corner of the _canvas_ to resize it. You can also pan and zoom using your mouse!**" - ] - }, - { - "cell_type": "markdown", - "id": "9ba07ec1-a0cb-4461-87c6-c7b64d4a882b", - "metadata": {}, - "source": [ - "This is how you can take a snapshot of the canvas. Snapshots are shown throughout this doc page for the purposes of documentation, they are NOT necessary for real interactive usage. Download the notebooks to run live demos." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b64ba135-e753-43a9-ad1f-adcc7310792d", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "ac5f5e75-9aa4-441f-9a41-66c22cd53de8", - "metadata": {}, - "source": [ - "Changing graphic **\"features\"**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d3541d1d-0819-450e-814c-588ffc8e7ed5", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "image_graphic.cmap = \"viridis\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ab544719-9187-45bd-8127-aac79eea30e4", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "9693cf94-11e9-46a6-a5b7-b0fbed42ad81", - "metadata": {}, - "source": [ - "### Slicing data\n", - "\n", - "**Most features, such as `data` support slicing!**\n", - "\n", - "Out image data is of shape [n_rows, n_cols]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "330a47b5-50b1-4e6a-b8ab-d55d92af2042", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "image_graphic.data().shape" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "601f46d9-7f32-4a43-9090-4674218800ea", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "image_graphic.data[::15, :] = 1\n", - "image_graphic.data[:, ::15] = 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3443948f-9ac9-484a-a4bf-3a06c1ce5658", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "53125b3b-3ce2-43c5-b2e3-7cd37cec7d7d", - "metadata": {}, - "source": [ - "**Fancy indexing**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7344cbbe-40c3-4d9e-ae75-7abe3ddaeeeb", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "image_graphic.data[data > 175] = 255" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ef113d79-5d86-4be0-868e-30f82f8ab528", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "4df5296e-2a18-403f-82f1-acb8eaf280e3", - "metadata": {}, - "source": [ - "Adjust vmin vmax" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "28af88d1-0518-47a4-ab73-431d6aaf9cb8", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "image_graphic.cmap.vmin = 50\n", - "image_graphic.cmap.vmax = 150" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e3dfb827-c812-447d-b413-dc15653160b1", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "19a1b56b-fdca-40c5-91c9-3c9486fd8a21", - "metadata": {}, - "source": [ - "**Set the entire data array again**\n", - "\n", - "Note: The shape of the new data array must match the current data shown in the Graphic." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4dc3d0e4-b128-42cd-a53e-76846fc9b8a8", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "new_data = iio.imread(\"imageio:astronaut.png\")\n", - "new_data.shape" - ] - }, - { - "cell_type": "markdown", - "id": "3bd06068-fe3f-404d-ba4a-a72a2105904f", - "metadata": {}, - "source": [ - "This is an RGB image, convert to grayscale to maintain the shape of (512, 512)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "150047a6-a6ac-442d-a468-3e0c224a2b7e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "gray = new_data.dot([0.3, 0.6, 0.1])\n", - "gray.shape" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bf24576b-d336-4754-9992-9649ccaa4d1e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "image_graphic.data = gray" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "67d810c2-4020-4769-a5ba-0d4a972ee243", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "2fe82654-e554-4be6-92a0-ecdee0ef8519", - "metadata": {}, - "source": [ - "reset vmin vmax" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0be6e4bb-cf9a-4155-9f6a-8106e66e6132", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "image_graphic.cmap.reset_vmin_vmax()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bd51936c-ad80-4b33-b855-23565265a430", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "a6c1f3fb-a3a7-4175-bd8d-bb3203740771", - "metadata": {}, - "source": [ - "### Indexing plots" - ] - }, - { - "cell_type": "markdown", - "id": "3fc38694-aca6-4f56-97ac-3435059a6be7", - "metadata": {}, - "source": [ - "**Plots are indexable and give you their graphics by name**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8a547138-0f7d-470b-9925-8df479c3979e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5551861f-9860-4515-8222-2f1c6d6a3220", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig[0, 0][\"sample-image\"]" - ] - }, - { - "cell_type": "markdown", - "id": "0c29b36e-0eb4-4bb3-a8db-add58c303ee8", - "metadata": {}, - "source": [ - "**You can also use numerical indexing on `plot.graphics`**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ce6adbb0-078a-4e74-b189-58f860ee5df5", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig[0, 0].graphics" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "119bd6af-c486-4378-bc23-79b1759aa3a4", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig[0, 0].graphics[0]" - ] - }, - { - "cell_type": "markdown", - "id": "6b8e3f0d-56f8-447f-bf26-b52629d06e95", - "metadata": {}, - "source": [ - "The `Graphic` instance is also returned when you call `plot.add_`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "967c0cbd-287c-4d99-9891-9baf18f7b56a", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "image_graphic" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5da72e26-3536-47b8-839c-53452dd94f7a", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "image_graphic is fig[0, 0][\"sample-image\"]" - ] - }, - { - "cell_type": "markdown", - "id": "2b5ee18b-e61b-415d-902a-688b1c9c03b8", - "metadata": {}, - "source": [ - "### RGB images\n", - "\n", - "`cmap` arguments are ignored for rgb images, but vmin vmax still works" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1f7143ec-8ee1-47d2-b017-d0a8efc69fc6", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_rgb = fpl.Figure()\n", - "\n", - "fig_rgb[0, 0].add_image(new_data, name=\"rgb-image\")\n", - "\n", - "fig_rgb.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a47b1eaf-3638-470a-88a5-0026c81d7e2b", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_rgb.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "4848a929-4f3b-46d7-921b-ebfe8de0ebb5", - "metadata": {}, - "source": [ - "vmin and vmax are still applicable to rgb images" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ffe50132-8dd0-433c-b9c6-9ead8c3d48de", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_rgb[0, 0][\"rgb-image\"].cmap.vmin = 100" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "161468ba-b836-4021-8d11-8dfc140b94eb", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_rgb.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "1cb03f42-1029-4b16-a16b-35447d9e2955", - "metadata": { - "tags": [] - }, - "source": [ - "## Image updates\n", - "\n", - "This examples show how you can define animation functions that run on every render cycle." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "aadd757f-6379-4f52-a709-46aa57c56216", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# create another `Figure` instance\n", - "fig_vid = fpl.Figure()\n", - "\n", - "# make some random data again\n", - "data = np.random.rand(512, 512)\n", - "\n", - "# plot the data\n", - "fig_vid[0, 0].add_image(data=data, name=\"random-image\")\n", - "\n", - "# a function to update the image_graphic\n", - "# a subplot will pass its instance to the animation function as an argument\n", - "def update_data(subplot):\n", - " new_data = np.random.rand(512, 512)\n", - " subplot[\"random-image\"].data = new_data\n", - "\n", - "#add this as an animation function to the subplot\n", - "fig_vid[0, 0].add_animations(update_data)\n", - "\n", - "# show the plot\n", - "fig_vid.show()" - ] - }, - { - "cell_type": "markdown", - "id": "b313eda1-6e6c-466f-9fd5-8b70c1d3c110", - "metadata": {}, - "source": [ - "**Share controllers across plots**\n", - "\n", - "This example creates a new plot, but it synchronizes the pan-zoom controller" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "86e70b1e-4328-4035-b992-70dff16d2a69", - "metadata": {}, - "outputs": [], - "source": [ - "fig_sync = fpl.Figure(controllers=fig_vid.controllers)\n", - "\n", - "data = np.random.rand(512, 512)\n", - "\n", - "image_graphic_instance = fig_sync[0, 0].add_image(data=data, cmap=\"viridis\")\n", - "\n", - "# you will need to define a new animation function for this graphic\n", - "def update_data_2():\n", - " new_data = np.random.rand(512, 512)\n", - " # alternatively, you can use the stored reference to the graphic as well instead of indexing the Plot\n", - " image_graphic_instance.data = new_data\n", - "\n", - "# add the animation function to the figure instead of the subplot\n", - "fig_sync.add_animations(update_data_2)\n", - "\n", - "fig_sync.show()" - ] - }, - { - "cell_type": "markdown", - "id": "f226c9c2-8d0e-41ab-9ab9-1ae31fd91de5", - "metadata": {}, - "source": [ - "Keeping a reference to the Graphic instance, as shown above `image_graphic_instance`, is useful if you're creating something where you need flexibility in the naming of the graphics" - ] - }, - { - "cell_type": "markdown", - "id": "d11fabb7-7c76-4e94-893d-80ed9ee3be3d", - "metadata": {}, - "source": [ - "You can also use `ipywidgets.VBox` and `HBox` to stack plots. See the `gridplot` notebooks for a proper gridplot interface for more automated subplotting\n", - "\n", - "Not shown in the docs, try the live demo for this feature" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ef9743b3-5f81-4b79-9502-fa5fca08e56d", - "metadata": {}, - "outputs": [], - "source": [ - "#VBox([plot_v.canvas, plot_sync.show()])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "11839d95-8ff7-444c-ae13-6b072c3112c5", - "metadata": {}, - "outputs": [], - "source": [ - "#HBox([plot_v.show(), plot_sync.show()])" - ] - }, - { - "cell_type": "markdown", - "id": "e7859338-8162-408b-ac72-37e606057045", - "metadata": { - "tags": [] - }, - "source": [ - "## Line plots\n", - "\n", - "2D line plots\n", - "\n", - "This example plots a sine wave, cosine wave, and ricker wavelet and demonstrates how **Graphic Features** can be modified by slicing!" - ] - }, - { - "cell_type": "markdown", - "id": "a6fee1c2-4a24-4325-bca2-26e5a4bf6338", - "metadata": {}, - "source": [ - "Generate some data." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e8280da-b421-43a5-a1a6-2a196a408e9a", - "metadata": {}, - "outputs": [], - "source": [ - "# linspace, create 100 evenly spaced x values from -10 to 10\n", - "xs = np.linspace(-10, 10, 100)\n", - "# sine wave\n", - "ys = np.sin(xs)\n", - "sine = np.dstack([xs, ys])[0]\n", - "\n", - "# cosine wave\n", - "ys = np.cos(xs) + 5\n", - "cosine = np.dstack([xs, ys])[0]\n", - "\n", - "# sinc function\n", - "a = 0.5\n", - "ys = np.sinc(xs) * 3 + 8\n", - "sinc = np.dstack([xs, ys])[0]" - ] - }, - { - "cell_type": "markdown", - "id": "fbb806e5-1565-4189-936c-b7cf147a59ee", - "metadata": {}, - "source": [ - "Plot all of it on the same plot. Each line plot will be an individual Graphic, you can have any combination of graphics on a plot." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "93a5d1e6-d019-4dd0-a0d1-25d1704ab7a7", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a plot instance\n", - "fig_line = fpl.Figure()\n", - "\n", - "# plot sine wave, use a single color\n", - "sine_graphic = fig_line[0, 0].add_line(data=sine, thickness=5, colors=\"magenta\")\n", - "\n", - "# you can also use colormaps for lines!\n", - "cosine_graphic = fig_line[0, 0].add_line(data=cosine, thickness=12, cmap=\"autumn\")\n", - "\n", - "# or a list of colors for each datapoint\n", - "colors = [\"r\"] * 25 + [\"purple\"] * 25 + [\"y\"] * 25 + [\"b\"] * 25\n", - "sinc_graphic = fig_line[0, 0].add_line(data=sinc, thickness=5, colors = colors)\n", - "\n", - "fig_line.show()" - ] - }, - { - "cell_type": "markdown", - "id": "22dde600-0f56-4370-b017-c8f23a6c01aa", - "metadata": {}, - "source": [ - "\"stretching\" the camera, useful for large timeseries data\n", - "\n", - "Set `maintain_aspect = False` on a camera, and then use the right mouse button and move the mouse to stretch and squeeze the view!\n", - "\n", - "You can also click the **`1:1`** button to toggle this." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2695f023-f6ce-4e26-8f96-4fbed5510d1d", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line[0, 0].camera.maintain_aspect = False" - ] - }, - { - "cell_type": "markdown", - "id": "1651e965-f750-47ac-bf53-c23dae84cc98", - "metadata": {}, - "source": [ - "reset the plot area" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ba50a6ed-0f1b-4795-91dd-a7c3e40b8e3c", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line[0, 0].auto_scale(maintain_aspect=True)" - ] - }, - { - "cell_type": "markdown", - "id": "dcd68796-c190-4c3f-8519-d73b98ff6367", - "metadata": {}, - "source": [ - "Graphic features support slicing! :D " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cb0d13ed-ef07-46ff-b19e-eeca4c831037", - "metadata": {}, - "outputs": [], - "source": [ - "# indexing of colors\n", - "cosine_graphic.colors[:15] = \"magenta\"\n", - "cosine_graphic.colors[90:] = \"red\"\n", - "cosine_graphic.colors[60] = \"w\"\n", - "\n", - "# indexing to assign colormaps to entire lines or segments\n", - "sinc_graphic.cmap[10:50] = \"gray\"\n", - "sine_graphic.cmap = \"seismic\"\n", - "\n", - "# more complex indexing, set the blue value directly from an array\n", - "cosine_graphic.colors[65:90, 0] = np.linspace(0, 1, 90-65)" - ] - }, - { - "cell_type": "markdown", - "id": "bfe14ed3-e81f-4058-96a7-e2720b6d2f45", - "metadata": {}, - "source": [ - "Make a snapshot of the canvas after slicing" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a061a888-d732-406e-a9c2-8cc632fbc368", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "c9689887-cdf3-4a4d-948f-7efdb09bde4e", - "metadata": {}, - "source": [ - "**You can capture changes to a graphic feature as events**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cfa001f6-c640-4f91-beb0-c19b030e503f", - "metadata": {}, - "outputs": [], - "source": [ - "def callback_func(event_data):\n", - " print(event_data)\n", - "\n", - "# Will print event data when the color changes\n", - "cosine_graphic.colors.add_event_handler(callback_func)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bb8a0f95-0063-4cd4-a117-e3d62c6e120d", - "metadata": {}, - "outputs": [], - "source": [ - "# more complex indexing of colors\n", - "# from point 15 - 30, set every 3rd point as \"cyan\"\n", - "cosine_graphic.colors[15:50:3] = \"cyan\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3da9a43b-35bd-4b56-9cc7-967536aac967", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "c29f81f9-601b-49f4-b20c-575c56e58026", - "metadata": {}, - "source": [ - "Graphic `data` is also indexable" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d1a4314b-5723-43c7-94a0-b4cbb0e44d60", - "metadata": {}, - "outputs": [], - "source": [ - "cosine_graphic.data[10:50:5, :2] = sine[10:50:5]\n", - "cosine_graphic.data[90:, 1] = 7" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "682db47b-8c7a-4934-9be4-2067e9fb12d5", - "metadata": {}, - "outputs": [], - "source": [ - "cosine_graphic.data[0] = np.array([[-10, 0, 0]])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f779cba0-7ee2-4795-8da8-9a9593d3893e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "3f6d264b-1b03-407e-9d83-cd6cfb02e706", - "metadata": {}, - "source": [ - "Toggle the presence of a graphic within the scene" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fcba75b7-9a1e-4aae-9dec-715f7f7456c3", - "metadata": {}, - "outputs": [], - "source": [ - "sinc_graphic.present = False" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5e22d0f-a244-47e2-9a2d-1eaf79eda1d9", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line.canvas.snapshot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "763b9943-53a4-4e2a-b47a-4e9e5c9d7be3", - "metadata": {}, - "outputs": [], - "source": [ - "sinc_graphic.present = True" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b22a8660-26b3-4c73-b87a-df9d7cb4353a", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "86f4e535-ce88-415a-b8d2-53612a2de7b9", - "metadata": {}, - "source": [ - "You can create callbacks to `present` too, for example to re-scale the plot w.r.t. graphics that are present in the scene" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "64a20a16-75a5-4772-a849-630ade9be4ff", - "metadata": {}, - "outputs": [], - "source": [ - "sinc_graphic.present.add_event_handler(fig_line[0, 0].auto_scale)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fb093046-c94c-4085-86b4-8cd85cb638ff", - "metadata": {}, - "outputs": [], - "source": [ - "sinc_graphic.present = False" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f9dd6a54-3460-4fb7-bffb-82fd9288902f", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line.canvas.snapshot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f05981c3-c768-4631-ae62-6a8407b20c4c", - "metadata": {}, - "outputs": [], - "source": [ - "sinc_graphic.present = True" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cb5bf73e-b015-4b4f-82a0-c3ae8cc39ef7", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "05f93e93-283b-45d8-ab31-8d15a7671dd2", - "metadata": {}, - "source": [ - "You can set the z-positions of graphics to have them appear under or over other graphics" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6bb33406-5bef-455b-86ea-358a7d3ffa94", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "img = np.random.rand(20, 100)\n", - "\n", - "fig_line[0, 0].add_image(img, name=\"image\", cmap=\"gray\")\n", - "\n", - "# z axis position -1 so it is below all the lines\n", - "fig_line[0, 0][\"image\"].position_z = -1\n", - "fig_line[0, 0][\"image\"].position_x = -50" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5b586a89-ca3e-4e88-a801-bdd665384f59", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "2c90862e-2f2a-451f-a468-0cf6b857e87a", - "metadata": {}, - "source": [ - "### 3D line plot" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9c51229f-13a2-4653-bff3-15d43ddbca7b", - "metadata": {}, - "outputs": [], - "source": [ - "# just set the camera as \"3d\", the rest is basically the same :D \n", - "fig_line_3d = fpl.Figure(cameras='3d')\n", - "\n", - "# create a spiral\n", - "phi = np.linspace(0, 30, 200)\n", - "\n", - "xs = phi * np.cos(phi)\n", - "ys = phi * np.sin(phi)\n", - "zs = phi\n", - "\n", - "# use 3D data\n", - "# note: you usually mix 3D and 2D graphics on the same plot\n", - "spiral = np.dstack([xs, ys, zs])[0]\n", - "\n", - "fig_line_3d[0, 0].add_line(data=spiral, thickness=2, cmap='winter')\n", - "\n", - "fig_line_3d.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "28eb7014-4773-4a34-8bfc-bd3a46429012", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line_3d[0, 0].auto_scale(maintain_aspect=True)" - ] - }, - { - "cell_type": "markdown", - "id": "a202b3d0-2a0b-450a-93d4-76d0a1129d1d", - "metadata": {}, - "source": [ - "## Scatter plots\n", - "\n", - "Plot tens of thousands or millions of points\n", - "\n", - "There might be a small delay for a few seconds before the plot shows, this is due to shaders being compiled and a few other things. The plot should be very fast and responsive once it is displayed and future modifications should also be fast!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "39252df5-9ae5-4132-b97b-2785c5fa92ea", - "metadata": {}, - "outputs": [], - "source": [ - "# create a random distribution\n", - "# only 1,000 points shown here in the docs, but it can be millions\n", - "n_points = 1_000\n", - "\n", - "# if you have a good GPU go for 1.5 million points :D \n", - "# this is multiplied by 3\n", - "#n_points = 500_000\n", - "\n", - "# dimensions always have to be [n_points, xyz]\n", - "dims = (n_points, 3)\n", - "\n", - "clouds_offset = 15\n", - "\n", - "# create some random clouds\n", - "normal = np.random.normal(size=dims, scale=5)\n", - "# stack the data into a single array\n", - "cloud = np.vstack(\n", - " [\n", - " normal - clouds_offset,\n", - " normal,\n", - " normal + clouds_offset,\n", - " ]\n", - ")\n", - "\n", - "# color each of them separately\n", - "colors = [\"yellow\"] * n_points + [\"cyan\"] * n_points + [\"magenta\"] * n_points\n", - "\n", - "# create plot\n", - "fig_scatter = fpl.Figure()\n", - "\n", - "# use an alpha value since this will be a lot of points\n", - "scatter_graphic = fig_scatter[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", - "\n", - "fig_scatter.show()" - ] - }, - { - "cell_type": "markdown", - "id": "b6e4a704-ee6b-4316-956e-acb4dcc1c6f2", - "metadata": {}, - "source": [ - "**Scatter graphic features work similarly to line graphic**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8fa46ec0-8680-44f5-894c-559de3145932", - "metadata": {}, - "outputs": [], - "source": [ - "# half of the first cloud's points to red\n", - "scatter_graphic.colors[:n_points:2] = \"r\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "293a4793-44b9-4d18-ae6a-68e7c6f91acc", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_scatter.canvas.snapshot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e4dc71e4-5144-436f-a464-f2a29eee8f0b", - "metadata": {}, - "outputs": [], - "source": [ - "# set the green value directly\n", - "scatter_graphic.colors[n_points:n_points * 2, 1] = 0.3" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5ea7852d-fdae-401b-83b6-b6cfd975f64f", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_scatter.canvas.snapshot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5b637a29-cd5e-4011-ab81-3f91490d9ecd", - "metadata": {}, - "outputs": [], - "source": [ - "# set color values directly using an array\n", - "scatter_graphic.colors[n_points * 2:] = np.repeat([[1, 1, 0, 0.5]], n_points, axis=0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "02c19f51-6436-4601-976e-04326df0de81", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_scatter.canvas.snapshot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a4084fce-78a2-48b3-9a0d-7b57c165c3c1", - "metadata": {}, - "outputs": [], - "source": [ - "# change the data, change y-values\n", - "scatter_graphic.data[n_points:n_points * 2, 1] += 15" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2ec43f58-4710-4603-9358-682c4af3f701", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_scatter.canvas.snapshot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f486083e-7c58-4255-ae1a-3fe5d9bfaeed", - "metadata": {}, - "outputs": [], - "source": [ - "# set x values directly but using an array\n", - "scatter_graphic.data[n_points:n_points * 2, 0] = np.linspace(-40, 0, n_points)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6bcb3bc3-4b75-4bbc-b8ca-f8a3219ec3d7", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_scatter.canvas.snapshot()" - ] - }, - { - "cell_type": "markdown", - "id": "d9e554de-c436-4684-a46a-ce8a33d409ac", - "metadata": {}, - "source": [ - "## ipywidget layouts\n", - "\n", - "This just plots everything from these examples in a single output cell" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "01a6f70b-c81b-4ee5-8a6b-d979b87227eb", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# row1 = HBox([plot.show(), plot_v.show(), plot_sync.show()])\n", - "# row2 = HBox([plot_l.show(), plot_l3d.show(), plot_s.show()])\n", - "\n", - "# VBox([row1, row2])" - ] - }, - { - "cell_type": "markdown", - "id": "a26c0063-b7e0-4f36-bb14-db06bafa31aa", - "metadata": {}, - "source": [ - "## More subplots" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6b7e1129-ae8e-4a0f-82dc-bd8fb65871fc", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Figure of shape 2 x 3 with all controllers synced\n", - "figure_grid = fpl.Figure(shape=(2, 3), controller_ids=\"sync\")\n", - "\n", - "# Make a random image graphic for each subplot\n", - "for subplot in figure_grid:\n", - " # create image data\n", - " data = np.random.rand(512, 512)\n", - " # add an image to the subplot\n", - " subplot.add_image(data, name=\"rand-img\")\n", - "\n", - "# Define a function to update the image graphics with new data\n", - "# add_animations will pass the gridplot to the animation function\n", - "def update_data(f):\n", - " for subplot in f:\n", - " new_data = np.random.rand(512, 512)\n", - " # index the image graphic by name and set the data\n", - " subplot[\"rand-img\"].data = new_data\n", - " \n", - "# add the animation function\n", - "figure_grid.add_animations(update_data)\n", - "\n", - "# show the gridplot \n", - "figure_grid.show()" - ] - }, - { - "cell_type": "markdown", - "id": "f4f71c34-3925-442f-bd76-60dd57d09f48", - "metadata": {}, - "source": [ - "### Slicing GridPlot" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d8194c9e-9a99-4d4a-8984-a4cfcab0c42c", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# positional indexing\n", - "# row 0 and col 0\n", - "figure_grid[0, 0]" - ] - }, - { - "cell_type": "markdown", - "id": "d626640f-bc93-4883-9bf4-47b825bbc663", - "metadata": {}, - "source": [ - "You can get the graphics within a subplot, just like with simple `Plot`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bffec80c-e81b-4945-85a2-c2c5e8395677", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "figure_grid[0, 1].graphics" - ] - }, - { - "cell_type": "markdown", - "id": "a4e3184f-c86a-4a7e-b803-31632cc163b0", - "metadata": {}, - "source": [ - "and change their properties" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "04b616fb-6644-42ba-8683-0589ce7d165e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "figure_grid[0, 1].graphics[0].vmax = 0.5" - ] - }, - { - "cell_type": "markdown", - "id": "28f7362c-d1b9-43ef-85c5-4d68f70f459c", - "metadata": {}, - "source": [ - "more slicing with `GridPlot`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "920e6365-bb50-4882-9b0d-8367dc485360", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# you can give subplots human-readable string names\n", - "figure_grid[0, 2].name = \"top-right-plot\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "73300d2c-3e70-43ad-b5a2-40341b701ac8", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "figure_grid[\"top-right-plot\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "834d9905-35e9-4711-9375-5b1828c80ee2", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# view its position\n", - "figure_grid[\"top-right-plot\"].position" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9aa61efa-c6a5-4611-a03b-1b8da66b19f0", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# these are really the same\n", - "figure_grid[\"top-right-plot\"] is figure_grid[0, 2]" - ] - }, - { - "cell_type": "markdown", - "id": "28c8b145-86cb-4445-92be-b7537a87f7ca", - "metadata": {}, - "source": [ - "Indexing with subplot name and graphic name" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2b7b73a3-5335-4bd5-bbef-c7d3cfbb3ca7", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "figure_grid[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" - ] - }, - { - "cell_type": "markdown", - "id": "6a5b4368-ae4d-442c-a11f-45c70267339b", - "metadata": {}, - "source": [ - "## Figure subplot customization" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "175d45a6-3351-4b75-8ff3-08797fe0a389", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# 2 rows and 3 columns\n", - "shape = (2, 3)\n", - "\n", - "# pan-zoom controllers for each view\n", - "# views are synced if they have the \n", - "# same controller ID\n", - "controller_ids = [\n", - " [0, 3, 1], # id each controller with an integer\n", - " [2, 2, 3]\n", - "]\n", - "\n", - "\n", - "# you can give string names for each subplot within the gridplot\n", - "names = [\n", - " [\"subplot0\", \"subplot1\", \"subplot2\"],\n", - " [\"subplot3\", \"subplot4\", \"subplot5\"]\n", - "]\n", - "\n", - "# Create the grid plot\n", - "figure_grid = fpl.Figure(\n", - " shape=shape,\n", - " controller_ids=controller_ids,\n", - " names=names,\n", - ")\n", - "\n", - "\n", - "# Make a random image graphic for each subplot\n", - "for subplot in figure_grid:\n", - " data = np.random.rand(512, 512)\n", - " # create and add an ImageGraphic\n", - " subplot.add_image(data=data, name=\"rand-image\")\n", - " \n", - "\n", - "# Define a function to update the image graphics \n", - "# with new randomly generated data\n", - "def set_random_frame(gp):\n", - " for subplot in gp:\n", - " new_data = np.random.rand(512, 512)\n", - " subplot[\"rand-image\"].data = new_data\n", - "\n", - "# add the animation\n", - "figure_grid.add_animations(set_random_frame)\n", - "figure_grid.show()" - ] - }, - { - "cell_type": "markdown", - "id": "4224f1c2-5e61-4894-8d72-0519598a3cef", - "metadata": {}, - "source": [ - "Indexing the gridplot to access subplots" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d88dd9b2-9359-42e8-9dfb-96dcbbb34b95", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# can access subplot by name\n", - "figure_grid[\"subplot0\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a14df7ea-14c3-4a8a-84f2-2e2194236d9e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# can access subplot by index\n", - "figure_grid[0, 0]" - ] - }, - { - "cell_type": "markdown", - "id": "5f8a3427-7949-40a4-aec2-38d5d95ef156", - "metadata": {}, - "source": [ - "**subplots also support indexing!**\n", - "\n", - "this can be used to get graphics if they are named" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8c99fee0-ce46-4f18-8300-af025c9a967c", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# can access graphic directly via name\n", - "figure_grid[\"subplot0\"][\"rand-image\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ed4eebb7-826d-4856-bbb8-db2de966a0c3", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "figure_grid[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", - "figure_grid[\"subplot0\"][\"rand-image\"].vmax = 0.8" - ] - }, - { - "cell_type": "markdown", - "id": "ad322f6f-e7de-4eb3-a1d9-cf28701a2eae", - "metadata": {}, - "source": [ - "positional indexing also works event if subplots have string names" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "759d3966-d92b-460f-ba48-e57adabbf163", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "figure_grid[1, 0][\"rand-image\"].vim = 0.1\n", - "figure_grid[1, 0][\"rand-image\"].vmax = 0.3" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/setup.py b/setup.py index b50a6a9bf..3ba77201d 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,6 @@ "ipywidgets>=8.0.0,<9", "sphinx-copybutton", "sphinx-design", - "nbsphinx", "pandoc", "jupyterlab", "sidecar", From 8f72fe5fd2314d221d10bc03d6423ee9e5d50a50 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 06:55:12 -0400 Subject: [PATCH 151/196] bump version --- fastplotlib/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/VERSION b/fastplotlib/VERSION index d9958b371..0ea3a944b 100644 --- a/fastplotlib/VERSION +++ b/fastplotlib/VERSION @@ -1 +1 @@ -0.1.0.a16 +0.2.0 From 183440af5d9c158b28a89f6cda55a4cea37234d6 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Jun 2024 07:07:24 -0400 Subject: [PATCH 152/196] docstring --- fastplotlib/graphics/_features/_positions_graphics.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index 920995fd7..ee7927a36 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -406,6 +406,7 @@ def name(self) -> str: @property def transform(self) -> np.ndarray | None: + """Get or set the cmap transform. Maps values from the transform array to the cmap colors""" return self._transform @transform.setter @@ -438,6 +439,7 @@ def transform( @property def alpha(self) -> float: + """Get or set the alpha level""" return self._alpha @alpha.setter From bb6d6b55c0df73eb3df49fd3667d505a6e47959c Mon Sep 17 00:00:00 2001 From: Caitlin Date: Mon, 10 Jun 2024 18:31:18 +0000 Subject: [PATCH 153/196] finish up line collection --- .../desktop/line_collection/line_collection_cmap_values.py | 2 +- .../line_collection_cmap_values_qualitative.py | 2 +- fastplotlib/graphics/_collection_base.py | 2 +- fastplotlib/layouts/_plot_area.py | 6 ++++++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/desktop/line_collection/line_collection_cmap_values.py b/examples/desktop/line_collection/line_collection_cmap_values.py index 9eeef40f8..d423f82a8 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values.py +++ b/examples/desktop/line_collection/line_collection_cmap_values.py @@ -38,7 +38,7 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: fig[0, 0].add_line_collection( circles, cmap="bwr", - cmap_values=cmap_values, + cmap_transform=cmap_values, thickness=10 ) diff --git a/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py index 85f0724d8..d04e521fa 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py +++ b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py @@ -44,7 +44,7 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: fig[0, 0].add_line_collection( circles, cmap="tab10", - cmap_values=cmap_values, + cmap_transform=cmap_values, thickness=10 ) diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index da7a7d7eb..2a2c6ce15 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -149,7 +149,7 @@ class GraphicCollection(Graphic): def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) - cls._features = cls.child_type._features + cls.features = cls.child_type._features def __init__(self, name: str = None): super().__init__(name) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 4d8900971..5ebc82c87 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -12,6 +12,7 @@ from ._utils import create_controller from ..graphics._base import Graphic +from ..graphics._collection_base import GraphicCollection from ..graphics.selectors._base_selector import BaseSelector from ..legends import Legend @@ -565,6 +566,11 @@ def _add_or_insert_graphic( # if we don't use the weakref above, then the object lingers if a plot hook is used! graphic._fpl_add_plot_area_hook(self) + # need to also hook in individual graphics in a collection to plot area + if isinstance(graphic, GraphicCollection): + for g in graphic.graphics: + g._fpl_add_plot_area_hook(self) + def _check_graphic_name_exists(self, name): if name in self: raise ValueError( From 9df7574fa25011c41474e3a1e8be6992d0cc73ba Mon Sep 17 00:00:00 2001 From: Caitlin Date: Mon, 10 Jun 2024 20:28:42 +0000 Subject: [PATCH 154/196] fix adding line selector to line collection --- fastplotlib/graphics/line.py | 13 +--- fastplotlib/graphics/line_collection.py | 88 ++++++++++------------- fastplotlib/graphics/selectors/_linear.py | 6 +- 3 files changed, 40 insertions(+), 67 deletions(-) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 7393ac91f..454279029 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -264,12 +264,6 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): # need y offset too for this origin = (limits[0] - offset, position_y + self.offset[1]) - # endpoints of the data range - # used by linear selector but not linear region - end_points = ( - self.data.value[:, 1].min() - padding, - self.data.value[:, 1].max() + padding, - ) else: offset = self.offset[1] # y limits @@ -284,12 +278,7 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): # need x offset too for this origin = (position_x + self.offset[0], limits[0] - offset) - end_points = ( - self.data.value[:, 0].min() - padding, - self.data.value[:, 0].max() + padding, - ) - # initial bounds are 20% of the limits range bounds_init = (limits[0], int(np.ptp(limits) * 0.2) + offset) - return bounds_init, limits, size, origin, axis, end_points + return bounds_init, limits, size, origin, axis diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index adc1589ae..8573514c7 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -86,18 +86,18 @@ class LineCollection(GraphicCollection, LineCollectionProperties): _indexer = CollectionIndexer def __init__( - self, - data: 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, - metadata: Sequence[Any] | np.ndarray = None, - isolated_buffer: bool = True, - **kwargs, + self, + data: 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, + metadata: Sequence[Any] | np.ndarray = None, + isolated_buffer: bool = True, + **kwargs, ): """ Create a collection of :class:`.LineGraphic` @@ -269,7 +269,7 @@ def __init__( self.add_graphic(lg) def add_linear_selector( - self, selection: int = None, padding: float = 50, **kwargs + self, selection: int = None, padding: float = 50, **kwargs ) -> LinearSelector: """ Adds a :class:`.LinearSelector` . @@ -298,7 +298,7 @@ def add_linear_selector( size, origin, axis, - end_points, + center ) = self._get_linear_selector_init_args(padding, **kwargs) if selection is None: @@ -309,21 +309,23 @@ def add_linear_selector( f"the passed selection: {selection} is beyond the limits: {limits}" ) + # center should be the middle of the collection + selector = LinearSelector( selection=selection, limits=limits, - end_points=end_points, parent=self, - **kwargs, + size=size, + center=center, + ** kwargs, ) self._plot_area.add_graphic(selector, center=False) - selector.position_z = self.position_z + 1 return weakref.proxy(selector) def add_linear_region_selector( - self, padding: float = 100.0, **kwargs + self, padding: float = 100.0, **kwargs ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector` @@ -371,7 +373,6 @@ def _get_linear_selector_init_args(self, padding, **kwargs): limits = list() sizes = list() origin = list() - end_points = list() for g in self.graphics: ( @@ -380,14 +381,12 @@ def _get_linear_selector_init_args(self, padding, **kwargs): _size, _origin, axis, - _end_points, ) = g._get_linear_selector_init_args(padding=0, **kwargs) bounds_init.append(_bounds_init) limits.append(_limits) sizes.append(_size) origin.append(_origin) - end_points.append(_end_points) # set the init bounds using the extents of the collection b = np.vstack(bounds_init) @@ -397,14 +396,6 @@ def _get_linear_selector_init_args(self, padding, **kwargs): limits = np.vstack(limits) limits = (limits[:, 0].min(), limits[:, 1].max()) - # stack endpoints - end_points = np.vstack(end_points) - # use the min endpoint for index 0, highest endpoint for index 1 - end_points = [ - end_points[:, 0].min() - padding, - end_points[:, 1].max() + padding, - ] - # TODO: refactor this to use `LineStack.graphics[-1].position.y` if isinstance(self, LineStack): stack_offset = self.separation * len(sizes) @@ -412,15 +403,6 @@ def _get_linear_selector_init_args(self, padding, **kwargs): size = sum(sizes) # add the separations size += stack_offset - - # a better way to get the max y value? - # graphics y-position + data y-max + padding - end_points[1] = ( - self.graphics[-1].position_y - + self.graphics[-1].data()[:, 1].max() - + padding - ) - else: # just the biggest one if not stacked size = max(sizes) @@ -431,12 +413,14 @@ def _get_linear_selector_init_args(self, padding, **kwargs): o = np.vstack(origin) origin_y = (o[:, 1].min() + o[:, 1].max()) / 2 origin = (limits[0], origin_y) + center = (self.graphics[-1].world_object.world.y - self.graphics[0].world_object.world.y) / 2 else: o = np.vstack(origin) origin_x = (o[:, 0].min() + o[:, 0].max()) / 2 origin = (origin_x, limits[0]) + center = (self.graphics[-1].world_object.world.x - self.graphics[0].world_object.world.x) / 2 - return bounds, limits, size, origin, axis, end_points + return bounds, limits, size, origin, axis, center axes = {"x": 0, "y": 1, "z": 2} @@ -444,18 +428,18 @@ def _get_linear_selector_init_args(self, padding, **kwargs): 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, - metadata: Iterable[Any] | np.ndarray = None, - separation: float = 10.0, - separation_axis: str = "y", - **kwargs, + 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, + metadata: Iterable[Any] | np.ndarray = None, + separation: float = 10.0, + separation_axis: str = "y", + **kwargs, ): """ Create a stack of :class:`.LineGraphic` that are separated along the "x" or "y" axis. @@ -531,7 +515,7 @@ def __init__( line.offset = (line.offset[0], axis_zero, line.offset[2]) axis_zero = ( - axis_zero + line.data.value[:, axes[separation_axis]].max() + separation + axis_zero + line.data.value[:, axes[separation_axis]].max() + separation ) self.separation = separation diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index e2debc6d7..f73aff606 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -81,12 +81,12 @@ def __init__( axis: str, default "x" "x" | "y", the axis which the slider can move along + center: float + center of selector along the opposite axis of `axis` + parent: Graphic parent graphic for this LineSelector - end_points: (int, int) - set length of slider by bounding it between two x-pos or two y-pos - 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``. Double click on the selector first to enable the From 04eeb10e3b0a5d76f37e02aba5fd48a31f8bf312 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Jun 2024 21:28:01 -0400 Subject: [PATCH 155/196] finish line collection --- fastplotlib/graphics/_collection_base.py | 39 +++++++++++++----------- fastplotlib/graphics/line_collection.py | 30 +++++++++++++++--- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index 2a2c6ce15..70cba92b4 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -11,6 +11,23 @@ class CollectionIndexer: """Collection Indexer""" + def __init__(self, selection: np.ndarray[Graphic], features: set[str]): + """ + + Parameters + ---------- + + selection: np.ndarray of Graphics + array of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` + + """ + + if isinstance(selection, Graphic): + selection = np.asarray([selection]) + + self._selection = selection + self.features = features + @property def name(self) -> np.ndarray[str | None]: return np.asarray([g.name for g in self.graphics]) @@ -39,12 +56,12 @@ def rotation(self, values: np.ndarray | list[np.ndarray]): def visible(self) -> np.ndarray[bool]: return np.asarray([g.visible for g in self.graphics]) + # TODO: how to work with deleted feature in a collection + @visible.setter def visible(self, values: np.ndarray[bool] | list[bool]): self._set_feature("visible", values) - # TODO: how to work with deleted feature in a collection - def _set_feature(self, feature, values): if not len(values) == len(self): raise IndexError @@ -52,20 +69,6 @@ def _set_feature(self, feature, values): for g, v in zip(self.graphics, values): setattr(g, feature, v) - def __init__(self, selection: np.ndarray[Graphic], features: set[str]): - """ - - Parameters - ---------- - - selection: np.ndarray of Graphics - array of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` - - """ - - self._selection = selection - self.features = features - @property def graphics(self) -> np.ndarray[Graphic]: """Returns an array of the selected graphics. Always returns a proxy to the Graphic""" @@ -149,7 +152,7 @@ class GraphicCollection(Graphic): def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) - cls.features = cls.child_type._features + cls._features = cls.child_type._features def __init__(self, name: str = None): super().__init__(name) @@ -264,7 +267,7 @@ def remove_event_handler(self, callback, *types): self[:].remove_event_handler(callback, *types) def __getitem__(self, key) -> CollectionIndexer: - return self._indexer(selection=self.graphics[key], features=self.features) + return self._indexer(selection=self.graphics[key], features=self._features) def __del__(self): self.world_object.clear() diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 8573514c7..86818c1e1 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -11,11 +11,12 @@ from .selectors import LinearRegionSelector, LinearSelector -class LineCollectionProperties: +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 @@ -53,6 +54,7 @@ def colors(self, values: str | np.ndarray | tuple[float] | list[float] | list[st @property def data(self) -> CollectionFeature: + """get or set data of lines in the collection""" return CollectionFeature(self.graphics, "data") @data.setter @@ -61,15 +63,25 @@ def data(self, values): @property def cmap(self) -> CollectionFeature: + """ + Get or set a cmap along the line collection. + + Optionally set using a tuple ("cmap", , ) 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, name: str): - colors = parse_cmap_values(n_colors=len(self), cmap_name=name) + def cmap(self, name: str, transform: np.ndarray = None, alpha: float = 1.0): + colors = parse_cmap_values(n_colors=len(self), cmap_name=name, transform=transform, alpha=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.graphics]) @thickness.setter @@ -81,9 +93,14 @@ def thickness(self, values: np.ndarray | list[float]): g.thickness = v -class LineCollection(GraphicCollection, LineCollectionProperties): +class LineCollectionIndexer(CollectionIndexer, _LineCollectionProperties): + """Indexer for line collections""" + pass + + +class LineCollection(GraphicCollection, _LineCollectionProperties): child_type = LineGraphic - _indexer = CollectionIndexer + _indexer = LineCollectionIndexer def __init__( self, @@ -267,6 +284,9 @@ def __init__( ) self.add_graphic(lg) + + def __getitem__(self, item) -> LineCollectionIndexer: + return super().__getitem__(item) def add_linear_selector( self, selection: int = None, padding: float = 50, **kwargs From 72e54eec7d14bbf2a33816a50060bdc01c0d3d29 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Jun 2024 21:28:18 -0400 Subject: [PATCH 156/196] update line collection examples --- examples/desktop/line_collection/line_collection.py | 4 ++-- .../line_collection/line_collection_cmap_values.py | 6 +++--- .../line_collection_cmap_values_qualitative.py | 6 +++--- .../desktop/line_collection/line_collection_colors.py | 6 +++--- examples/desktop/line_collection/line_stack.py | 8 ++++---- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/desktop/line_collection/line_collection.py b/examples/desktop/line_collection/line_collection.py index dd6f3ca33..db99e32ed 100644 --- a/examples/desktop/line_collection/line_collection.py +++ b/examples/desktop/line_collection/line_collection.py @@ -1,6 +1,6 @@ """ -Line Plot -============ +Line collection +=============== Example showing how to plot line collections """ diff --git a/examples/desktop/line_collection/line_collection_cmap_values.py b/examples/desktop/line_collection/line_collection_cmap_values.py index d423f82a8..a727b41bf 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values.py +++ b/examples/desktop/line_collection/line_collection_cmap_values.py @@ -1,7 +1,7 @@ """ -Line Plot -============ -Example showing how to plot line collections +Line collections quantitative cmap +================================== +Example showing a line collection with a quantitative cmap """ # test_example = true diff --git a/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py index d04e521fa..f96fd3aac 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py +++ b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py @@ -1,7 +1,7 @@ """ -Line Plot -============ -Example showing how to plot line collections +Line collections qualitative cmaps +================================== +Example showing a line collection with a qualitative cmap """ # test_example = true diff --git a/examples/desktop/line_collection/line_collection_colors.py b/examples/desktop/line_collection/line_collection_colors.py index d53afcd5b..3ee561d8f 100644 --- a/examples/desktop/line_collection/line_collection_colors.py +++ b/examples/desktop/line_collection/line_collection_colors.py @@ -1,7 +1,7 @@ """ -Line Plot -============ -Example showing how to plot line collections +Line collection colors +====================== +Example showing one way ot setting colors for individual lines in a collection """ # test_example = true diff --git a/examples/desktop/line_collection/line_stack.py b/examples/desktop/line_collection/line_stack.py index cf5d933e3..4693ce313 100644 --- a/examples/desktop/line_collection/line_stack.py +++ b/examples/desktop/line_collection/line_stack.py @@ -1,7 +1,7 @@ """ -Line Plot -============ -Example showing how to plot line collections +Line stack +========== +Example showing how to plot a stack of lines """ # test_example = true @@ -20,7 +20,7 @@ fig = fpl.Figure() # line stack takes all the same arguments as line collection and behaves similarly -fig[0, 0].add_line_stack(data, cmap="jet") +lines_stack = fig[0, 0].add_line_stack(data, cmap="jet") fig.show(maintain_aspect=False) From 3ce055e50ecc5ee6b0a990aada1023d452dbb12a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 00:14:10 -0400 Subject: [PATCH 157/196] graphic collections are now iterables, add examples for setting properties, add names and metadatas args, separate kwargs for collection and individual lines --- .../line_collection_slicing.py | 66 +++++++++++ .../desktop/line_collection/line_stack.py | 16 ++- fastplotlib/graphics/_collection_base.py | 108 ++++++++++++------ fastplotlib/graphics/line_collection.py | 92 ++++++++++----- fastplotlib/layouts/_graphic_methods_mixin.py | 70 ++++++------ 5 files changed, 255 insertions(+), 97 deletions(-) create mode 100644 examples/desktop/line_collection/line_collection_slicing.py diff --git a/examples/desktop/line_collection/line_collection_slicing.py b/examples/desktop/line_collection/line_collection_slicing.py new file mode 100644 index 000000000..fdf4ef89d --- /dev/null +++ b/examples/desktop/line_collection/line_collection_slicing.py @@ -0,0 +1,66 @@ +""" +Line collection slicing +======================= +Example showing how to slice a line collection +""" + +# test_example = true + +import numpy as np +import fastplotlib as fpl + + +xs = np.linspace(0, np.pi * 10, 100) +# sine wave +ys = np.sin(xs) + +data = np.column_stack([xs, ys]) +multi_data = np.stack([data] * 15) + + +fig = fpl.Figure() + +lines = fig[0, 0].add_line_stack( + multi_data, + thickness=[2, 10, 2, 5, 5, 5, 8, 8, 8, 9, 3, 3, 3, 4, 4], + separation=1, + metadatas=list(range(15)), # some metadata + names=list("abcdefghijklmno"), # unique name for each line +) + +print("slice a collection to return a collection indexer") +print(lines[1:5]) # lines 1, 2, 3, 4 +print("collections supports fancy indexing!") +print(lines[::3]) +print("fancy index using properties of individual lines!") +print(lines[lines.thickness < 3]) +print(lines[lines.metadatas > 10]) + +# set line properties, such as data +# set y-values of lines 3 and 4 +lines[3:6].data[:, 1] = np.cos(xs) +# set these same lines to a different color +lines[3:6].colors = "cyan" + +# setting properties using fancy indexing +# set cmap along the line collection +lines[-3:].cmap = "plasma" + +# set cmap of along a single line +lines[7].cmap = "jet" + +# fancy indexing using line properties! +lines[lines.thickness > 8].colors = "r" +lines[lines.names == "a"].colors = "b" + +# fancy index at the level of lines and individual line properties! +lines[::2].colors[::5] = "magenta" # set every 5th point of every other line to magenta +lines[3:6].colors[50:, -1] = 0.6 # set half the points alpha to 0.6 + +fig.show(maintain_aspect=False) + +fig.canvas.set_logical_size(900, 600) + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/line_collection/line_stack.py b/examples/desktop/line_collection/line_stack.py index 4693ce313..676e6e5c2 100644 --- a/examples/desktop/line_collection/line_stack.py +++ b/examples/desktop/line_collection/line_stack.py @@ -10,17 +10,21 @@ import fastplotlib as fpl -xs = np.linspace(0, 100, 1000) +xs = np.linspace(0, np.pi * 10, 100) # sine wave -ys = np.sin(xs) * 20 +ys = np.sin(xs) -# make 25 lines -data = np.vstack([ys] * 25) +data = np.column_stack([xs, ys]) +multi_data = np.stack([data] * 10) fig = fpl.Figure() -# line stack takes all the same arguments as line collection and behaves similarly -lines_stack = fig[0, 0].add_line_stack(data, cmap="jet") +line_stack = fig[0, 0].add_line_stack( + multi_data, # shape: (10, 100, 2), i.e. [n_lines, n_points, xy] + cmap="jet", # applied along n_lines + thickness=5, + separation=1, # spacing between lines along the separation axis, default separation along "y" axis +) fig.show(maintain_aspect=False) diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index 70cba92b4..508c2b6d5 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -1,36 +1,41 @@ +from typing import Any import weakref import numpy as np -from ._base import HexStr, Graphic, PYGFX_EVENTS +from ._base import HexStr, Graphic # Dict that holds all collection graphics in one python instance COLLECTION_GRAPHICS: dict[HexStr, Graphic] = dict() -class CollectionIndexer: - """Collection Indexer""" - - def __init__(self, selection: np.ndarray[Graphic], features: set[str]): - """ +class CollectionProperties: + @property + def names(self) -> np.ndarray[str | None]: + return np.asarray([g.name for g in self]) - Parameters - ---------- + @names.setter + def names(self, values: np.ndarray[str] | list[str]): + self._set_feature("name", values) - selection: np.ndarray of Graphics - array of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` + def _set_feature(self, feature, values): + if not len(values) == len(self): + raise IndexError - """ + for g, v in zip(self, values): + setattr(g, feature, v) - if isinstance(selection, Graphic): - selection = np.asarray([selection]) + @property + def metadatas(self) -> np.ndarray[str | None]: + return np.asarray([g.metadata for g in self]) - self._selection = selection - self.features = features + @metadatas.setter + def metadatas(self, values: np.ndarray[str] | list[str]): + self._set_feature("metadata", values) @property def name(self) -> np.ndarray[str | None]: - return np.asarray([g.name for g in self.graphics]) + return np.asarray([g.name for g in self]) @name.setter def name(self, values: np.ndarray[str] | list[str]): @@ -38,7 +43,7 @@ def name(self, values: np.ndarray[str] | list[str]): @property def offset(self) -> np.ndarray: - return np.stack([g.offset for g in self.graphics]) + return np.stack([g.offset for g in self]) @offset.setter def offset(self, values: np.ndarray | list[np.ndarray]): @@ -46,28 +51,42 @@ def offset(self, values: np.ndarray | list[np.ndarray]): @property def rotation(self) -> np.ndarray: - return np.stack([g.rotation for g in self.graphics]) + return np.stack([g.rotation for g in self]) @rotation.setter def rotation(self, values: np.ndarray | list[np.ndarray]): self._set_feature("rotation", values) + # TODO: how to work with deleted feature in a collection + @property def visible(self) -> np.ndarray[bool]: - return np.asarray([g.visible for g in self.graphics]) - - # TODO: how to work with deleted feature in a collection + return np.asarray([g.visible for g in self]) @visible.setter def visible(self, values: np.ndarray[bool] | list[bool]): self._set_feature("visible", values) - def _set_feature(self, feature, values): - if not len(values) == len(self): - raise IndexError - for g, v in zip(self.graphics, values): - setattr(g, feature, v) +class CollectionIndexer(CollectionProperties): + """Collection Indexer""" + + def __init__(self, selection: np.ndarray[Graphic], features: set[str]): + """ + + Parameters + ---------- + + selection: np.ndarray of Graphics + array of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` + + """ + + if isinstance(selection, Graphic): + selection = np.asarray([selection]) + + self._selection = selection + self.features = features @property def graphics(self) -> np.ndarray[Graphic]: @@ -118,17 +137,17 @@ def my_handler(event): if decorating: def decorator(_callback): - for g in self.graphics: + for g in self: g.add_event_handler(_callback, *types) return _callback return decorator - for g in self.graphics: + for g in self: g.add_event_handler(*args) def remove_event_handler(self, callback, *types): - for g in self.graphics: + for g in self: g.remove_event_handler(callback, *types) def __getitem__(self, item): @@ -137,6 +156,15 @@ def __getitem__(self, item): def __len__(self): return len(self._selection) + def __iter__(self): + self._iter = iter(range(len(self))) + return self + + def __next__(self) -> Graphic: + index = next(self._iter) + + return self.graphics[index] + def __repr__(self): return ( f"{self.__class__.__name__} @ {hex(id(self))}\n" @@ -144,7 +172,7 @@ def __repr__(self): ) -class GraphicCollection(Graphic): +class GraphicCollection(Graphic, CollectionProperties): """Graphic Collection base class""" child_type: type @@ -154,8 +182,8 @@ def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) cls._features = cls.child_type._features - def __init__(self, name: str = None): - super().__init__(name) + def __init__(self, name: str = None, metadata: Any = None, **kwargs): + super().__init__(name=name, metadata=metadata, **kwargs) # list of mem locations of the graphics self._graphics: list[str] = list() @@ -163,6 +191,8 @@ def __init__(self, name: str = None): self._graphics_changed: bool = True self._graphics_array: np.ndarray[Graphic] = None + self._iter = None + @property def graphics(self) -> np.ndarray[Graphic]: """The Graphics within this collection. Always returns a proxy to the Graphics.""" @@ -267,6 +297,10 @@ def remove_event_handler(self, callback, *types): self[:].remove_event_handler(callback, *types) def __getitem__(self, key) -> CollectionIndexer: + if np.issubdtype(type(key), np.integer): + addr = self._graphics[key] + return weakref.proxy(COLLECTION_GRAPHICS[addr]) + return self._indexer(selection=self.graphics[key], features=self._features) def __del__(self): @@ -280,6 +314,16 @@ def __del__(self): def __len__(self): return len(self._graphics) + def __iter__(self): + self._iter = iter(range(len(self))) + return self + + def __next__(self) -> Graphic: + index = next(self._iter) + addr = self._graphics[index] + + return weakref.proxy(COLLECTION_GRAPHICS[addr]) + def __repr__(self): rval = super().__repr__() return f"{rval}\nCollection of <{len(self._graphics)}> Graphics" diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 86818c1e1..0dc4ddec2 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -23,7 +23,8 @@ def colors(self) -> CollectionFeature: 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 - self.colors[:] = values + for g in self: + g.colors = values return elif all(isinstance(v, str) for v in values): @@ -39,7 +40,7 @@ def colors(self, values: str | np.ndarray | tuple[float] | list[float] | list[st if isinstance(values, np.ndarray): if values.ndim == 2: # assume individual colors for each - for g, v in zip(self.graphics, values): + for g, v in zip(self, values): g.colors = v return @@ -49,7 +50,7 @@ def colors(self, values: str | np.ndarray | tuple[float] | list[float] | list[st else: # assume individual colors for each - for g, v in zip(self.graphics, values): + for g, v in zip(self, values): g.colors = v @property @@ -59,7 +60,8 @@ def data(self) -> CollectionFeature: @data.setter def data(self, values): - self.data[:] = values + for g, v in zip(self, values): + g.data = v @property def cmap(self) -> CollectionFeature: @@ -76,20 +78,21 @@ def cmap(self) -> CollectionFeature: @cmap.setter def cmap(self, name: str, transform: np.ndarray = None, alpha: float = 1.0): - colors = parse_cmap_values(n_colors=len(self), cmap_name=name, transform=transform, alpha=alpha) + 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.graphics]) + 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.graphics, values): + for g, v in zip(self, values): g.thickness = v @@ -104,7 +107,7 @@ class LineCollection(GraphicCollection, _LineCollectionProperties): def __init__( self, - data: List[np.ndarray], + 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, @@ -112,18 +115,23 @@ def __init__( cmap: Sequence[str] | str = None, cmap_transform: np.ndarray | List = None, name: str = None, - metadata: Sequence[Any] | np.ndarray = None, + names: list[str] = None, + metadata: Any = None, + metadatas: Sequence[Any] | np.ndarray = None, isolated_buffer: bool = True, - **kwargs, + kwargs_lines: list[dict] = None, + **kwargs_collection, ): """ Create a collection of :class:`.LineGraphic` 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] + 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 @@ -149,14 +157,23 @@ def __init__( if provided, these values are used to map the colors from the cmap name: str, optional - name of the line collection + name of the line collection as a whole - metadata: Iterable or array - metadata associated with this collection, this is for the user to manage. + 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 - passed to Graphic + 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 Features -------- @@ -167,18 +184,30 @@ def __init__( """ - super().__init__(name) + super().__init__(name=name, metadata=metadata, **kwargs_collection) if not isinstance(thickness, (float, int)): if len(thickness) != len(data): raise ValueError( - "args must be a single float or an iterable with same length as data" + 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 metadata is not None: - if len(metadata) != len(data): + if metadatas is not None: + if len(metadatas) != len(data): raise ValueError( - f"len(metadata) != len(data)\n" f"{len(metadata)} != {len(data)}" + 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 @@ -248,6 +277,9 @@ def __init__( "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): @@ -267,20 +299,26 @@ def __init__( _cmap = cmap[i] _c = None - if metadata is not None: - _m = metadata[i] + 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, + **kwargs_lines, ) self.add_graphic(lg) @@ -394,7 +432,7 @@ def _get_linear_selector_init_args(self, padding, **kwargs): sizes = list() origin = list() - for g in self.graphics: + for g in self: ( _bounds_init, _limits, diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index 489c8d4f6..359fbd081 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -72,18 +72,6 @@ def add_image( kwargs: additional keyword arguments passed to Graphic - Features - -------- - - **data**: :class:`.HeatmapDataFeature` - Manages the data buffer displayed in the HeatmapGraphic - - **cmap**: :class:`.HeatmapCmapFeature` - Manages the colormap - - **present**: :class:`.PresentFeature` - Control the presence of the Graphic in the scene - """ return self._create_graphic( @@ -100,7 +88,7 @@ def add_image( def add_line_collection( self, - data: List[numpy.ndarray], + data: Union[numpy.ndarray, List[numpy.ndarray]], thickness: Union[float, Sequence[float]] = 2.0, colors: Union[str, Sequence[str], numpy.ndarray, Sequence[numpy.ndarray]] = "w", uniform_colors: bool = False, @@ -108,9 +96,12 @@ def add_line_collection( cmap: Union[Sequence[str], str] = None, cmap_transform: Union[numpy.ndarray, List] = None, name: str = None, - metadata: Union[Sequence[Any], numpy.ndarray] = None, + names: list[str] = None, + metadata: Any = None, + metadatas: Union[Sequence[Any], numpy.ndarray] = None, isolated_buffer: bool = True, - **kwargs + kwargs_lines: list[dict] = None, + **kwargs_collection ) -> LineCollection: """ @@ -118,9 +109,11 @@ 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] + 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 @@ -146,14 +139,23 @@ def add_line_collection( if provided, these values are used to map the colors from the cmap name: str, optional - name of the line collection + name of the line collection as a whole - metadata: Iterable or array - metadata associated with this collection, this is for the user to manage. + 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 - passed to Graphic + 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 Features -------- @@ -174,8 +176,11 @@ def add_line_collection( cmap, cmap_transform, name, + names, metadata, + metadatas, isolated_buffer, + kwargs_lines, **kwargs ) @@ -207,6 +212,13 @@ def add_line( 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" @@ -214,12 +226,6 @@ def add_line( cmap_transform: 1D array-like of numerical values, optional if provided, these values are used to map the colors from the cmap - alpha: float, optional, default 1.0 - alpha value for the colors - - z_position: float, optional - z-axis position for placing the graphic - **kwargs passed to Graphic @@ -398,7 +404,7 @@ def add_text( font_size: float | int = 14, face_color: str | numpy.ndarray | list[float] | tuple[float] = "w", outline_color: str | numpy.ndarray | list[float] | tuple[float] = "w", - outline_thickness: float | int = 0, + outline_thickness: float = 0.0, screen_space: bool = True, offset: tuple[float] = (0, 0, 0), anchor: str = "middle-center", @@ -422,8 +428,8 @@ def add_text( outline_color: str or array, default "w" str or RGBA array to set the outline color of the text - outline_thickness: float | int, default 0 - text outline thickness + outline_thickness: float, default 0 + relative outline thickness, value between 0.0 - 0.5 screen_space: bool = True if True, text size is in screen space, if False the text size is in data space From acd743cc2201dd09ffe39ad6503042e562870c27 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 00:53:24 -0400 Subject: [PATCH 158/196] 3d line stack example with animation --- .../desktop/line_collection/line_stack_3d.py | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 examples/desktop/line_collection/line_stack_3d.py diff --git a/examples/desktop/line_collection/line_stack_3d.py b/examples/desktop/line_collection/line_stack_3d.py new file mode 100644 index 000000000..abf989a17 --- /dev/null +++ b/examples/desktop/line_collection/line_stack_3d.py @@ -0,0 +1,103 @@ +""" +Line stack 3D +============= +Example showing a 3D stack of lines with animations +""" + +# test_example = true + +import numpy as np +import fastplotlib as fpl + + +xs = np.linspace(0, np.pi * 10, 100) +# spiral +ys = np.sin(xs) +zs = np.cos(xs) + +data = np.column_stack([xs, ys, zs]) +multi_data = np.stack([data] * 10) + +# create figure to plot lines and use an orbit controller in 3D +fig = fpl.Figure(cameras="3d", controller_types="orbit") + +line_stack = fig[0, 0].add_line_stack( + multi_data, # shape: (10, 100, 2), i.e. [n_lines, n_points, xy] + cmap="jet", # applied along n_lines + thickness=3, + separation=1, # spacing between lines along the separation axis, default separation along "y" axis + name="lines", +) + + +x_increment = 0.1 + + +def animate_data(subplot): + """animate with different rates of spinning the spirals""" + global xs # x vals + global x_increment # increment + + # calculate the new data + # new a different spinning rate for each spiral + # top ones will spin faster than the bottom ones + new_xs = [xs + (factor * x_increment) for factor in np.linspace(0.5, 1.5, 10)] + y = [np.sin(x) for x in new_xs] + z = [np.cos(x) for x in new_xs] + + # iterate through collection and set data of each line + for i, line in enumerate(subplot["lines"]): + # set y and z values + line.data[:, 1:] = np.column_stack([y[i], z[i]]) + + x_increment += 0.1 + + +colors_iteration = 0 + + +def animate_colors(subplot): + """animate the colors""" + global colors_iteration + + # change the colors only on every 50th render cycle + # otherwise it just looks like flickering because it's too fast :) + if colors_iteration % 50 != 0: + colors_iteration += 1 + return + + # use cmap_transform to shift the cmap + cmap_transform = np.roll(np.arange(10), shift=int(colors_iteration / 50)) + + # set cmap with the transform + subplot["lines"].cmap = "jet", cmap_transform + + colors_iteration += 1 + + +fig[0, 0].add_animations(animate_data, animate_colors) + +# just a pre-saved camera state +camera_state = { + "position": np.array([-18.0, 9.0, 8.0]), + "rotation": np.array([0.00401791, -0.5951809, 0.00297593, 0.80357619]), + "scale": np.array([1.0, 1.0, 1.0]), + "reference_up": np.array([0.0, 1.0, 0.0]), + "fov": 50.0, + "width": 32, + "height": 20, + "zoom": 1, + "maintain_aspect": True, + "depth_range": None, +} + +fig.show(maintain_aspect=False) + +fig[0, 0].camera.set_state(camera_state) + +fig.canvas.set_logical_size(500, 500) + + +if __name__ == "__main__": + print(__doc__) + fpl.run() From ce078c60589bea7c551686aee570f34c9d36e406 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 00:53:48 -0400 Subject: [PATCH 159/196] fix line collection cmap with additional args --- fastplotlib/graphics/line_collection.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 0dc4ddec2..c6c80f5c1 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -77,7 +77,18 @@ def cmap(self) -> CollectionFeature: return CollectionFeature(self.graphics, "cmap") @cmap.setter - def cmap(self, name: str, transform: np.ndarray = None, alpha: float = 1.0): + def cmap(self, args): + if 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 From 4c69ab60b27b27697a0e1146e7ef00abf441beed Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 00:55:13 -0400 Subject: [PATCH 160/196] black --- .../desktop/line_collection/line_collection_cmap_values.py | 5 +---- examples/desktop/line_collection/line_collection_slicing.py | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/desktop/line_collection/line_collection_cmap_values.py b/examples/desktop/line_collection/line_collection_cmap_values.py index a727b41bf..5ffc032e9 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values.py +++ b/examples/desktop/line_collection/line_collection_cmap_values.py @@ -36,10 +36,7 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: fig = fpl.Figure() fig[0, 0].add_line_collection( - circles, - cmap="bwr", - cmap_transform=cmap_values, - thickness=10 + circles, cmap="bwr", cmap_transform=cmap_values, thickness=10 ) fig.show() diff --git a/examples/desktop/line_collection/line_collection_slicing.py b/examples/desktop/line_collection/line_collection_slicing.py index fdf4ef89d..9eaebdd7e 100644 --- a/examples/desktop/line_collection/line_collection_slicing.py +++ b/examples/desktop/line_collection/line_collection_slicing.py @@ -30,14 +30,16 @@ print("slice a collection to return a collection indexer") print(lines[1:5]) # lines 1, 2, 3, 4 + print("collections supports fancy indexing!") print(lines[::3]) + print("fancy index using properties of individual lines!") print(lines[lines.thickness < 3]) print(lines[lines.metadatas > 10]) # set line properties, such as data -# set y-values of lines 3 and 4 +# set y-values of lines 3, 4, 5 lines[3:6].data[:, 1] = np.cos(xs) # set these same lines to a different color lines[3:6].colors = "cyan" From c2f647d57829fff0ac94302f46502815d6cfc4a6 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 01:09:43 -0400 Subject: [PATCH 161/196] update kwargs for line collection because of mixin --- fastplotlib/graphics/line_collection.py | 61 +++++++++++-------- fastplotlib/layouts/_graphic_methods_mixin.py | 59 ++++++++++-------- 2 files changed, 69 insertions(+), 51 deletions(-) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index c6c80f5c1..7b05c85f3 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -131,7 +131,7 @@ def __init__( metadatas: Sequence[Any] | np.ndarray = None, isolated_buffer: bool = True, kwargs_lines: list[dict] = None, - **kwargs_collection, + **kwargs, ): """ Create a collection of :class:`.LineGraphic` @@ -186,16 +186,9 @@ def __init__( kwargs_collection kwargs for the collection, passed to GraphicCollection - Features - -------- - - Collections support the same features as the underlying graphic. You just have to slice the selection. - - See :class:`LineGraphic` details on the features. - """ - super().__init__(name=name, metadata=metadata, **kwargs_collection) + super().__init__(name=name, metadata=metadata, **kwargs) if not isinstance(thickness, (float, int)): if len(thickness) != len(data): @@ -505,9 +498,13 @@ def __init__( cmap: Iterable[str] | str = None, cmap_transform: np.ndarray | List = None, name: str = None, - metadata: Iterable[Any] | np.ndarray = 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, ): """ @@ -515,9 +512,11 @@ 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] + 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 @@ -529,6 +528,9 @@ def __init__( | 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 @@ -536,11 +538,20 @@ def __init__( .. note:: ``cmap`` overrides any arguments passed to ``colors`` - cmap_transform: 1D array-like or Iterable of numerical values, optional + cmap_transform: 1D array-like 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. + 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 @@ -549,18 +560,12 @@ def __init__( separation_axis: str, default "y" axis in which the line graphics in the stack should be separated - name: str, optional - name of the line stack - - kwargs - passed to LineCollection - - Features - -------- - Collections support the same features as the underlying graphic. You just have to slice the selection. + kwargs_lines: list[dict], optional + list of kwargs passed to the individual lines, ``len(kwargs_lines)`` must equal ``len(data)`` - See :class:`LineGraphic` details on the features. + kwargs_collection + kwargs for the collection, passed to GraphicCollection """ super().__init__( @@ -571,7 +576,11 @@ def __init__( cmap=cmap, cmap_transform=cmap_transform, name=name, + names=names, metadata=metadata, + metadatas=metadatas, + isolated_buffer=isolated_buffer, + kwargs_lines=kwargs_lines, **kwargs, ) diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index 359fbd081..387549ade 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -101,7 +101,7 @@ def add_line_collection( metadatas: Union[Sequence[Any], numpy.ndarray] = None, isolated_buffer: bool = True, kwargs_lines: list[dict] = None, - **kwargs_collection + **kwargs ) -> LineCollection: """ @@ -157,13 +157,6 @@ def add_line_collection( kwargs_collection kwargs for the collection, passed to GraphicCollection - Features - -------- - - Collections support the same features as the underlying graphic. You just have to slice the selection. - - See :class:`LineGraphic` details on the features. - """ return self._create_graphic( @@ -253,9 +246,13 @@ def add_line_stack( cmap: Union[Iterable[str], str] = None, cmap_transform: Union[numpy.ndarray, List] = None, name: str = None, - metadata: Union[Iterable[Any], numpy.ndarray] = None, + names: list[str] = None, + metadata: Any = None, + metadatas: Union[Sequence[Any], numpy.ndarray] = None, + isolated_buffer: bool = True, separation: float = 10.0, separation_axis: str = "y", + kwargs_lines: list[dict] = None, **kwargs ) -> LineStack: """ @@ -264,9 +261,11 @@ def add_line_stack( 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] + 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 @@ -278,6 +277,9 @@ def add_line_stack( | 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 @@ -285,11 +287,20 @@ def add_line_stack( .. note:: ``cmap`` overrides any arguments passed to ``colors`` - cmap_transform: 1D array-like or Iterable of numerical values, optional + cmap_transform: 1D array-like 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. + 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 @@ -298,18 +309,12 @@ def add_line_stack( separation_axis: str, default "y" axis in which the line graphics in the stack should be separated - name: str, optional - name of the line stack - - kwargs - passed to LineCollection - - Features - -------- - Collections support the same features as the underlying graphic. You just have to slice the selection. + kwargs_lines: list[dict], optional + list of kwargs passed to the individual lines, ``len(kwargs_lines)`` must equal ``len(data)`` - See :class:`LineGraphic` details on the features. + kwargs_collection + kwargs for the collection, passed to GraphicCollection """ @@ -322,9 +327,13 @@ def add_line_stack( cmap, cmap_transform, name, + names, metadata, + metadatas, + isolated_buffer, separation, separation_axis, + kwargs_lines, **kwargs ) From 6d9cf620e748da0811fd7ee69a72e8818c6f53fc Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 01:10:06 -0400 Subject: [PATCH 162/196] add numpy.integer check for buffermanager parse slice --- fastplotlib/graphics/_features/_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 9b256b697..6fae28d0b 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -227,8 +227,8 @@ def _parse_offset_size( """ parse offset and size for first, i.e. n_datapoints, dimension """ - if isinstance(key, int): - # simplest case + if np.issubdtype(type(key), np.integer): + # simplest case, just an int offset = key size = 1 From 7525209005edd83f1e56257fe2000fd1b3fc9cc5 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 01:14:53 -0400 Subject: [PATCH 163/196] rename --- fastplotlib/graphics/_collection_base.py | 45 +++++++++++------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index 508c2b6d5..4c3ed20d0 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -10,23 +10,25 @@ class CollectionProperties: + def _set_feature(self, feature, values): + if not len(values) == len(self): + raise IndexError + + for g, v in zip(self, values): + setattr(g, feature, v) + @property def names(self) -> np.ndarray[str | None]: + """get or set the name of the individual graphics in the collection""" return np.asarray([g.name for g in self]) @names.setter def names(self, values: np.ndarray[str] | list[str]): self._set_feature("name", values) - def _set_feature(self, feature, values): - if not len(values) == len(self): - raise IndexError - - for g, v in zip(self, values): - setattr(g, feature, v) - @property def metadatas(self) -> np.ndarray[str | None]: + """get or set the metadata of the individual graphics in the collection""" return np.asarray([g.metadata for g in self]) @metadatas.setter @@ -34,37 +36,32 @@ def metadatas(self, values: np.ndarray[str] | list[str]): self._set_feature("metadata", values) @property - def name(self) -> np.ndarray[str | None]: - return np.asarray([g.name for g in self]) - - @name.setter - def name(self, values: np.ndarray[str] | list[str]): - self._set_feature("name", values) - - @property - def offset(self) -> np.ndarray: + def offsets(self) -> np.ndarray: + """get or set the offset of the individual graphics in the collection""" return np.stack([g.offset for g in self]) - @offset.setter - def offset(self, values: np.ndarray | list[np.ndarray]): + @offsets.setter + def offsets(self, values: np.ndarray | list[np.ndarray]): self._set_feature("offset", values) @property - def rotation(self) -> np.ndarray: + def rotations(self) -> np.ndarray: + """get or set the rotation of the individual graphics in the collection""" return np.stack([g.rotation for g in self]) - @rotation.setter - def rotation(self, values: np.ndarray | list[np.ndarray]): + @rotations.setter + def rotations(self, values: np.ndarray | list[np.ndarray]): self._set_feature("rotation", values) # TODO: how to work with deleted feature in a collection @property - def visible(self) -> np.ndarray[bool]: + def visibles(self) -> np.ndarray[bool]: + """get or set the offsets of the individual graphics in the collection""" return np.asarray([g.visible for g in self]) - @visible.setter - def visible(self, values: np.ndarray[bool] | list[bool]): + @visibles.setter + def visibles(self, values: np.ndarray[bool] | list[bool]): self._set_feature("visible", values) From 0380ea95efdbc3709ed049eafae124ec743eb52e Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 01:17:36 -0400 Subject: [PATCH 164/196] docstrings --- fastplotlib/graphics/_collection_base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index 4c3ed20d0..8f96159e5 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -10,6 +10,11 @@ class CollectionProperties: + """ + Properties common to all Graphic Collections + + Allows getting and setting the common properties of the individual graphics in the collection + """ def _set_feature(self, feature, values): if not len(values) == len(self): raise IndexError @@ -87,7 +92,7 @@ def __init__(self, selection: np.ndarray[Graphic], features: set[str]): @property def graphics(self) -> np.ndarray[Graphic]: - """Returns an array of the selected graphics. Always returns a proxy to the Graphic""" + """Returns an array of the selected graphics""" return tuple(self._selection) def add_event_handler(self, *args): @@ -291,6 +296,7 @@ def my_handler(event): return self[:].add_event_handler(*args) def remove_event_handler(self, callback, *types): + """remove an event handler""" self[:].remove_event_handler(callback, *types) def __getitem__(self, key) -> CollectionIndexer: From 43a9f8d7af912f7ef34b3075d263467b69a8b9f5 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 01:38:14 -0400 Subject: [PATCH 165/196] line linear selector init logic --- fastplotlib/graphics/line.py | 136 +++++++++++++---------------------- 1 file changed, 48 insertions(+), 88 deletions(-) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 454279029..6055c2471 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -114,11 +114,16 @@ def add_linear_selector( Parameters ---------- - selection: float - initial position of the selector + 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 - pad the length of the selector + 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` @@ -129,32 +134,10 @@ def add_linear_selector( """ - 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, limits, size, center = self._get_linear_selector_init_args(axis, padding) if selection is None: - selection = axis_vals[0] - limits = axis_vals[0], axis_vals[-1] - - if not limits[0] <= selection <= limits[1]: - raise ValueError( - f"the passed selection: {selection} is beyond the limits: {limits}" - ) - - # 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) + selection = bounds_init[0] selector = LinearSelector( selection=selection, @@ -174,7 +157,11 @@ def add_linear_selector( return weakref.proxy(selector) def add_linear_region_selector( - self, padding: float = 0.0, axis: str = "x", **kwargs + 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, @@ -182,8 +169,14 @@ def add_linear_region_selector( Parameters ---------- - padding: float, default 100.0 - Extends the linear selector along the y-axis to make it easier to interact with. + 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`` @@ -195,34 +188,14 @@ def add_linear_region_selector( """ - n_datapoints = self.data.value.shape[0] - value_25p = int(n_datapoints / 4) + bounds_init, limits, size, center = self._get_linear_selector_init_args(axis, padding) - # 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) + if selection is None: + selection = bounds_init # create selector selector = LinearRegionSelector( - selection=bounds_init, + selection=selection, limits=limits, size=size, center=center, @@ -241,44 +214,31 @@ def add_linear_region_selector( return weakref.proxy(selector) # TODO: this method is a bit of a mess, can refactor later - def _get_linear_selector_init_args(self, padding: float, **kwargs): - # computes initial bounds, limits, size and origin of linear selectors - data = self.data.value + 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) - if "axis" in kwargs.keys(): - axis = kwargs["axis"] - else: - axis = "x" + # remove any nans + data = self.data.value[~np.any(np.isnan(self.data.value), axis=1)] if axis == "x": - offset = self.offset[0] - # x limits - limits = (data[0, 0] + offset, data[-1, 0] + offset) - - # height + padding - size = np.ptp(data[:, 1]) + padding - - # initial position of the selector - position_y = (data[:, 1].min() + data[:, 1].max()) / 2 - - # need y offset too for this - origin = (limits[0] - offset, position_y + self.offset[1]) - - else: - offset = self.offset[1] - # y limits - limits = (data[0, 1] + offset, data[-1, 1] + offset) + # xvals + axis_vals = data[:, 0] - # width + padding - size = np.ptp(data[:, 0]) + padding + # yvals to get size and center + magn_vals = data[:, 1] + elif axis == "y": + axis_vals = data[:, 1] + magn_vals = data[:, 0] - # initial position of the selector - position_x = (data[:, 0].min() + data[:, 0].max()) / 2 + bounds_init = axis_vals[0], axis_vals[value_25p] + limits = axis_vals[0], axis_vals[-1] - # need x offset too for this - origin = (position_x + self.offset[0], limits[0] - offset) + # width or height of selector + size = int(np.ptp(magn_vals) * 1.5 + padding) - # initial bounds are 20% of the limits range - bounds_init = (limits[0], int(np.ptp(limits) * 0.2) + offset) + # center of selector along the other axis + center = np.nanmean(magn_vals) - return bounds_init, limits, size, origin, axis + return bounds_init, limits, size, center From cc2b92b2b50225608bb122e0e4bba9b5b1d3b634 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 02:01:31 -0400 Subject: [PATCH 166/196] fix line collection init selectors --- fastplotlib/graphics/line_collection.py | 167 +++++++++++------------- 1 file changed, 75 insertions(+), 92 deletions(-) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 7b05c85f3..f3f337650 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -331,18 +331,23 @@ def __getitem__(self, item) -> LineCollectionIndexer: return super().__getitem__(item) def add_linear_selector( - self, selection: int = None, padding: float = 50, **kwargs + self, selection: float = None, padding: float = 0.0, axis: str = "x", **kwargs ) -> LinearSelector: """ - Adds a :class:`.LinearSelector` . + Adds a linear selector. Parameters ---------- - selection: int - initial position of the selector + 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 - pad the length of the selector + 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` @@ -352,50 +357,50 @@ def add_linear_selector( LinearSelector """ - # TODO: Use bbox to get size and center for selectors! - ( - bounds, - limits, - size, - origin, - axis, - center - ) = self._get_linear_selector_init_args(padding, **kwargs) + bounds_init, limits, size, center = self._get_linear_selector_init_args(axis, padding) if selection is None: - selection = limits[0] - - if selection < limits[0] or selection > limits[1]: - raise ValueError( - f"the passed selection: {selection} is beyond the limits: {limits}" - ) - - # center should be the middle of the collection + selection = bounds_init[0] selector = LinearSelector( selection=selection, limits=limits, - parent=self, size=size, center=center, - ** kwargs, + axis=axis, + parent=weakref.proxy(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 weakref.proxy(selector) def add_linear_region_selector( - self, padding: float = 100.0, **kwargs + self, + selection: tuple[float, float] = None, + padding: float = 0.0, + axis: str = "x", + **kwargs ) -> LinearRegionSelector: """ - Add a :class:`.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 ---------- - padding: float, default 100.0 - Extends the linear selector along the y-axis to make it easier to interact with. + 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`` @@ -407,82 +412,60 @@ def add_linear_region_selector( """ - ( - bounds, - limits, - size, - origin, - axis, - end_points, - ) = self._get_linear_selector_init_args(padding, **kwargs) + 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=bounds, + selection=selection, limits=limits, size=size, - origin=origin, - parent=self, + center=center, + axis=axis, + parent=weakref.proxy(self), **kwargs, ) self._plot_area.add_graphic(selector, center=False) - selector.position_z = self.position_z - 1 - return weakref.proxy(selector) + # place selector below this graphic + selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] - 1) - def _get_linear_selector_init_args(self, padding, **kwargs): - bounds_init = list() - limits = list() - sizes = list() - origin = list() - - for g in self: - ( - _bounds_init, - _limits, - _size, - _origin, - axis, - ) = g._get_linear_selector_init_args(padding=0, **kwargs) - - bounds_init.append(_bounds_init) - limits.append(_limits) - sizes.append(_size) - origin.append(_origin) - - # set the init bounds using the extents of the collection - b = np.vstack(bounds_init) - bounds = (b[:, 0].min(), b[:, 1].max()) - - # set the limits using the extents of the collection - limits = np.vstack(limits) - limits = (limits[:, 0].min(), limits[:, 1].max()) - - # TODO: refactor this to use `LineStack.graphics[-1].position.y` - if isinstance(self, LineStack): - stack_offset = self.separation * len(sizes) - # sum them if it's a stack - size = sum(sizes) - # add the separations - size += stack_offset - else: - # just the biggest one if not stacked - size = max(sizes) + # 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 weakref.proxy(selector) - size += padding + 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": - o = np.vstack(origin) - origin_y = (o[:, 1].min() + o[:, 1].max()) / 2 - origin = (limits[0], origin_y) - center = (self.graphics[-1].world_object.world.y - self.graphics[0].world_object.world.y) / 2 - else: - o = np.vstack(origin) - origin_x = (o[:, 0].min() + o[:, 0].max()) / 2 - origin = (origin_x, limits[0]) - center = (self.graphics[-1].world_object.world.x - self.graphics[0].world_object.world.x) / 2 - - return bounds, limits, size, origin, axis, center + 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 = bbox[:, 1].ptp() * 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 = bbox[:, 0].ptp() * 1.5 + # center on orthogonal axis + center = bbox[:, 0].mean() + + return bounds, limits, size, center axes = {"x": 0, "y": 1, "z": 2} From c8bf51565e8f05da6c8b825ca56ea64f2e497ab2 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 02:09:57 -0400 Subject: [PATCH 167/196] fix selector --- fastplotlib/graphics/selectors/_linear_region.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index c792abd80..ecc67b885 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -65,7 +65,7 @@ def __init__( self, selection: Sequence[float], limits: Sequence[float], - size: int, + size: float, center: float, axis: str = "x", parent: Graphic = None, @@ -96,7 +96,7 @@ def __init__( height or width of the selector center: float - center offset of the selector, by default the data mean + center offset of the selector on the orthogonal axis, by default the data mean axis: str, default "x" "x" | "y", axis the selected can move on @@ -291,7 +291,7 @@ def get_selected_data( # slices n_datapoints dim data_selections.append(g.data[s]) - # return source[:].data[s] + return source.data[s] else: if ixs.size == 0: # empty selection From 67dfd00180fce7b8b369a18d838f4622c7709241 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 02:10:17 -0400 Subject: [PATCH 168/196] add plot area hook for collections --- fastplotlib/graphics/_collection_base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index 8f96159e5..8f3a3f017 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -299,6 +299,12 @@ def remove_event_handler(self, callback, *types): """remove an event handler""" self[:].remove_event_handler(callback, *types) + def _fpl_add_plot_area_hook(self, plot_area): + super()._fpl_add_plot_area_hook(plot_area) + + for g in self: + g._fpl_add_plot_area_hook(plot_area) + def __getitem__(self, key) -> CollectionIndexer: if np.issubdtype(type(key), np.integer): addr = self._graphics[key] @@ -351,7 +357,7 @@ def __init__(self, selection: np.ndarray[Graphic], feature: str): self._feature_instances = [getattr(g, feature) for g in self._selection] def __getitem__(self, item): - return [fi[item] for fi in self._feature_instances] + return np.stack([fi[item] for fi in self._feature_instances]) def __setitem__(self, key, value): for fi in self._feature_instances: From ea3cb02335ac4e9df58799dcc18a81a6a1ad8a2e Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 02:10:46 -0400 Subject: [PATCH 169/196] docstring --- fastplotlib/graphics/selectors/_linear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index f73aff606..22ba96a28 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -82,7 +82,7 @@ def __init__( "x" | "y", the axis which the slider can move along center: float - center of selector along the opposite axis of `axis` + center offset of the selector on the orthogonal axis, by default the data mean parent: Graphic parent graphic for this LineSelector From f8518ee8f83d0beb144328903cb6362bf63b7a28 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 02:12:20 -0400 Subject: [PATCH 170/196] update selector nbs --- .../notebooks/linear_region_selector.ipynb | 61 ++----------------- examples/notebooks/linear_selector.ipynb | 2 - 2 files changed, 5 insertions(+), 58 deletions(-) diff --git a/examples/notebooks/linear_region_selector.ipynb b/examples/notebooks/linear_region_selector.ipynb index cbe845f71..57a72bdec 100644 --- a/examples/notebooks/linear_region_selector.ipynb +++ b/examples/notebooks/linear_region_selector.ipynb @@ -113,14 +113,6 @@ "np.clip(-0.1, 0, 10)" ] }, - { - "cell_type": "markdown", - "id": "1588a89e-1da4-4ada-92e2-7437ba942065", - "metadata": {}, - "source": [ - "### However, for the y-axis line we have passed a 2D array where we've used a linspace, so there is not a 1-1 mapping from the data to the line geometry positions. Use `get_selected_indices()` to get the indices of the data bounded by the current selection. In addition the position of the Graphic is not `(0, 0)`. You must use `get_selected_indices()` whenever you want the indices of the selected data." - ] - }, { "cell_type": "code", "execution_count": null, @@ -128,7 +120,7 @@ "metadata": {}, "outputs": [], "source": [ - "ls_y.selection()" + "ls_y.selection" ] }, { @@ -174,17 +166,18 @@ " subplot.add_line(zoomed_init, name=\"zoomed\")\n", "\n", "\n", + "@selector.add_event_handler(\"selection\")\n", "def update_zoomed_subplots(ev):\n", " \"\"\"update the zoomed subplots\"\"\"\n", - " zoomed_data = selector.get_selected_data()\n", + " zoomed_data = ev.get_selected_data()\n", " \n", " for i in range(len(zoomed_data)):\n", + " # interpolate y-vals\n", " data = interpolate(zoomed_data[i], axis=1)\n", - " fig_stack[i + 1, 0][\"zoomed\"].data = data\n", + " fig_stack[i + 1, 0][\"zoomed\"].data[:, 1] = data\n", " fig_stack[i + 1, 0].auto_scale()\n", "\n", "\n", - "selector.selection.add_event_handler(update_zoomed_subplots)\n", "fig_stack.show()" ] }, @@ -196,50 +189,6 @@ "# Large line stack with selector" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "d5ffb678-c989-49ee-85a9-4fd7822f033c", - "metadata": {}, - "outputs": [], - "source": [ - "import fastplotlib as fpl\n", - "import numpy as np\n", - "\n", - "# data to plot\n", - "xs = np.linspace(0, 250, 10_000)\n", - "sine = np.sin(xs) * 20\n", - "cosine = np.cos(xs) * 20\n", - "\n", - "fig_stack_large = fpl.Figure((1, 2))\n", - "\n", - "# sines and cosines\n", - "sines = [sine] * 1_00\n", - "cosines = [cosine] * 1_00\n", - "\n", - "# make line stack\n", - "line_stack = fig_stack_large[0, 0].add_line_stack(sines + cosines, separation=50)\n", - "\n", - "# make selector\n", - "stack_selector = line_stack.add_linear_region_selector(padding=200)\n", - "\n", - "zoomed_line_stack = fig_stack_large[0, 1].add_line_stack([zoomed_init] * 2_000, separation=50, name=\"zoomed\")\n", - " \n", - "def update_zoomed_stack(ev):\n", - " \"\"\"update the zoomed subplots\"\"\"\n", - " zoomed_data = stack_selector.get_selected_data()\n", - " \n", - " for i in range(len(zoomed_data)):\n", - " data = interpolate(zoomed_data[i], axis=1)\n", - " zoomed_line_stack.graphics[i].data = data\n", - " \n", - " fig_stack_large[0, 1].auto_scale()\n", - "\n", - "\n", - "stack_selector.selection.add_event_handler(update_zoomed_stack)\n", - "fig_stack_large.show()" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/examples/notebooks/linear_selector.ipynb b/examples/notebooks/linear_selector.ipynb index ee590f20b..bac8df182 100644 --- a/examples/notebooks/linear_selector.ipynb +++ b/examples/notebooks/linear_selector.ipynb @@ -113,8 +113,6 @@ "for i, c in enumerate(colors):\n", " sel = sine_stack.add_linear_selector(i * 100, color=c, name=str(i))\n", " selectors.append(sel)\n", - " \n", - "ss = Synchronizer(*selectors)\n", "\n", "fig.show()" ] From b3792821ad7dca9d7f0b50a8061cc9fb2054809a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 02:33:59 -0400 Subject: [PATCH 171/196] update example nb --- examples/notebooks/heatmap.ipynb | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/examples/notebooks/heatmap.ipynb b/examples/notebooks/heatmap.ipynb index 90c07a3cb..7de3af2a0 100644 --- a/examples/notebooks/heatmap.ipynb +++ b/examples/notebooks/heatmap.ipynb @@ -5,9 +5,7 @@ "id": "d8c90f4b-b635-4027-b7d5-080d77bd40a3", "metadata": {}, "source": [ - "# The `HeatmapGraphic` is useful for looking at very large arrays\n", - "\n", - "`ImageGraphic` is limited to a max size of `8192 x 8192`" + "# Looking at very large arrays" ] }, { @@ -40,13 +38,11 @@ }, "outputs": [], "source": [ - "xs = np.linspace(0, 50, 10_000)\n", - "\n", - "sine_data = np.sin(xs)\n", + "xs = np.linspace(0, 1_000, 20_000)\n", "\n", - "cosine_data = np.cos(xs)\n", + "sine = np.sin(np.sqrt(xs))\n", "\n", - "data = np.vstack([(sine_data, cosine_data) for i in range(5)])" + "data = np.vstack([sine * i for i in range(10_000)])" ] }, { @@ -72,7 +68,7 @@ "source": [ "fig = fpl.Figure()\n", "\n", - "fig[0, 0].add_heatmap(data, cmap=\"viridis\")\n", + "fig[0, 0].add_image(data, cmap=\"viridis\")\n", "\n", "fig.show(maintain_aspect=False)" ] From 7eba849fbbd80a601336d89c8bc768d6aba83656 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 02:41:30 -0400 Subject: [PATCH 172/196] update nb --- examples/notebooks/lines_cmap.ipynb | 30 +++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/examples/notebooks/lines_cmap.ipynb b/examples/notebooks/lines_cmap.ipynb index dbcbb3e16..3ceb25326 100644 --- a/examples/notebooks/lines_cmap.ipynb +++ b/examples/notebooks/lines_cmap.ipynb @@ -39,11 +39,11 @@ "xs = np.linspace(-10, 10, 100)\n", "# sine wave\n", "ys = np.sin(xs)\n", - "sine = np.dstack([xs, ys])[0]\n", + "sine = np.column_stack([xs, ys])\n", "\n", "# cosine wave\n", "ys = np.cos(xs)\n", - "cosine = np.dstack([xs, ys])[0]" + "cosine = np.column_stack([xs, ys])" ] }, { @@ -107,6 +107,14 @@ "plot_test(\"lines-cmap-jet\", fig)" ] }, + { + "cell_type": "markdown", + "id": "13c1c034-2b3b-4568-b979-7c0bbea698ae", + "metadata": {}, + "source": [ + "map colors from sine data values by setting the cmap transform" + ] + }, { "cell_type": "code", "execution_count": null, @@ -116,7 +124,7 @@ }, "outputs": [], "source": [ - "fig[0, 0].graphics[0].cmap.values = sine[:, 1]" + "fig[0, 0].graphics[0].cmap.transform = sine[:, 1]" ] }, { @@ -141,7 +149,8 @@ }, "outputs": [], "source": [ - "fig[0, 0].graphics[0].cmap.values = cosine[:, 1]" + "# set transform from cosine\n", + "fig[0, 0].graphics[0].cmap.transform = cosine[:, 1]" ] }, { @@ -166,6 +175,7 @@ }, "outputs": [], "source": [ + "# change cmap\n", "fig[0, 0].graphics[0].cmap = \"viridis\"" ] }, @@ -182,6 +192,14 @@ "plot_test(\"lines-cmap-viridis\", fig)" ] }, + { + "cell_type": "markdown", + "id": "1f52bfdc-8151-4bab-973c-1bac36011802", + "metadata": {}, + "source": [ + "use cmap transform to map for a qualitative transform" + ] + }, { "cell_type": "code", "execution_count": null, @@ -191,7 +209,7 @@ }, "outputs": [], "source": [ - "cmap_values = [0] * 25 + [1] * 5 + [2] * 50 + [3] * 20" + "cmap_transform = [0] * 25 + [1] * 5 + [2] * 50 + [3] * 20" ] }, { @@ -203,7 +221,7 @@ }, "outputs": [], "source": [ - "fig[0, 0].graphics[0].cmap.values = cmap_values" + "fig[0, 0].graphics[0].cmap.transform = cmap_transform" ] }, { From 7280cf16abf520db5a3327941507d249d9952dea Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 02:50:00 -0400 Subject: [PATCH 173/196] cleanup --- fastplotlib/layouts/_plot_area.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 5ebc82c87..d8e0adebc 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -566,11 +566,6 @@ def _add_or_insert_graphic( # if we don't use the weakref above, then the object lingers if a plot hook is used! graphic._fpl_add_plot_area_hook(self) - # need to also hook in individual graphics in a collection to plot area - if isinstance(graphic, GraphicCollection): - for g in graphic.graphics: - g._fpl_add_plot_area_hook(self) - def _check_graphic_name_exists(self, name): if name in self: raise ValueError( From 933a3e8f859af6b27a97f69a684fac1c77457ea4 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 03:00:00 -0400 Subject: [PATCH 174/196] update nb --- examples/notebooks/quickstart.ipynb | 102 +++++----------------------- 1 file changed, 18 insertions(+), 84 deletions(-) diff --git a/examples/notebooks/quickstart.ipynb b/examples/notebooks/quickstart.ipynb index 9bfd822ab..5c5040418 100644 --- a/examples/notebooks/quickstart.ipynb +++ b/examples/notebooks/quickstart.ipynb @@ -156,7 +156,9 @@ }, "outputs": [], "source": [ - "image_graphic.data().shape" + "# some graphic properties behave like arrays\n", + "# access the underlying array using .values\n", + "image_graphic.data.value.shape" ] }, { @@ -209,8 +211,8 @@ }, "outputs": [], "source": [ - "image_graphic.cmap.vmin = 50\n", - "image_graphic.cmap.vmax = 150" + "image_graphic.vmin = 50\n", + "image_graphic.vmax = 150" ] }, { @@ -301,7 +303,7 @@ }, "outputs": [], "source": [ - "image_graphic.cmap.reset_vmin_vmax()" + "image_graphic.reset_vmin_vmax()" ] }, { @@ -500,7 +502,7 @@ }, "outputs": [], "source": [ - "fig_rgb[0, 0][\"rgb-image\"].cmap.vmin = 100" + "fig_rgb[0, 0][\"rgb-image\"].vmin = 100" ] }, { @@ -893,72 +895,6 @@ "plot_test(\"lines-data\", fig_lines)" ] }, - { - "cell_type": "markdown", - "id": "3f6d264b-1b03-407e-9d83-cd6cfb02e706", - "metadata": {}, - "source": [ - "### Toggle the presence of a graphic within the scene" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fcba75b7-9a1e-4aae-9dec-715f7f7456c3", - "metadata": {}, - "outputs": [], - "source": [ - "sinc_graphic.present = False" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "763b9943-53a4-4e2a-b47a-4e9e5c9d7be3", - "metadata": {}, - "outputs": [], - "source": [ - "sinc_graphic.present = True" - ] - }, - { - "cell_type": "markdown", - "id": "86f4e535-ce88-415a-b8d2-53612a2de7b9", - "metadata": {}, - "source": [ - "### You can create callbacks to this too, for example to re-scale the plot w.r.t. graphics that are present in the scene" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "64a20a16-75a5-4772-a849-630ade9be4ff", - "metadata": {}, - "outputs": [], - "source": [ - "sinc_graphic.present.add_event_handler(subplot.auto_scale)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fb093046-c94c-4085-86b4-8cd85cb638ff", - "metadata": {}, - "outputs": [], - "source": [ - "sinc_graphic.present = False" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f05981c3-c768-4631-ae62-6a8407b20c4c", - "metadata": {}, - "outputs": [], - "source": [ - "sinc_graphic.present = True" - ] - }, { "cell_type": "markdown", "id": "05f93e93-283b-45d8-ab31-8d15a7671dd2", @@ -978,12 +914,13 @@ "source": [ "img = iio.imread(\"imageio:camera.png\")\n", "\n", - "subplot.add_image(img[::20, ::20], name=\"image\", cmap=\"gray\")\n", + "subplot.add_image(\n", + " img[::20, ::20],\n", + " name=\"image\",\n", + " cmap=\"gray\",\n", + ")\n", "\n", - "# z axis position -1 so it is below all the lines\n", - "subplot[\"image\"].position_z = -1\n", - "subplot[\"image\"].position_x = -8\n", - "subplot[\"image\"].position_y = -8" + "subplot[\"image\"].offset = (-12, -10, -1)" ] }, { @@ -1282,11 +1219,8 @@ "magnetic_vectors = [np.array([[0, 0, z], [0, y, z]]) for (y, z) in zip(m_ys[::10], zs[::10])]\n", "\n", "# add as a line collection\n", - "fig_em[0, 0].add_line_collection(electric_vectors, colors=\"blue\", thickness=1.5, name=\"e-vec\", z_offset=0)\n", - "fig_em[0, 0].add_line_collection(magnetic_vectors, colors=\"red\", thickness=1.5, name=\"m-vec\", z_offset=0)\n", - "# note that the z_offset in `add_line_collection` is not data-related\n", - "# it is the z-offset for where to place the *graphic*, by default with Orthographic cameras (i.e. 2D views)\n", - "# it will increment by 1 for each line in the collection, we want to disable this so set z_position=0\n", + "fig_em[0, 0].add_line_collection(electric_vectors, colors=\"blue\", thickness=1.5, name=\"e-vec\")\n", + "fig_em[0, 0].add_line_collection(magnetic_vectors, colors=\"red\", thickness=1.5, name=\"m-vec\")\n", "\n", "# axes are a WIP, just draw a white line along z for now\n", "z_axis = np.array([[0, 0, 0], [0, 0, stop]])\n", @@ -1537,8 +1471,8 @@ "source": [ "def update_points(subplot):\n", " # move every point by a small amount\n", - " deltas = np.random.normal(size=scatter_graphic.data().shape, loc=0, scale=0.15)\n", - " scatter_graphic.data = scatter_graphic.data() + deltas \n", + " deltas = np.random.normal(size=scatter_graphic.data.value.shape, loc=0, scale=0.15)\n", + " scatter_graphic.data = scatter_graphic.data[:] + deltas\n", "\n", "subplot_scatter.add_animations(update_points)" ] @@ -2048,7 +1982,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.3" } }, "nbformat": 4, From 3a53ac4f484dfc611a6c27c9d795e53b68fb4f8b Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 03:31:12 -0400 Subject: [PATCH 175/196] update nbs --- .../notebooks/scatter_sizes_animation.ipynb | 22 +++----- examples/notebooks/subplots.ipynb | 54 ++++++++++--------- examples/notebooks/subplots_simple.ipynb | 41 ++------------ 3 files changed, 41 insertions(+), 76 deletions(-) diff --git a/examples/notebooks/scatter_sizes_animation.ipynb b/examples/notebooks/scatter_sizes_animation.ipynb index 9ca067bee..0cd301fb1 100644 --- a/examples/notebooks/scatter_sizes_animation.ipynb +++ b/examples/notebooks/scatter_sizes_animation.ipynb @@ -17,16 +17,17 @@ "size_delta_scales = np.array([10, 40, 100], dtype=np.float32)\n", "min_sizes = 6\n", "\n", + "\n", "def update_positions(subplot):\n", - " current_time = time()\n", - " newPositions = points + np.sin(((current_time / 4) % 1)*np.pi)\n", - " subplot.graphics[0].data = newPositions\n", + " g = subplot.graphics[0]\n", + " g.data[:, :-1] += np.sin(((time() / 4))*np.pi)\n", + "\n", "\n", "def update_sizes(subplot):\n", - " current_time = time()\n", - " sin_sample = np.sin(((current_time / 4) % 1)*np.pi)\n", - " size_delta = sin_sample*size_delta_scales\n", - " subplot.graphics[0].sizes = min_sizes + size_delta\n", + " sin_sample = np.abs(np.sin((time() / 1)*np.pi))\n", + " size_delta = sin_sample * size_delta_scales\n", + " subplot.graphics[0].sizes = size_delta\n", + "\n", "\n", "scatter = fig[0, 0].add_scatter(points, colors=[\"red\", \"green\", \"blue\"], sizes=12)\n", "fig[0, 0].add_animations(update_positions, update_sizes)\n", @@ -34,13 +35,6 @@ "fig[0, 0].camera.width = 12\n", "fig.show(autoscale=False)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/notebooks/subplots.ipynb b/examples/notebooks/subplots.ipynb index 72b4b3007..c9774029f 100644 --- a/examples/notebooks/subplots.ipynb +++ b/examples/notebooks/subplots.ipynb @@ -136,39 +136,51 @@ "metadata": {}, "outputs": [], "source": [ - "fig[\"subplot0\"][\"rand-image\"].cmap.vmin = 0.6\n", - "fig[\"subplot0\"][\"rand-image\"].cmap.vmax = 0.8" + "fig[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", + "fig[\"subplot0\"][\"rand-image\"].vmax = 0.8" ] }, { "cell_type": "markdown", + "id": "39c8a5acbad7980b", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "If they are not named use .graphics" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "id": "d27af25002237db5", + "metadata": { + "collapsed": false, + "is_executing": true, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "fig[\"subplot0\"].graphics" - ], - "metadata": { - "collapsed": false, - "is_executing": true - } + ] }, { "cell_type": "markdown", + "id": "2299a8ae23e39c37", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "### positional indexing also works" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", @@ -177,17 +189,9 @@ "metadata": {}, "outputs": [], "source": [ - "fig[1, 0][\"rand-image\"].cmap.vim = 0.1\n", - "fig[1, 0][\"rand-image\"].cmap.vmax = 0.3" + "fig[1, 0][\"rand-image\"].vim = 0.1\n", + "fig[1, 0][\"rand-image\"].vmax = 0.3" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a61e34a5-ee1b-4abb-8718-ec4715ffaa52", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/notebooks/subplots_simple.ipynb b/examples/notebooks/subplots_simple.ipynb index e519584d3..9ff4e4284 100644 --- a/examples/notebooks/subplots_simple.ipynb +++ b/examples/notebooks/subplots_simple.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "5171a06e-1bdc-4908-9726-3c1fd45dbb9d", "metadata": { "ExecuteTime": { @@ -19,40 +19,7 @@ }, "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "301d76bd4c5c42c7912cdd28651e2899", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Unable to find extension: VK_EXT_swapchain_colorspace\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Available devices:\n", - "✅ (default) | AMD RADV POLARIS10 (ACO) | DiscreteGPU | Vulkan | Mesa 20.3.5 (ACO)\n", - "❗ | llvmpipe (LLVM 11.0.1, 256 bits) | CPU | Vulkan | Mesa 20.3.5 (LLVM 11.0.1)\n", - "✅ | NVIDIA GeForce RTX 3080 | DiscreteGPU | Vulkan | 530.30.02\n", - "❗ | Radeon RX 570 Series (POLARIS10, DRM 3.40.0, 5.10.0-21-amd64, LLVM 11.0.1) | Unknown | OpenGL | \n" - ] - } - ], + "outputs": [], "source": [ "import numpy as np\n", "import fastplotlib as fpl" @@ -165,7 +132,7 @@ }, "outputs": [], "source": [ - "fig[0, 1].graphics[0].cmap.vmax = 0.5" + "fig[0, 1].graphics[0].vmax = 0.5" ] }, { @@ -244,7 +211,7 @@ }, "outputs": [], "source": [ - "fig[\"top-right-plot\"][\"rand-img\"].cmap.vmin = 0.5" + "fig[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" ] }, { From a63490716e5939d75fad42f969bcd34de0be73be Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 03:33:07 -0400 Subject: [PATCH 176/196] feature is private class attr --- fastplotlib/graphics/_base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 6370fe720..bafa0bcf3 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -384,9 +384,8 @@ def _fpl_cleanup(self): self.world_object._event_handlers.clear() - feature_names = getattr(self, "feature_events") - for n in feature_names: - fea = getattr(self, n) + for n in self._features: + fea = getattr(self, f"_{n}") fea.clear_event_handlers() def __del__(self): From a8d350a9825a28a763001728c75b5a0cf03851ca Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 03:34:23 -0400 Subject: [PATCH 177/196] fix --- fastplotlib/graphics/line_collection.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index f3f337650..aaa20fa78 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -78,6 +78,9 @@ def cmap(self) -> CollectionFeature: @cmap.setter def cmap(self, args): + if isinstance(args, str): + name = args + transform, alpha = None, 1.0 if len(args) == 1: name = args[0] transform, alpha = None, None From b6665f8b70460bed2012fb2888ba4623f3c98ef9 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 03:37:48 -0400 Subject: [PATCH 178/196] remove anim example from screenshot tests --- examples/desktop/line_collection/line_stack_3d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/desktop/line_collection/line_stack_3d.py b/examples/desktop/line_collection/line_stack_3d.py index abf989a17..41914e2d2 100644 --- a/examples/desktop/line_collection/line_stack_3d.py +++ b/examples/desktop/line_collection/line_stack_3d.py @@ -4,7 +4,7 @@ Example showing a 3D stack of lines with animations """ -# test_example = true +# test_example = false import numpy as np import fastplotlib as fpl From 2e8e612311cb325d2352ffa3dee4342e31db14d7 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 03:39:21 -0400 Subject: [PATCH 179/196] update screenshots --- examples/desktop/screenshots/heatmap.png | 4 ++-- examples/desktop/screenshots/heatmap_cmap.png | 4 ++-- examples/desktop/screenshots/heatmap_data.png | 4 ++-- examples/desktop/screenshots/heatmap_square.png | 3 +++ examples/desktop/screenshots/heatmap_vmin_vmax.png | 4 ++-- examples/desktop/screenshots/heatmap_wide.png | 3 +++ examples/desktop/screenshots/line_collection_slicing.png | 3 +++ examples/desktop/screenshots/line_dataslice.png | 4 ++-- examples/desktop/screenshots/line_stack.png | 4 ++-- 9 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 examples/desktop/screenshots/heatmap_square.png create mode 100644 examples/desktop/screenshots/heatmap_wide.png create mode 100644 examples/desktop/screenshots/line_collection_slicing.png diff --git a/examples/desktop/screenshots/heatmap.png b/examples/desktop/screenshots/heatmap.png index a8c8b73fe..ec6cf9955 100644 --- a/examples/desktop/screenshots/heatmap.png +++ b/examples/desktop/screenshots/heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5620e4dcb964dbf3318ac77e566af395a35b9762e0687dec2e1a2864eb291fd3 -size 102994 +oid sha256:754bd8713617bf61d1adf57b3e84c1257b038bf15412aa3c8bd466d1405086e7 +size 48524 diff --git a/examples/desktop/screenshots/heatmap_cmap.png b/examples/desktop/screenshots/heatmap_cmap.png index cee81dd30..c495cf72c 100644 --- a/examples/desktop/screenshots/heatmap_cmap.png +++ b/examples/desktop/screenshots/heatmap_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8863461569f5b89d1443e3051a5512f3987487fcb9e057215d2f030a180fa09f -size 97996 +oid sha256:d2ba0b76e982ceb1439c5ebaabfaf089ea9b09e50934718eaaa29d7492272196 +size 42746 diff --git a/examples/desktop/screenshots/heatmap_data.png b/examples/desktop/screenshots/heatmap_data.png index 316a73753..229d6c2cc 100644 --- a/examples/desktop/screenshots/heatmap_data.png +++ b/examples/desktop/screenshots/heatmap_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a975179e82893dbb04e4674310761e7b02bb62ae6abb1b89397720bddf96ae5f -size 19084 +oid sha256:a7160c4f034214f8052a6d88001dac706b0a85a5a4df076958ba1a176344b85a +size 53854 diff --git a/examples/desktop/screenshots/heatmap_square.png b/examples/desktop/screenshots/heatmap_square.png new file mode 100644 index 000000000..00a01133e --- /dev/null +++ b/examples/desktop/screenshots/heatmap_square.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d01171b2bd05b5c88df4312c303094fdede36b1cf930455ace6d1fb12d8eb36 +size 81274 diff --git a/examples/desktop/screenshots/heatmap_vmin_vmax.png b/examples/desktop/screenshots/heatmap_vmin_vmax.png index 357683d82..b028291f7 100644 --- a/examples/desktop/screenshots/heatmap_vmin_vmax.png +++ b/examples/desktop/screenshots/heatmap_vmin_vmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9592f3724016db1b7431bc100b16bec175e197c111e7b442dc2255d51da3f5e8 -size 114957 +oid sha256:61c3754de3a7e6622ce1a77dbbf9bbd6ccfd3ccad3b1463b009bf93511258034 +size 44426 diff --git a/examples/desktop/screenshots/heatmap_wide.png b/examples/desktop/screenshots/heatmap_wide.png new file mode 100644 index 000000000..927b933d6 --- /dev/null +++ b/examples/desktop/screenshots/heatmap_wide.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:271e0d769b608d0f34a153ab8b8353f1e5d127f239951fc407ccedd3eee5e2e5 +size 82687 diff --git a/examples/desktop/screenshots/line_collection_slicing.png b/examples/desktop/screenshots/line_collection_slicing.png new file mode 100644 index 000000000..ba4170874 --- /dev/null +++ b/examples/desktop/screenshots/line_collection_slicing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01090d611117fd0d2b3f9971e359871c9598a634a1829e74848b1c78a770d437 +size 131764 diff --git a/examples/desktop/screenshots/line_dataslice.png b/examples/desktop/screenshots/line_dataslice.png index 0863751bf..e55a6111e 100644 --- a/examples/desktop/screenshots/line_dataslice.png +++ b/examples/desktop/screenshots/line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78ccd51d1891fb6a345cb2885a341f276d8ad7a6fa506deda6cae6ef14c64094 -size 45843 +oid sha256:e76275ea6f5719e16ff0ef3401dc33fe4b70c4c9010b3b673fca26812f33b9e8 +size 46400 diff --git a/examples/desktop/screenshots/line_stack.png b/examples/desktop/screenshots/line_stack.png index c13f05f04..29d941fd4 100644 --- a/examples/desktop/screenshots/line_stack.png +++ b/examples/desktop/screenshots/line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5480aefe6e723863b919a4eeb4755310fe7036b27beb8e2e2402e04943ee8c1e -size 201102 +oid sha256:73226917c233f3fd3d7ec0b40d5a7ded904d275c871242dc0578bddf4c19d0bd +size 93687 From af6ab4f05caaca46916aca2d6ff6f9c3e3129663 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 03:43:27 -0400 Subject: [PATCH 180/196] update screenshot --- examples/notebooks/screenshots/nb-lines-underlay.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png index d4b3d9f6d..e7f6aeb0c 100644 --- a/examples/notebooks/screenshots/nb-lines-underlay.png +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b12c8f29436be8d17c38f420120ab3d54b0eee9bef751eea2f99d01b1a8fa43 -size 50761 +oid sha256:8ed174c362c7e7c491eba02b32102f59423af41537577c694fdcd54d69c065b3 +size 50422 From 47cecfa300d0e712418ae4c72a2944f37eff2105 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 03:52:17 -0400 Subject: [PATCH 181/196] update CONTRIBUTING --- CONTRIBUTING.md | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe9c90242..0786596b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,35 +77,37 @@ keeps a *private* global dictionary of all `WorldObject` instances and users are This is due to garbage collection. This may be quite complicated for beginners, for more details see this PR: https://github.com/fastplotlib/fastplotlib/pull/160 . If you are curious or have more questions on garbage collection in fastplotlib you're welcome to post an issue :D. -#### Graphic Features +#### Graphic properties -There is one important thing that `fastplotlib` uses which we call "graphic features". +Graphic properties are all evented, and internally we called these "graphic features". They are the various +aspects of a graphic that the user can change. The "graphic features" subpackage can be found at `fastplotlib/graphics/_features`. As we can see this -is a private subpackage and never meant to be accessible to users. In `fastplotlib` "graphic features" are the various -aspects of a graphic that the user can change. Users can also run callbacks whenever a graphic feature changes. +is a private subpackage and never meant to be accessible to users.. ##### LineGraphic For example let's look at `LineGraphic` in `fastplotlib/graphics/line.py`. Every graphic has a class variable called -`feature_events` which is a set of all graphic features. It has the following graphic features: "data", "colors", "cmap", "thickness", "present". +`_features` which is a set of all graphic properties that are evented. It has the following evented properties: +`"data", "colors", "cmap", "thickness"` in addition to properties common to all graphics, such as `"name", "offset", "rotation", and "visible"` -Now look at the constructor for `LineGraphic`, it first creates an instance of `PointsDataFeature`. This is basically a -class that wraps the positions buffer, the vertex positions that define the line, and provides additional useful functionality. -For example, every time that the `data` is changed event handlers will be called (if any event handlers are registered). +Now look at the constructor for the `LineGraphic` base class `PositionsGraphic`, it first creates an instance of `VertexPositions`. +This is a class that manages vertex positions buffer. It defines the line, and provides additional useful functionality. +For example, every time that the `data` is changed, the new data will be marked for upload to the GPU before the next draw. +In addition, event handlers will be called if any event handlers are registered. -`ColorFeature`behaves similarly, but it can perform additional parsing that can create the colors buffer from different forms of user input. For example if a user runs: -`line_graphic.colors = "blue"`, then `ColorFeature.__setitem__()` will create a buffer that corresponds to what `pygfx.Color` thinks is "blue". -Users can also take advantage of fancy indexing, ex: `line_graphics.colors[bool_array] = "red"` :smile: +`VertexColors`behaves similarly, but it can perform additional parsing that can create the colors buffer from different +forms of user input. For example if a user runs: `line_graphic.colors = "blue"`, then `VertexColors.__setitem__()` will +create a buffer that corresponds to what `pygfx.Color` thinks is "blue". Users can also take advantage of fancy indexing, +ex: `line_graphics.colors[bool_array] = "red"` :smile: -`LineGraphic` also has a `CmapFeature`, this is a subclass of `ColorFeature` which can parse colormaps, for example: +`LineGraphic` also has a `VertexCmap`, this manages the line `VertexColors` instance to parse colormaps, for example: `line_graphic.cmap = "jet"` or even `line_graphic.cmap[50:] = "viridis"`. -`LineGraphic` also has `ThicknessFeature` which is pretty simple, `PresentFeature` which indicates if a graphic is -currently in the scene, and `DeletedFeature` which is useful if you need callbacks to indicate that the graphic has been -deleted (for example, removing references to a graphic from a legend). +`LineGraphic` also has a `thickness` property which is pretty simple, and `DeletedFeature` which is useful if you need +callbacks to indicate that the graphic has been deleted (for example, removing references to a graphic from a legend). -Other graphics have graphic features that are relevant to them, for example `ImageGraphic` has a `cmap` feature which is -unique to images or heatmaps. +Other graphics have properties that are relevant to them, for example `ImageGraphic` has `cmap`, `vmin`, `vmax`, +properties unique to images. #### Selectors @@ -192,9 +194,10 @@ the subplots. All subplots within a `Figure` share the same canvas and use diffe ## Tests in detail -The CI pipeline for a plotting library that is supposed to produce things that "look visually correct". Each example -within the `examples` dir is run and an image of the canvas is taken and compared with a ground-truth -screenshot that we have manually inspected. Ground-truth image are stored using `git-lfs`. +Backend tests are in `tests/`, in addition as a plotting library CI pipeline produces things that +"look visually correct". Each example within the `examples` dir is run and an image of the canvas +is taken and compared with a ground-truth screenshot that we have manually inspected. +Ground-truth image are stored using `git-lfs`. The ground-truth images are in: From 3079fa633644456bce53bed004c715e94f3da584 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 04:43:39 -0400 Subject: [PATCH 182/196] smaller hm data test for CI --- examples/desktop/heatmap/heatmap_data.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/desktop/heatmap/heatmap_data.py b/examples/desktop/heatmap/heatmap_data.py index 9e914be9b..20a99e0f0 100644 --- a/examples/desktop/heatmap/heatmap_data.py +++ b/examples/desktop/heatmap/heatmap_data.py @@ -12,11 +12,11 @@ fig = fpl.Figure() -xs = np.linspace(0, 1_000, 10_000) +xs = np.linspace(0, 1_000, 9_000) sine = np.sin(np.sqrt(xs)) -data = np.vstack([sine * i for i in range(20_000)]) +data = np.vstack([sine * i for i in range(9_000)]) # plot the image data img = fig[0, 0].add_image(data=data, name="heatmap") @@ -26,10 +26,10 @@ fig.canvas.set_logical_size(1500, 1500) fig[0, 0].auto_scale() -cosine = np.cos(np.sqrt(xs)) +cosine = np.cos(np.sqrt(xs)[:3000]) -# change first 10,000 rows and 9,000 columns -img.data[:10_000, :9000] = np.vstack([cosine[:9000] * i * 2 for i in range(10_000)]) +# change first 2,000 rows and 3,000 columns +img.data[:2_000, :3_000] = np.vstack([cosine * i * 4 for i in range(2_000)]) if __name__ == "__main__": print(__doc__) From d719ad6ae8aef3f36e4a5002c0bcf8facfb3b6e0 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 04:50:44 -0400 Subject: [PATCH 183/196] exclude heatmap change data from tests, too large RAM usage probably --- examples/desktop/heatmap/heatmap_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/desktop/heatmap/heatmap_data.py b/examples/desktop/heatmap/heatmap_data.py index 20a99e0f0..449b122ce 100644 --- a/examples/desktop/heatmap/heatmap_data.py +++ b/examples/desktop/heatmap/heatmap_data.py @@ -4,7 +4,7 @@ Change the data of a heatmap """ -# test_example = true +# test_example = false import fastplotlib as fpl import numpy as np From e63043cc63e877401522c01ccefd04122acd888c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 04:52:34 -0400 Subject: [PATCH 184/196] black --- fastplotlib/graphics/__init__.py | 2 +- fastplotlib/graphics/_collection_base.py | 1 + fastplotlib/graphics/_features/__init__.py | 4 +- fastplotlib/graphics/_features/_base.py | 8 ++- fastplotlib/graphics/_positions_base.py | 4 +- fastplotlib/graphics/line.py | 14 ++-- fastplotlib/graphics/line_collection.py | 84 ++++++++++++---------- fastplotlib/graphics/selectors/__init__.py | 5 +- 8 files changed, 70 insertions(+), 52 deletions(-) diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py index 40293fc67..ff96baa4c 100644 --- a/fastplotlib/graphics/__init__.py +++ b/fastplotlib/graphics/__init__.py @@ -11,5 +11,5 @@ "ScatterGraphic", "TextGraphic", "LineCollection", - "LineStack" + "LineStack", ] diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index 8f3a3f017..2d4fe2dc9 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -15,6 +15,7 @@ class CollectionProperties: Allows getting and setting the common properties of the individual graphics in the collection """ + def _set_feature(self, feature, values): if not len(values) == len(self): raise IndexError diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index cf1d0af02..e36de089e 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -35,7 +35,7 @@ from ._common import Name, Offset, Rotation, Visible, Deleted -__all__ =[ +__all__ = [ "VertexColors", "UniformColor", "UniformSize", @@ -61,4 +61,4 @@ "Rotation", "Visible", "Deleted", -] \ No newline at end of file +] diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 6fae28d0b..1b24d3b78 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -260,7 +260,9 @@ def _parse_offset_size( key = np.array(key) if not key.ndim == 1: - raise TypeError(f"can only use 1D arrays for fancy indexing, you have passed a data with: {key.ndim} dimensions") + raise TypeError( + f"can only use 1D arrays for fancy indexing, you have passed a data with: {key.ndim} dimensions" + ) if key.dtype == bool: # convert bool mask to integer indices @@ -268,7 +270,9 @@ def _parse_offset_size( if not np.issubdtype(key.dtype, np.integer): # fancy indexing doesn't make sense with non-integer types - raise TypeError(f"can only using integer or booleans arrays for fancy indexing, your array is of type: {key.dtype}") + raise TypeError( + f"can only using integer or booleans arrays for fancy indexing, your array is of type: {key.dtype}" + ) if key.size < 1: # nothing to update diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 723bd6c8a..b6d505eac 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -100,7 +100,9 @@ def __init__( self._cmap = cmap self._colors = cmap._vertex_colors else: - raise TypeError("`cmap` argument must be a cmap name or an existing `VertexCmap` instance") + raise TypeError( + "`cmap` argument must be a cmap name or an existing `VertexCmap` instance" + ) else: # no cmap given if isinstance(colors, VertexColors): diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 6055c2471..d0a8cc336 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -134,7 +134,9 @@ def add_linear_selector( """ - bounds_init, limits, size, center = self._get_linear_selector_init_args(axis, padding) + bounds_init, limits, size, center = self._get_linear_selector_init_args( + axis, padding + ) if selection is None: selection = bounds_init[0] @@ -161,7 +163,7 @@ def add_linear_region_selector( selection: tuple[float, float] = None, padding: float = 0.0, axis: str = "x", - **kwargs + **kwargs, ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, @@ -188,7 +190,9 @@ def add_linear_region_selector( """ - bounds_init, limits, size, center = self._get_linear_selector_init_args(axis, padding) + bounds_init, limits, size, center = self._get_linear_selector_init_args( + axis, padding + ) if selection is None: selection = bounds_init @@ -214,7 +218,9 @@ def add_linear_region_selector( return weakref.proxy(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]: + 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) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index aaa20fa78..15897cafc 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -92,7 +92,9 @@ def cmap(self, args): elif len(args) == 3: name, transform, alpha = args - colors = parse_cmap_values(n_colors=len(self), cmap_name=name, transform=transform) + colors = parse_cmap_values( + n_colors=len(self), cmap_name=name, transform=transform + ) colors[:, -1] = alpha self.colors = colors @@ -112,6 +114,7 @@ def thickness(self, values: np.ndarray | list[float]): class LineCollectionIndexer(CollectionIndexer, _LineCollectionProperties): """Indexer for line collections""" + pass @@ -120,21 +123,21 @@ class LineCollection(GraphicCollection, _LineCollectionProperties): _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, + 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` @@ -214,7 +217,8 @@ def __init__( 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)}" + f"len(kwargs_lines) != len(data)\n" + f"{len(kwargs_lines)} != {len(data)}" ) self._cmap_transform = cmap_transform @@ -329,7 +333,7 @@ def __init__( ) self.add_graphic(lg) - + def __getitem__(self, item) -> LineCollectionIndexer: return super().__getitem__(item) @@ -361,7 +365,9 @@ def add_linear_selector( """ - bounds_init, limits, size, center = self._get_linear_selector_init_args(axis, padding) + bounds_init, limits, size, center = self._get_linear_selector_init_args( + axis, padding + ) if selection is None: selection = bounds_init[0] @@ -388,7 +394,7 @@ def add_linear_region_selector( selection: tuple[float, float] = None, padding: float = 0.0, axis: str = "x", - **kwargs + **kwargs, ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, @@ -415,7 +421,9 @@ def add_linear_region_selector( """ - bounds_init, limits, size, center = self._get_linear_selector_init_args(axis, padding) + bounds_init, limits, size, center = self._get_linear_selector_init_args( + axis, padding + ) if selection is None: selection = bounds_init @@ -476,22 +484,22 @@ def _get_linear_selector_init_args(self, axis, padding): 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, + 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. @@ -579,7 +587,7 @@ def __init__( line.offset = (line.offset[0], axis_zero, line.offset[2]) axis_zero = ( - axis_zero + line.data.value[:, axes[separation_axis]].max() + separation + axis_zero + line.data.value[:, axes[separation_axis]].max() + separation ) self.separation = separation diff --git a/fastplotlib/graphics/selectors/__init__.py b/fastplotlib/graphics/selectors/__init__.py index af2585437..4f28f571c 100644 --- a/fastplotlib/graphics/selectors/__init__.py +++ b/fastplotlib/graphics/selectors/__init__.py @@ -3,7 +3,4 @@ from ._polygon import PolygonSelector -__all__ = [ - "LinearSelector", - "LinearRegionSelector" -] \ No newline at end of file +__all__ = ["LinearSelector", "LinearRegionSelector"] From 06a489f8c24a28b851d596b3f13348689738be6f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 05:01:34 -0400 Subject: [PATCH 185/196] change dtype to save ram usage for CI --- examples/desktop/heatmap/heatmap.py | 2 +- examples/desktop/heatmap/heatmap_cmap.py | 2 +- examples/desktop/heatmap/heatmap_data.py | 2 +- examples/desktop/heatmap/heatmap_square.py | 3 ++- examples/desktop/heatmap/heatmap_vmin_vmax.py | 2 +- examples/desktop/heatmap/heatmap_wide.py | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/desktop/heatmap/heatmap.py b/examples/desktop/heatmap/heatmap.py index c43f0ba84..f3a1bf460 100644 --- a/examples/desktop/heatmap/heatmap.py +++ b/examples/desktop/heatmap/heatmap.py @@ -12,7 +12,7 @@ fig = fpl.Figure() -xs = np.linspace(0, 1_000, 10_000) +xs = np.linspace(0, 1_000, 10_000, dtype=np.float32) sine = np.sin(np.sqrt(xs)) diff --git a/examples/desktop/heatmap/heatmap_cmap.py b/examples/desktop/heatmap/heatmap_cmap.py index a06f587d6..a5623f7c1 100644 --- a/examples/desktop/heatmap/heatmap_cmap.py +++ b/examples/desktop/heatmap/heatmap_cmap.py @@ -12,7 +12,7 @@ fig = fpl.Figure() -xs = np.linspace(0, 1_000, 10_000) +xs = np.linspace(0, 1_000, 10_000, dtype=np.float32) sine = np.sin(np.sqrt(xs)) diff --git a/examples/desktop/heatmap/heatmap_data.py b/examples/desktop/heatmap/heatmap_data.py index 449b122ce..75ef3ce41 100644 --- a/examples/desktop/heatmap/heatmap_data.py +++ b/examples/desktop/heatmap/heatmap_data.py @@ -12,7 +12,7 @@ fig = fpl.Figure() -xs = np.linspace(0, 1_000, 9_000) +xs = np.linspace(0, 1_000, 9_000, dtype=np.float32) sine = np.sin(np.sqrt(xs)) diff --git a/examples/desktop/heatmap/heatmap_square.py b/examples/desktop/heatmap/heatmap_square.py index 002af81f9..5cb902788 100644 --- a/examples/desktop/heatmap/heatmap_square.py +++ b/examples/desktop/heatmap/heatmap_square.py @@ -12,7 +12,7 @@ fig = fpl.Figure() -xs = np.linspace(0, 1_000, 20_000) +xs = np.linspace(0, 1_000, 20_000, dtype=np.float32) sine = np.sin(np.sqrt(xs)) @@ -21,6 +21,7 @@ # plot the image data img = fig[0, 0].add_image(data=data, name="heatmap") +del data # data no longer needed after given to graphic fig.show() fig.canvas.set_logical_size(1500, 1500) diff --git a/examples/desktop/heatmap/heatmap_vmin_vmax.py b/examples/desktop/heatmap/heatmap_vmin_vmax.py index 58dfc5542..0b11452c4 100644 --- a/examples/desktop/heatmap/heatmap_vmin_vmax.py +++ b/examples/desktop/heatmap/heatmap_vmin_vmax.py @@ -12,7 +12,7 @@ fig = fpl.Figure() -xs = np.linspace(0, 1_000, 10_000) +xs = np.linspace(0, 1_000, 10_000, dtype=np.float32) sine = np.sin(np.sqrt(xs)) diff --git a/examples/desktop/heatmap/heatmap_wide.py b/examples/desktop/heatmap/heatmap_wide.py index f1080b522..baf4bee1a 100644 --- a/examples/desktop/heatmap/heatmap_wide.py +++ b/examples/desktop/heatmap/heatmap_wide.py @@ -12,7 +12,7 @@ fig = fpl.Figure() -xs = np.linspace(0, 1_000, 20_000) +xs = np.linspace(0, 1_000, 20_000, dtype=np.float32) sine = np.sin(np.sqrt(xs)) From b5247992f95c476ec2c9d428628956c44df5c869 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 05:05:05 -0400 Subject: [PATCH 186/196] remove large square heatmap from screenshot tests --- examples/desktop/heatmap/heatmap_square.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/desktop/heatmap/heatmap_square.py b/examples/desktop/heatmap/heatmap_square.py index 5cb902788..f776b74e1 100644 --- a/examples/desktop/heatmap/heatmap_square.py +++ b/examples/desktop/heatmap/heatmap_square.py @@ -4,7 +4,7 @@ square heatmap test """ -# test_example = true +# test_example = false import fastplotlib as fpl import numpy as np From fc5160eb7fc5ac6ce2814dffb25ec89c6d7ee45c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 05:05:20 -0400 Subject: [PATCH 187/196] black --- fastplotlib/widgets/histogram_lut.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index db1161145..a3edffcbd 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -58,10 +58,10 @@ def __init__( center=origin[0], axis="y", edge_thickness=8, - parent=self._histogram_line + parent=self._histogram_line, ) - #there will be a small difference with the histogram edges so this makes them both line up exactly + # there will be a small difference with the histogram edges so this makes them both line up exactly self._linear_region_selector.selection = ( self._image_graphic.vmin, self._image_graphic.vmax, From 2c6d0caf0b019760e461544c45da06dfe57eae88 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 05:10:22 -0400 Subject: [PATCH 188/196] disable all but one hm test --- examples/desktop/heatmap/heatmap_cmap.py | 2 +- examples/desktop/heatmap/heatmap_vmin_vmax.py | 2 +- examples/desktop/heatmap/heatmap_wide.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/desktop/heatmap/heatmap_cmap.py b/examples/desktop/heatmap/heatmap_cmap.py index a5623f7c1..39e697c93 100644 --- a/examples/desktop/heatmap/heatmap_cmap.py +++ b/examples/desktop/heatmap/heatmap_cmap.py @@ -4,7 +4,7 @@ Change the cmap of a heatmap """ -# test_example = true +# test_example = false import fastplotlib as fpl import numpy as np diff --git a/examples/desktop/heatmap/heatmap_vmin_vmax.py b/examples/desktop/heatmap/heatmap_vmin_vmax.py index 0b11452c4..75b6b7b68 100644 --- a/examples/desktop/heatmap/heatmap_vmin_vmax.py +++ b/examples/desktop/heatmap/heatmap_vmin_vmax.py @@ -4,7 +4,7 @@ Change the vmin vmax of a heatmap """ -# test_example = true +# test_example = false import fastplotlib as fpl import numpy as np diff --git a/examples/desktop/heatmap/heatmap_wide.py b/examples/desktop/heatmap/heatmap_wide.py index baf4bee1a..251c25fa4 100644 --- a/examples/desktop/heatmap/heatmap_wide.py +++ b/examples/desktop/heatmap/heatmap_wide.py @@ -4,7 +4,7 @@ Wide example """ -# test_example = true +# test_example = false import fastplotlib as fpl import numpy as np From 3fd77340e09bc0ed5367ea64794aa5ea585fd0eb Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 16:27:51 -0400 Subject: [PATCH 189/196] fix gc --- examples/notebooks/test_gc.ipynb | 24 +++++++++++++++--------- fastplotlib/graphics/_base.py | 9 +++++++++ fastplotlib/graphics/_collection_base.py | 20 ++++++++++++++++++++ 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/examples/notebooks/test_gc.ipynb b/examples/notebooks/test_gc.ipynb index 39f964cf7..57d7bb576 100644 --- a/examples/notebooks/test_gc.ipynb +++ b/examples/notebooks/test_gc.ipynb @@ -93,12 +93,11 @@ "\n", "\n", "for g in objects:\n", - " for feature in g.feature_events:\n", - " if isinstance(g, fpl.LineCollection):\n", - " continue # skip collections for now\n", + " for feature in g._features:\n", + " # if isinstance(g, fpl.LineCollection):?\n", + " # continue # skip collections for now\n", " \n", - " f = getattr(g, feature)\n", - " f.add_event_handler(feature_changed_handler)\n", + " g.add_event_handler(feature_changed_handler, feature)\n", "\n", "fig.show()" ] @@ -136,9 +135,8 @@ "\n", "# add some events onto all the image graphics\n", "for g in iw.managed_graphics:\n", - " for f in g.feature_events:\n", - " fea = getattr(g, f)\n", - " fea.add_event_handler(feature_changed_handler)\n", + " for f in g._features:\n", + " g.add_event_handler(feature_changed_handler, f)\n", "\n", "iw.show()" ] @@ -174,6 +172,14 @@ "source": [ "test_references(old_graphics)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "712bb6ea-7244-4e03-8dfa-9419daa34915", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -192,7 +198,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index bafa0bcf3..46d83cdee 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -282,6 +282,12 @@ def decorator(_callback): return decorator(callback) + def clear_event_handlers(self): + for ev, handlers in self.event_handlers: + handlers = list(handlers) + for h in handlers: + self.remove_event_handler(h, ev) + def _handle_event(self, callback, event: pygfx.Event): """Wrap pygfx event to add graphic to pick_info""" event.graphic = self @@ -358,6 +364,9 @@ def _fpl_cleanup(self): Optionally implemented in subclasses """ + # remove event handlers + self.clear_event_handlers() + # clear any attached event handlers and animation functions for attr in dir(self): try: diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index 2d4fe2dc9..8b6c1778a 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -153,6 +153,10 @@ def remove_event_handler(self, callback, *types): for g in self: g.remove_event_handler(callback, *types) + def clear_event_handlers(self): + for g in self: + g.clear_event_handlers() + def __getitem__(self, item): return self.graphics[item] @@ -300,12 +304,28 @@ def remove_event_handler(self, callback, *types): """remove an event handler""" self[:].remove_event_handler(callback, *types) + def clear_event_handlers(self): + self[:].clear_event_handlers() + def _fpl_add_plot_area_hook(self, plot_area): super()._fpl_add_plot_area_hook(plot_area) for g in self: g._fpl_add_plot_area_hook(plot_area) + def _fpl_cleanup(self): + """ + Cleans up the graphic in preparation for __del__(), such as removing event handlers from + plot renderer, feature event handlers, etc. + + Optionally implemented in subclasses + """ + # clear any attached event handlers and animation functions + self.world_object._event_handlers.clear() + + for g in self: + g._fpl_cleanup() + def __getitem__(self, key) -> CollectionIndexer: if np.issubdtype(type(key), np.integer): addr = self._graphics[key] From 24b5d95c7e69adb95ed0ee3d1bd4351bc219e5a0 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 19:08:35 -0400 Subject: [PATCH 190/196] update screenshots --- examples/desktop/screenshots/gridplot.png | 4 ++-- examples/desktop/screenshots/image_cmap.png | 4 ++-- examples/desktop/screenshots/image_rgb.png | 4 ++-- examples/desktop/screenshots/image_rgbvminvmax.png | 4 ++-- examples/desktop/screenshots/image_simple.png | 4 ++-- examples/desktop/screenshots/image_vminvmax.png | 4 ++-- examples/desktop/screenshots/scatter_cmap.png | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/desktop/screenshots/gridplot.png b/examples/desktop/screenshots/gridplot.png index ebf2d3a97..9e81fe8c6 100644 --- a/examples/desktop/screenshots/gridplot.png +++ b/examples/desktop/screenshots/gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f972f67b8830657ab14899f749fb385a080280304377d8868e6cd39c766a0afd -size 267084 +oid sha256:462a06e9c74dc9f0958aa265349dfac9c31d77a3ab3915f85596c85f2e7a6f3a +size 266056 diff --git a/examples/desktop/screenshots/image_cmap.png b/examples/desktop/screenshots/image_cmap.png index bbf51ab18..be16ba213 100644 --- a/examples/desktop/screenshots/image_cmap.png +++ b/examples/desktop/screenshots/image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:555fd969606d0cb231ac152724f7c9717a2220ce22db663c5e7d5793f828ed34 -size 189654 +oid sha256:552a4d5141a5a87baaedd8a9d7d911dfdddee5792c024c77012665268af865e9 +size 189479 diff --git a/examples/desktop/screenshots/image_rgb.png b/examples/desktop/screenshots/image_rgb.png index 9a5082b12..8d391f07c 100644 --- a/examples/desktop/screenshots/image_rgb.png +++ b/examples/desktop/screenshots/image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95f3cae6caf8d64d1a6b4799df52dc61cc05bd6b6ea465edbec06a9678f32435 -size 218089 +oid sha256:55e76cea92eb34e1e25d730d2533a9a0d845921e78bc980708d320bb353a2d73 +size 218413 diff --git a/examples/desktop/screenshots/image_rgbvminvmax.png b/examples/desktop/screenshots/image_rgbvminvmax.png index 00bbdc0c5..eabe85d28 100644 --- a/examples/desktop/screenshots/image_rgbvminvmax.png +++ b/examples/desktop/screenshots/image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fc06b8cdd72040cf2ffc44cde80d5ae21ca392daac25d79fe175b5865b13552 -size 34894 +oid sha256:7ee27d89170b30a3da7fe6d752961b30e17712d7905d8fa0686f9597debe68f9 +size 34620 diff --git a/examples/desktop/screenshots/image_simple.png b/examples/desktop/screenshots/image_simple.png index 94fcd3061..853eb2f01 100644 --- a/examples/desktop/screenshots/image_simple.png +++ b/examples/desktop/screenshots/image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3dcfb5d48d0e4db920c33ee725e2c66f3c8e04a66e03d283a6481f42a4121a16 -size 190178 +oid sha256:e943bd3b1e00acaed274dd185f5362210e39330e0f541db9ceee489fa0a9a344 +size 189822 diff --git a/examples/desktop/screenshots/image_vminvmax.png b/examples/desktop/screenshots/image_vminvmax.png index 00bbdc0c5..eabe85d28 100644 --- a/examples/desktop/screenshots/image_vminvmax.png +++ b/examples/desktop/screenshots/image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fc06b8cdd72040cf2ffc44cde80d5ae21ca392daac25d79fe175b5865b13552 -size 34894 +oid sha256:7ee27d89170b30a3da7fe6d752961b30e17712d7905d8fa0686f9597debe68f9 +size 34620 diff --git a/examples/desktop/screenshots/scatter_cmap.png b/examples/desktop/screenshots/scatter_cmap.png index 87a6e0ded..560f1942d 100644 --- a/examples/desktop/screenshots/scatter_cmap.png +++ b/examples/desktop/screenshots/scatter_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a02d2b5d4735d656d1b754ac3681a7700d961d7e4a43dfaf3a7dd0d4f6516ba6 -size 37808 +oid sha256:9479bb3995bd145a163a2f25592a4c85c52c663d33381efee7743ffc1f16aef1 +size 32894 From 25876bf5df73c9dc8254c741c633b6c5a73d79c3 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 19:17:31 -0400 Subject: [PATCH 191/196] replace one more test image --- examples/desktop/screenshots/gridplot_non_square.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/desktop/screenshots/gridplot_non_square.png b/examples/desktop/screenshots/gridplot_non_square.png index bc642b729..b74be7065 100644 --- a/examples/desktop/screenshots/gridplot_non_square.png +++ b/examples/desktop/screenshots/gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:352bf94c68444a330b000d7b6b3ec51b5b694ff3a0ce810299b325315923d9af -size 175938 +oid sha256:9ab4b1f8188824b81fe29b5c6ac7177734fb2b9958133e19f02919d1da98b96c +size 174978 From eb83cfdd14e47dd66e4884d27871070bb16a3caf Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 19:47:21 -0400 Subject: [PATCH 192/196] fix docs --- docs/source/api/gpu.rst | 7 ++++--- docs/source/api/graphics/ImageGraphic.rst | 1 + docs/source/api/graphics/LineCollection.rst | 7 ++++++- docs/source/api/graphics/LineGraphic.rst | 1 + docs/source/api/graphics/LineStack.rst | 7 ++++++- docs/source/api/graphics/ScatterGraphic.rst | 1 + docs/source/api/graphics/TextGraphic.rst | 1 + docs/source/api/selectors/LinearRegionSelector.rst | 1 + docs/source/api/selectors/LinearSelector.rst | 1 + docs/source/conf.py | 2 +- docs/source/generate_api.py | 6 ++++++ docs/source/index.rst | 7 +------ fastplotlib/graphics/_collection_base.py | 8 ++++---- fastplotlib/graphics/line_collection.py | 2 +- 14 files changed, 35 insertions(+), 17 deletions(-) diff --git a/docs/source/api/gpu.rst b/docs/source/api/gpu.rst index 62ffd5797..6f94aff23 100644 --- a/docs/source/api/gpu.rst +++ b/docs/source/api/gpu.rst @@ -1,5 +1,6 @@ -fastplotlib.utils -***************** +fastplotlib.utils.gpu +********************* -.. automodule:: fastplotlib.utils.gpu +.. currentmodule:: fastplotlib.utils.gpu +.. automodule:: fastplotlib :members: diff --git a/docs/source/api/graphics/ImageGraphic.rst b/docs/source/api/graphics/ImageGraphic.rst index 639a02cd1..310763b0e 100644 --- a/docs/source/api/graphics/ImageGraphic.rst +++ b/docs/source/api/graphics/ImageGraphic.rst @@ -45,6 +45,7 @@ Methods ImageGraphic.add_linear_region_selector ImageGraphic.add_linear_selector ImageGraphic.attach_feature + ImageGraphic.clear_event_handlers ImageGraphic.detach_feature ImageGraphic.remove_event_handler ImageGraphic.reset_vmin_vmax diff --git a/docs/source/api/graphics/LineCollection.rst b/docs/source/api/graphics/LineCollection.rst index 6399320ac..b6b98aa77 100644 --- a/docs/source/api/graphics/LineCollection.rst +++ b/docs/source/api/graphics/LineCollection.rst @@ -28,11 +28,16 @@ Properties LineCollection.event_handlers LineCollection.events LineCollection.graphics + LineCollection.metadatas LineCollection.name + LineCollection.names LineCollection.offset + LineCollection.offsets LineCollection.rotation + LineCollection.rotations LineCollection.thickness LineCollection.visible + LineCollection.visibles LineCollection.world_object Methods @@ -45,7 +50,7 @@ Methods LineCollection.add_linear_region_selector LineCollection.add_linear_selector LineCollection.attach_feature - LineCollection.child_type + LineCollection.clear_event_handlers LineCollection.detach_feature LineCollection.remove_event_handler LineCollection.remove_graphic diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst index 3f3c22207..353aa33e9 100644 --- a/docs/source/api/graphics/LineGraphic.rst +++ b/docs/source/api/graphics/LineGraphic.rst @@ -43,6 +43,7 @@ Methods LineGraphic.add_linear_region_selector LineGraphic.add_linear_selector LineGraphic.attach_feature + LineGraphic.clear_event_handlers LineGraphic.detach_feature LineGraphic.remove_event_handler LineGraphic.rotate diff --git a/docs/source/api/graphics/LineStack.rst b/docs/source/api/graphics/LineStack.rst index ac670c5b2..3ebbb99a5 100644 --- a/docs/source/api/graphics/LineStack.rst +++ b/docs/source/api/graphics/LineStack.rst @@ -28,11 +28,16 @@ Properties LineStack.event_handlers LineStack.events LineStack.graphics + LineStack.metadatas LineStack.name + LineStack.names LineStack.offset + LineStack.offsets LineStack.rotation + LineStack.rotations LineStack.thickness LineStack.visible + LineStack.visibles LineStack.world_object Methods @@ -45,7 +50,7 @@ Methods LineStack.add_linear_region_selector LineStack.add_linear_selector LineStack.attach_feature - LineStack.child_type + LineStack.clear_event_handlers LineStack.detach_feature LineStack.remove_event_handler LineStack.remove_graphic diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst index 9174f2b5c..e8de53fdd 100644 --- a/docs/source/api/graphics/ScatterGraphic.rst +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -41,6 +41,7 @@ Methods ScatterGraphic.add_event_handler ScatterGraphic.attach_feature + ScatterGraphic.clear_event_handlers ScatterGraphic.detach_feature ScatterGraphic.remove_event_handler ScatterGraphic.rotate diff --git a/docs/source/api/graphics/TextGraphic.rst b/docs/source/api/graphics/TextGraphic.rst index 3a52890ef..94df8e630 100644 --- a/docs/source/api/graphics/TextGraphic.rst +++ b/docs/source/api/graphics/TextGraphic.rst @@ -42,6 +42,7 @@ Methods TextGraphic.add_event_handler TextGraphic.attach_feature + TextGraphic.clear_event_handlers TextGraphic.detach_feature TextGraphic.remove_event_handler TextGraphic.rotate diff --git a/docs/source/api/selectors/LinearRegionSelector.rst b/docs/source/api/selectors/LinearRegionSelector.rst index 5840964a6..b39e77480 100644 --- a/docs/source/api/selectors/LinearRegionSelector.rst +++ b/docs/source/api/selectors/LinearRegionSelector.rst @@ -42,6 +42,7 @@ Methods LinearRegionSelector.add_event_handler LinearRegionSelector.add_ipywidget_handler LinearRegionSelector.attach_feature + LinearRegionSelector.clear_event_handlers LinearRegionSelector.detach_feature LinearRegionSelector.get_selected_data LinearRegionSelector.get_selected_index diff --git a/docs/source/api/selectors/LinearSelector.rst b/docs/source/api/selectors/LinearSelector.rst index d3623b5e3..93e2cf884 100644 --- a/docs/source/api/selectors/LinearSelector.rst +++ b/docs/source/api/selectors/LinearSelector.rst @@ -42,6 +42,7 @@ Methods LinearSelector.add_event_handler LinearSelector.add_ipywidget_handler LinearSelector.attach_feature + LinearSelector.clear_event_handlers LinearSelector.detach_feature LinearSelector.get_selected_data LinearSelector.get_selected_index diff --git a/docs/source/conf.py b/docs/source/conf.py index 16d6ed7d1..38133c901 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -51,7 +51,7 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "numpy": ("https://numpy.org/doc/stable/", None), - "pygfx": ("https://pygfx.readthedocs.io/en/latest", None), + "pygfx": ("https://pygfx.com/latest", None), "wgpu": ("https://wgpu-py.readthedocs.io/en/latest", None), } diff --git a/docs/source/generate_api.py b/docs/source/generate_api.py index a5f668130..0150836ec 100644 --- a/docs/source/generate_api.py +++ b/docs/source/generate_api.py @@ -263,6 +263,12 @@ def main(): with open(API_DIR.joinpath("utils.rst"), "w") as f: f.write(utils_str) + # gpu selection + fpl_functions = generate_functions_module(fastplotlib, "fastplotlib.utils.gpu") + + with open(API_DIR.joinpath("gpu.rst"), "w") as f: + f.write(fpl_functions) + if __name__ == "__main__": main() diff --git a/docs/source/index.rst b/docs/source/index.rst index 0bca839b9..35113c699 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,12 +1,6 @@ Welcome to fastplotlib's documentation! ======================================= -.. toctree:: - :caption: Quick Start - :maxdepth: 2 - - quickstart - .. toctree:: :caption: User Guide :maxdepth: 2 @@ -23,6 +17,7 @@ Welcome to fastplotlib's documentation! Graphic Features Selectors Widgets + Functions Utils GPU diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index 8b6c1778a..2805c684d 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -182,12 +182,12 @@ def __repr__(self): class GraphicCollection(Graphic, CollectionProperties): """Graphic Collection base class""" - child_type: type + _child_type: type _indexer: type def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) - cls._features = cls.child_type._features + cls._features = cls._child_type._features def __init__(self, name: str = None, metadata: Any = None, **kwargs): super().__init__(name=name, metadata=metadata, **kwargs) @@ -224,10 +224,10 @@ def add_graphic(self, graphic: Graphic): """ - if not type(graphic) == self.child_type: + if not type(graphic) == self._child_type: raise TypeError( f"Can only add graphics of the same type to a collection.\n" - f"You can only add {self.child_type.__name__} to a {self.__class__.__name__}, " + f"You can only add {self._child_type.__name__} to a {self.__class__.__name__}, " f"you are trying to add a {graphic.__class__.__name__}." ) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 15897cafc..92aad56b2 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -119,7 +119,7 @@ class LineCollectionIndexer(CollectionIndexer, _LineCollectionProperties): class LineCollection(GraphicCollection, _LineCollectionProperties): - child_type = LineGraphic + _child_type = LineGraphic _indexer = LineCollectionIndexer def __init__( From 8696d328226bc90ab2fd6eb9ac1ad8d64ed0a3b7 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 21:15:16 -0400 Subject: [PATCH 193/196] update image screenshots --- examples/notebooks/screenshots/nb-astronaut.png | 4 ++-- examples/notebooks/screenshots/nb-astronaut_RGB.png | 4 ++-- examples/notebooks/screenshots/nb-camera.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-movie-set_data.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-single-gnuplot2.png | 4 ++-- examples/notebooks/screenshots/nb-image-widget-single.png | 4 ++-- .../nb-image-widget-zfish-frame-50-frame-apply-gaussian.png | 4 ++-- .../nb-image-widget-zfish-frame-50-frame-apply-reset.png | 4 ++-- .../nb-image-widget-zfish-frame-50-max-window-13.png | 4 ++-- .../nb-image-widget-zfish-frame-50-mean-window-13.png | 4 ++-- .../nb-image-widget-zfish-frame-50-mean-window-5.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-zfish-frame-50.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-zfish-frame-99.png | 4 ++-- ...-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-max-window-13.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-mean-window-13.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-mean-window-5.png | 4 ++-- .../screenshots/nb-image-widget-zfish-grid-frame-50.png | 4 ++-- .../screenshots/nb-image-widget-zfish-grid-frame-99.png | 4 ++-- .../nb-image-widget-zfish-grid-init-mean-window-5.png | 4 ++-- ...b-image-widget-zfish-grid-set_data-reset-indices-false.png | 4 ++-- ...nb-image-widget-zfish-grid-set_data-reset-indices-true.png | 4 ++-- .../screenshots/nb-image-widget-zfish-init-mean-window-5.png | 4 ++-- .../nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png | 4 ++-- .../nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png | 4 ++-- .../nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png | 4 ++-- 27 files changed, 54 insertions(+), 54 deletions(-) diff --git a/examples/notebooks/screenshots/nb-astronaut.png b/examples/notebooks/screenshots/nb-astronaut.png index 378260288..3e55979ee 100644 --- a/examples/notebooks/screenshots/nb-astronaut.png +++ b/examples/notebooks/screenshots/nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e584533ea04b9758634ba62dceeb72991861c509d01dc082436c54c272686409 -size 112104 +oid sha256:a0fdb5b319347b4db4611dcf92cf08359c938f42a64b05d0dd163e0ca289e3c3 +size 112299 diff --git a/examples/notebooks/screenshots/nb-astronaut_RGB.png b/examples/notebooks/screenshots/nb-astronaut_RGB.png index bf11bf667..fbb514e3e 100644 --- a/examples/notebooks/screenshots/nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db9602a610f258803d74ac03cd46447dd5a7ad62241ec26a4c3df30c1d6de299 -size 110408 +oid sha256:2d312ce9097114bc32886c0370861bcf7deebfb4fda99e03817ebec2226eabdc +size 110338 diff --git a/examples/notebooks/screenshots/nb-camera.png b/examples/notebooks/screenshots/nb-camera.png index 9db4005bc..5629bd211 100644 --- a/examples/notebooks/screenshots/nb-camera.png +++ b/examples/notebooks/screenshots/nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bb9080b99c2717e093bf6ae4986bf0689a8d377e137a7022c9c6929b9a335d3 -size 77965 +oid sha256:0a415917cc16f09ab7b78eea5e5579d7dd45b6d92e80d87ba0970e9dd0568eb2 +size 77419 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index 5be8f55a3..486c89963 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66a310e312add59a310ff0a50335db97ac557d7f2967d8251a7d811c25a4de28 -size 40517 +oid sha256:3dbb4d04175c5603ff7e56a04438c8f0cfff7deff61889a06c342cedc04ac323 +size 43172 diff --git a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png index b8bf7adeb..02423a02a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png +++ b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbfa1e7aeb7f0a068a33f2f11023a06f834332f7b3d8e4cf97b51222536fd6cb -size 434782 +oid sha256:d7384d1a69629cfcdbebdc9e9e6a152383446f3cb696e69a11543253cdde2e64 +size 434060 diff --git a/examples/notebooks/screenshots/nb-image-widget-single.png b/examples/notebooks/screenshots/nb-image-widget-single.png index 86119e247..408739d6e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single.png +++ b/examples/notebooks/screenshots/nb-image-widget-single.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ceee2cdd73092cb84b4b0f2876fc08d838b8a47bb94d431a6c19c8a4793a153 -size 403521 +oid sha256:b57dffe179b6f52d204968085c885d70183d1f6a9a3f5a1dc2d005229b7acd01 +size 404179 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png index 82cee281f..596548486 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47e3e6cea0e738b2731060488886606f05595cfdfb0d81e6db1aa099dc8e3a84 -size 148181 +oid sha256:da1c660e4fb779ac6a4baed3d329cf80274981288ea076f243bb43aee7fb8eff +size 157731 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png index 0b7832eee..1318be413 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0af4ceb50ed269aa80667c3012a871c87f73777cd8cb497ebb243b53932b9bad -size 72377 +oid sha256:0d4f6407c3b029b01088fab9522a21f7d90d7a87def1ecbbb45f0fb4f8508f87 +size 69106 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png index 2bc2db3a5..e5fdbdd28 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9053c70da35fd42fe44a76e0ace8788ba79667b33c596409ca1e1f2f6d6ba3ad -size 195906 +oid sha256:e4176109805f4f521a1b630b68df1dce80a63b82a5ed01a6ba4c2cae0dfeb6bd +size 184423 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png index d5999dd0f..bf9548962 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77d4a8542a5507e3eda1203a6da29a2f533bbbe2988ad297948c74e44a4337ec -size 177152 +oid sha256:12df56b1045cdaddb94355b7e960aa58137a44eff4ff22aab3596c53ea7944c8 +size 179403 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png index 29af0398d..7b3e6bfba 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3422923039d45b20ea6150f0ad545bdf876596ba60b156df5ec4004590a29a3e -size 139029 +oid sha256:d5fd2f0918f4a29769ebb572f5163abb52667cf24e89efdd1d99bc57a0f5f607 +size 140124 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png index bb07b8fbb..a72245f3b 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba9e055298372238ce0cd0c5ac4d75db8cd53f3f4acffbcc22bf7d503b40ec57 -size 79174 +oid sha256:981c925f52ae8789f7f0d24ef3fe34efb7a08f221a7bc6079dd12f01099c3d25 +size 75054 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png index 6e8274659..19c19dc1f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6aed15f9f1b6bae442687613c5b04621f42e78f1dbda1e3560b000d652ba0b3 -size 61523 +oid sha256:6a5ecd1f966250ead16a96df996ff39877b4ee28534b7724a4a8e1db9c8984d2 +size 58334 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png index 28704bd2d..bcf663279 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:499fab9183f2528297fdfa3c96a25eb58b2376a44556d790ef06928e0379af3a -size 174612 +oid sha256:dbe1375ae8d2f348ad5d7e030fa67d45c250c6ed263c67179d542d0bd903e0d3 +size 177334 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png index d163fd22a..963290515 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72832b86f802ee90b4eb54cb64d64aff59527fe0c7dcb87a4d8ab281ad15726b -size 142136 +oid sha256:0ff341df374d816411e58c82d432e98a9f4cec1551725dafcd65cdb0c43edb12 +size 138235 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png index 52ebd8591..a049a484c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b414fbb8f6935901b65851de9c7cb37df628d7b24759dc7f407ee130389216a3 -size 371687 +oid sha256:bd4c51f7e07e46d7c660d28563aff1b7d3759387fc10db10addca29dfc0919b0 +size 365838 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png index 6c406a621..ada15017c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0ef52156509f308d972533fb45509ba7451b4d6149400d519aae28274609e41 -size 212053 +oid sha256:1d13cc8a32b40f5c721ab30020632d7dc1c679c8c8e5857476176e986de18ad3 +size 211240 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png index aaed804b8..2e71fd30d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8fd94e597074094dc3718153c8bb94fb0b1bf58e58e34601e5c7281f938f52bd -size 200278 +oid sha256:39044f4bb54038eee17f0d75940fd848b304629858d9e747ac0c81ce076d3c25 +size 199075 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png index 3110fa7cf..690b1c578 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76ab21314fbd7846c1b94aeeed9ef7b97be99d2f2f7f09c13c0474a889c21179 -size 159319 +oid sha256:109a4c8d708114e1b36d1a9fa129dbd9a768617baa953f37500310c0968b688a +size 154169 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png index 0cfad54e7..3e577698c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77f247de5374a8cb7ce27a338ab8be880d53d9586b52f646b400901ba70be3aa -size 146217 +oid sha256:568ae05e8889cec56530db0149613826f2697f55d8252cffbd32ff692b565fcf +size 141338 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png index c74807939..1ab48d117 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33c3dfa77bbc558493634ab83fd1539ef76022a2e20d10e1353d2bd0a0e94a2c -size 183739 +oid sha256:e7847dc083d6df2b20788b42153674c557b627b75db74d9446b06e165aa5a50a +size 182713 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png index a2841b1d5..0b0f05fc3 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:715f7909db0d374c2e618bb41f7a6341a8cc8891b1e3e8678a6f934fd71159a4 -size 127129 +oid sha256:839dd3fdc9db98d7044d88e816c179bff34f30584ba26ce7a96ea3b35fc3374e +size 122463 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png index 9064f2323..534403b1e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76708e8e6e6865d700aa5286fca4d58ba4eb91f21ab3b0243bb128e9a84f063c -size 131192 +oid sha256:f7088e11517a4a78492d16746ac8101b2e5e9142ebd61966030c555ab173443e +size 126267 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png index 1fbaec974..94993c688 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51c62474b9ebee76242ef82a710a83b90e0183792790f6a2cd00213642b76755 -size 99519 +oid sha256:709c7ec07e3e37e0415fad3fa542729d2896e8e546b6ea8d1373e7b46005bc26 +size 97278 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png index 5e0750ac8..27c693c1a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f8f74a0a5fa24e10a88d3723836306913243fa5fc23f46f44bbdae4c0209075 -size 58878 +oid sha256:c66db583e0d455319b665d35a8c5c8a5f717653c11311cdaba90e2c85e64235f +size 58941 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png index 8df83fe33..7444d7dbf 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0809b2dda0e773b7f100386f97144c40d36d51cd935c86ef1dcd4a938fce3981 -size 56319 +oid sha256:36867cd793634d00c46c782911abae6e7c579067aeeed891e40ddedbb0c228d9 +size 56505 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png index 5bbefc7ae..3941f3120 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2e2e2cf7ac6be1a4fccec54494c3fd48af673765653675438fa2469c549e90c -size 55055 +oid sha256:8b23f3655fcfcd85f6138f4de5cedf228a6dbad0c8cff0c87900042f83b8f409 +size 55269 From 70ca7c7cf4cf4ff79db449f045ae4e3a7e8a19cc Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Jun 2024 21:21:16 -0400 Subject: [PATCH 194/196] fix docs --- docs/source/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 35113c699..e99e38c52 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -17,7 +17,6 @@ Welcome to fastplotlib's documentation! Graphic Features Selectors Widgets - Functions Utils GPU From 28e08b930dae8f2f65ddd05bbccb470b48c662b5 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 12 Jun 2024 22:26:22 -0400 Subject: [PATCH 195/196] docstrings, Graphic.events -> Graphic.supported_events --- fastplotlib/graphics/_base.py | 9 ++++---- fastplotlib/graphics/_positions_base.py | 30 +++++++++++++------------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 46d83cdee..cab941894 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -115,7 +115,7 @@ def __init__( self._block_events = False @property - def events(self) -> tuple[str]: + def supported_events(self) -> tuple[str]: """events supported by this graphic""" return (*tuple(self._features), *PYGFX_EVENTS) @@ -192,10 +192,10 @@ def _set_world_object(self, wo: pygfx.WorldObject): if not all(self.world_object.world.rotation == self.rotation): self.rotation = self.rotation - def detach_feature(self, feature: str): + def unshare_property(self, feature: str): raise NotImplementedError - def attach_feature(self, feature: BufferManager): + def share_property(self, feature: BufferManager): raise NotImplementedError @property @@ -248,7 +248,7 @@ def my_handler(event): callback = None if decorating else args[0] types = args if decorating else args[1:] - unsupported_events = [t for t in types if t not in self.events] + unsupported_events = [t for t in types if t not in self.supported_events] if len(unsupported_events) > 0: raise TypeError( @@ -283,6 +283,7 @@ def decorator(_callback): return decorator(callback) def clear_event_handlers(self): + """clear all event handlers added to this graphic""" for ev, handlers in self.event_handlers: handlers = list(handlers) for h in handlers: diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index b6d505eac..3727087cc 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -134,50 +134,52 @@ def __init__( super().__init__(*args, **kwargs) - def detach_feature(self, feature: str): - if not isinstance(feature, str): + def unshare_property(self, property: str): + """unshare a shared property. Experimental and untested!""" + if not isinstance(property, str): raise TypeError - f = getattr(self, feature) + f = getattr(self, property) if f.shared == 0: raise BufferError("Cannot detach an independent buffer") - if feature == "colors" and isinstance(feature, VertexColors): + if property == "colors" and isinstance(property, VertexColors): self._colors._buffer = pygfx.Buffer(self._colors.value.copy()) self.world_object.geometry.colors = self._colors.buffer self._colors._shared -= 1 - elif feature == "data": + elif property == "data": self._data._buffer = pygfx.Buffer(self._data.value.copy()) self.world_object.geometry.positions = self._data.buffer self._data._shared -= 1 - elif feature == "sizes": + elif property == "sizes": self._sizes._buffer = pygfx.Buffer(self._sizes.value.copy()) self.world_object.geometry.positions = self._sizes.buffer self._sizes._shared -= 1 - def attach_feature( - self, feature: VertexPositions | VertexColors | PointsSizesFeature + def share_property( + self, property: VertexPositions | VertexColors | PointsSizesFeature ): - if isinstance(feature, VertexPositions): + """share a property from another graphic. Experimental and untested!""" + if isinstance(property, VertexPositions): # TODO: check if this causes a memory leak self._data._shared -= 1 - self._data = feature + self._data = property self._data._shared += 1 self.world_object.geometry.positions = self._data.buffer - elif isinstance(feature, VertexColors): + elif isinstance(property, VertexColors): self._colors._shared -= 1 - self._colors = feature + self._colors = property self._colors._shared += 1 self.world_object.geometry.colors = self._colors.buffer - elif isinstance(feature, PointsSizesFeature): + elif isinstance(property, PointsSizesFeature): self._sizes._shared -= 1 - self._sizes = feature + self._sizes = property self._sizes._shared += 1 self.world_object.geometry.sizes = self._sizes.buffer From d610690d9db7a9751ac0ddcbfebd775032240ea4 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 16 Jun 2024 17:55:53 -0400 Subject: [PATCH 196/196] update docs --- docs/source/api/graphics/ImageGraphic.rst | 6 +++--- docs/source/api/graphics/LineCollection.rst | 6 +++--- docs/source/api/graphics/LineGraphic.rst | 6 +++--- docs/source/api/graphics/LineStack.rst | 6 +++--- docs/source/api/graphics/ScatterGraphic.rst | 6 +++--- docs/source/api/graphics/TextGraphic.rst | 6 +++--- docs/source/api/selectors/LinearRegionSelector.rst | 6 +++--- docs/source/api/selectors/LinearSelector.rst | 6 +++--- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/source/api/graphics/ImageGraphic.rst b/docs/source/api/graphics/ImageGraphic.rst index 310763b0e..a0ae8a5ed 100644 --- a/docs/source/api/graphics/ImageGraphic.rst +++ b/docs/source/api/graphics/ImageGraphic.rst @@ -26,11 +26,11 @@ Properties ImageGraphic.data ImageGraphic.deleted ImageGraphic.event_handlers - ImageGraphic.events ImageGraphic.interpolation ImageGraphic.name ImageGraphic.offset ImageGraphic.rotation + ImageGraphic.supported_events ImageGraphic.visible ImageGraphic.vmax ImageGraphic.vmin @@ -44,10 +44,10 @@ Methods ImageGraphic.add_event_handler ImageGraphic.add_linear_region_selector ImageGraphic.add_linear_selector - ImageGraphic.attach_feature ImageGraphic.clear_event_handlers - ImageGraphic.detach_feature ImageGraphic.remove_event_handler ImageGraphic.reset_vmin_vmax ImageGraphic.rotate + ImageGraphic.share_property + ImageGraphic.unshare_property diff --git a/docs/source/api/graphics/LineCollection.rst b/docs/source/api/graphics/LineCollection.rst index b6b98aa77..c000b7334 100644 --- a/docs/source/api/graphics/LineCollection.rst +++ b/docs/source/api/graphics/LineCollection.rst @@ -26,7 +26,6 @@ Properties LineCollection.data LineCollection.deleted LineCollection.event_handlers - LineCollection.events LineCollection.graphics LineCollection.metadatas LineCollection.name @@ -35,6 +34,7 @@ Properties LineCollection.offsets LineCollection.rotation LineCollection.rotations + LineCollection.supported_events LineCollection.thickness LineCollection.visible LineCollection.visibles @@ -49,10 +49,10 @@ Methods LineCollection.add_graphic LineCollection.add_linear_region_selector LineCollection.add_linear_selector - LineCollection.attach_feature LineCollection.clear_event_handlers - LineCollection.detach_feature LineCollection.remove_event_handler LineCollection.remove_graphic LineCollection.rotate + LineCollection.share_property + LineCollection.unshare_property diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst index 353aa33e9..d260c3214 100644 --- a/docs/source/api/graphics/LineGraphic.rst +++ b/docs/source/api/graphics/LineGraphic.rst @@ -26,10 +26,10 @@ Properties LineGraphic.data LineGraphic.deleted LineGraphic.event_handlers - LineGraphic.events LineGraphic.name LineGraphic.offset LineGraphic.rotation + LineGraphic.supported_events LineGraphic.thickness LineGraphic.visible LineGraphic.world_object @@ -42,9 +42,9 @@ Methods LineGraphic.add_event_handler LineGraphic.add_linear_region_selector LineGraphic.add_linear_selector - LineGraphic.attach_feature LineGraphic.clear_event_handlers - LineGraphic.detach_feature LineGraphic.remove_event_handler LineGraphic.rotate + LineGraphic.share_property + LineGraphic.unshare_property diff --git a/docs/source/api/graphics/LineStack.rst b/docs/source/api/graphics/LineStack.rst index 3ebbb99a5..18b35932d 100644 --- a/docs/source/api/graphics/LineStack.rst +++ b/docs/source/api/graphics/LineStack.rst @@ -26,7 +26,6 @@ Properties LineStack.data LineStack.deleted LineStack.event_handlers - LineStack.events LineStack.graphics LineStack.metadatas LineStack.name @@ -35,6 +34,7 @@ Properties LineStack.offsets LineStack.rotation LineStack.rotations + LineStack.supported_events LineStack.thickness LineStack.visible LineStack.visibles @@ -49,10 +49,10 @@ Methods LineStack.add_graphic LineStack.add_linear_region_selector LineStack.add_linear_selector - LineStack.attach_feature LineStack.clear_event_handlers - LineStack.detach_feature LineStack.remove_event_handler LineStack.remove_graphic LineStack.rotate + LineStack.share_property + LineStack.unshare_property diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst index e8de53fdd..8f2b17fd6 100644 --- a/docs/source/api/graphics/ScatterGraphic.rst +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -26,11 +26,11 @@ Properties ScatterGraphic.data ScatterGraphic.deleted ScatterGraphic.event_handlers - ScatterGraphic.events ScatterGraphic.name ScatterGraphic.offset ScatterGraphic.rotation ScatterGraphic.sizes + ScatterGraphic.supported_events ScatterGraphic.visible ScatterGraphic.world_object @@ -40,9 +40,9 @@ Methods :toctree: ScatterGraphic_api ScatterGraphic.add_event_handler - ScatterGraphic.attach_feature ScatterGraphic.clear_event_handlers - ScatterGraphic.detach_feature ScatterGraphic.remove_event_handler ScatterGraphic.rotate + ScatterGraphic.share_property + ScatterGraphic.unshare_property diff --git a/docs/source/api/graphics/TextGraphic.rst b/docs/source/api/graphics/TextGraphic.rst index 94df8e630..a3cd9bbb9 100644 --- a/docs/source/api/graphics/TextGraphic.rst +++ b/docs/source/api/graphics/TextGraphic.rst @@ -23,7 +23,6 @@ Properties TextGraphic.block_events TextGraphic.deleted TextGraphic.event_handlers - TextGraphic.events TextGraphic.face_color TextGraphic.font_size TextGraphic.name @@ -31,6 +30,7 @@ Properties TextGraphic.outline_color TextGraphic.outline_thickness TextGraphic.rotation + TextGraphic.supported_events TextGraphic.text TextGraphic.visible TextGraphic.world_object @@ -41,9 +41,9 @@ Methods :toctree: TextGraphic_api TextGraphic.add_event_handler - TextGraphic.attach_feature TextGraphic.clear_event_handlers - TextGraphic.detach_feature TextGraphic.remove_event_handler TextGraphic.rotate + TextGraphic.share_property + TextGraphic.unshare_property diff --git a/docs/source/api/selectors/LinearRegionSelector.rst b/docs/source/api/selectors/LinearRegionSelector.rst index b39e77480..c9140bc7d 100644 --- a/docs/source/api/selectors/LinearRegionSelector.rst +++ b/docs/source/api/selectors/LinearRegionSelector.rst @@ -24,13 +24,13 @@ Properties LinearRegionSelector.block_events LinearRegionSelector.deleted LinearRegionSelector.event_handlers - LinearRegionSelector.events LinearRegionSelector.limits LinearRegionSelector.name LinearRegionSelector.offset LinearRegionSelector.parent LinearRegionSelector.rotation LinearRegionSelector.selection + LinearRegionSelector.supported_events LinearRegionSelector.visible LinearRegionSelector.world_object @@ -41,13 +41,13 @@ Methods LinearRegionSelector.add_event_handler LinearRegionSelector.add_ipywidget_handler - LinearRegionSelector.attach_feature LinearRegionSelector.clear_event_handlers - LinearRegionSelector.detach_feature LinearRegionSelector.get_selected_data LinearRegionSelector.get_selected_index LinearRegionSelector.get_selected_indices LinearRegionSelector.make_ipywidget_slider LinearRegionSelector.remove_event_handler LinearRegionSelector.rotate + LinearRegionSelector.share_property + LinearRegionSelector.unshare_property diff --git a/docs/source/api/selectors/LinearSelector.rst b/docs/source/api/selectors/LinearSelector.rst index 93e2cf884..fa21f8f15 100644 --- a/docs/source/api/selectors/LinearSelector.rst +++ b/docs/source/api/selectors/LinearSelector.rst @@ -24,13 +24,13 @@ Properties LinearSelector.block_events LinearSelector.deleted LinearSelector.event_handlers - LinearSelector.events LinearSelector.limits LinearSelector.name LinearSelector.offset LinearSelector.parent LinearSelector.rotation LinearSelector.selection + LinearSelector.supported_events LinearSelector.visible LinearSelector.world_object @@ -41,13 +41,13 @@ Methods LinearSelector.add_event_handler LinearSelector.add_ipywidget_handler - LinearSelector.attach_feature LinearSelector.clear_event_handlers - LinearSelector.detach_feature LinearSelector.get_selected_data LinearSelector.get_selected_index LinearSelector.get_selected_indices LinearSelector.make_ipywidget_slider LinearSelector.remove_event_handler LinearSelector.rotate + LinearSelector.share_property + LinearSelector.unshare_property pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy