From 84e6590658e53d50d661174926d8c33edefd14e4 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 12 Jan 2026 23:07:42 -0500 Subject: [PATCH 01/10] remove isolated_buffer --- fastplotlib/graphics/_positions_base.py | 3 +-- fastplotlib/graphics/features/_base.py | 19 +++++-------------- fastplotlib/graphics/features/_image.py | 12 ++++-------- fastplotlib/graphics/features/_mesh.py | 4 ++-- fastplotlib/graphics/features/_positions.py | 19 +++++++++++++++---- fastplotlib/graphics/features/_scatter.py | 8 +++----- fastplotlib/graphics/features/_vectors.py | 2 -- fastplotlib/graphics/features/_volume.py | 12 ++++-------- fastplotlib/graphics/image.py | 9 +-------- fastplotlib/graphics/image_volume.py | 8 +------- fastplotlib/graphics/line.py | 2 -- fastplotlib/graphics/line_collection.py | 4 ---- fastplotlib/graphics/mesh.py | 11 ++--------- fastplotlib/graphics/scatter.py | 6 ------ 14 files changed, 38 insertions(+), 81 deletions(-) diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 73520cc84..4754c3372 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -78,7 +78,6 @@ def __init__( uniform_color: bool = False, cmap: str | VertexCmap = None, cmap_transform: np.ndarray = None, - isolated_buffer: bool = True, size_space: str = "screen", *args, **kwargs, @@ -86,7 +85,7 @@ def __init__( if isinstance(data, VertexPositions): self._data = data else: - self._data = VertexPositions(data, isolated_buffer=isolated_buffer) + self._data = VertexPositions(data) if cmap_transform is not None and cmap is None: raise ValueError("must pass `cmap` if passing `cmap_transform`") diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index 779310476..e41414cd2 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -138,29 +138,20 @@ class BufferManager(GraphicFeature): def __init__( self, data: NDArray | pygfx.Buffer, - buffer_type: Literal["buffer", "texture", "texture-array"] = "buffer", - isolated_buffer: bool = True, **kwargs, ): super().__init__(**kwargs) - 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[:] - else: - # user's input array is used as the buffer - bdata = data if isinstance(data, pygfx.Resource): # 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) else: - raise ValueError( - "`data` must be a pygfx.Buffer instance or `buffer_type` must be one of: 'buffer' or 'texture'" - ) + # create a buffer + bdata = np.zeros(data.shape, dtype=data.dtype) + bdata[:] = data[:] + + self._buffer = pygfx.Buffer(bdata) self._event_handlers: list[callable] = list() diff --git a/fastplotlib/graphics/features/_image.py b/fastplotlib/graphics/features/_image.py index 648f79bc8..cb66bb1ef 100644 --- a/fastplotlib/graphics/features/_image.py +++ b/fastplotlib/graphics/features/_image.py @@ -33,7 +33,7 @@ class TextureArray(GraphicFeature): }, ] - def __init__(self, data, isolated_buffer: bool = True, property_name: str = "data"): + def __init__(self, data, property_name: str = "data"): super().__init__(property_name=property_name) data = self._fix_data(data) @@ -41,13 +41,9 @@ def __init__(self, data, isolated_buffer: bool = True, property_name: str = "dat shared = pygfx.renderers.wgpu.get_shared() self._texture_limit_2d = shared.device.limits["max-texture-dimension-2d"] - 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 + # create a new buffer + self._value = np.zeros(data.shape, dtype=data.dtype) + self.value[:] = data[:] # data start indices for each Texture self._row_indices = np.arange( diff --git a/fastplotlib/graphics/features/_mesh.py b/fastplotlib/graphics/features/_mesh.py index 7355acb4e..586354117 100644 --- a/fastplotlib/graphics/features/_mesh.py +++ b/fastplotlib/graphics/features/_mesh.py @@ -52,7 +52,7 @@ class MeshIndices(VertexPositions): ] def __init__( - self, data: Any, isolated_buffer: bool = True, property_name: str = "indices" + self, data: Any, property_name: str = "indices" ): """ Manages the vertex indices buffer shown in the graphic. @@ -61,7 +61,7 @@ def __init__( data = self._fix_data(data) super().__init__( - data, isolated_buffer=isolated_buffer, property_name=property_name + data, property_name=property_name ) def _fix_data(self, data): diff --git a/fastplotlib/graphics/features/_positions.py b/fastplotlib/graphics/features/_positions.py index 295d22417..6c8a47c5a 100644 --- a/fastplotlib/graphics/features/_positions.py +++ b/fastplotlib/graphics/features/_positions.py @@ -39,7 +39,6 @@ def __init__( self, colors: str | pygfx.Color | np.ndarray | Sequence[float] | Sequence[str], n_colors: int, - isolated_buffer: bool = True, property_name: str = "colors", ): """ @@ -58,7 +57,7 @@ def __init__( data = parse_colors(colors, n_colors) super().__init__( - data=data, isolated_buffer=isolated_buffer, property_name=property_name + data=data, property_name=property_name ) @block_reentrance @@ -232,7 +231,7 @@ class VertexPositions(BufferManager): ] def __init__( - self, data: Any, isolated_buffer: bool = True, property_name: str = "data" + self, data: Any, property_name: str = "data" ): """ Manages the vertex positions buffer shown in the graphic. @@ -241,7 +240,7 @@ def __init__( data = self._fix_data(data) super().__init__( - data, isolated_buffer=isolated_buffer, property_name=property_name + data, property_name=property_name ) def _fix_data(self, data): @@ -261,6 +260,18 @@ def _fix_data(self, data): return to_gpu_supported_dtype(data) + def set_value(self, graphic, value): + """Sets the entire array, creates new buffer if necessary""" + if isinstance(value, np.ndarray): + if self.buffer.data.shape[0] != value.shape[0]: + # number of items doesn't match, create a new buffer + bdata = np.zeros(value.shape, dtype=np.float32) + bdata[:] = value[:] + self._buffer = pygfx.Buffer(bdata) + graphic.world_object.geometry.position = self.buffer + else: + self[:] = value + @block_reentrance def __setitem__( self, diff --git a/fastplotlib/graphics/features/_scatter.py b/fastplotlib/graphics/features/_scatter.py index 16671ef89..1d9cb153d 100644 --- a/fastplotlib/graphics/features/_scatter.py +++ b/fastplotlib/graphics/features/_scatter.py @@ -158,7 +158,7 @@ def __init__( ) super().__init__( - markers_int_array, isolated_buffer=False, property_name=property_name + markers_int_array, property_name=property_name ) @property @@ -414,7 +414,6 @@ def __init__( self, rotations: int | float | np.ndarray | Sequence[int | float], n_datapoints: int, - isolated_buffer: bool = True, property_name: str = "point_rotations", ): """ @@ -422,7 +421,7 @@ def __init__( """ sizes = self._fix_sizes(rotations, n_datapoints) super().__init__( - data=sizes, isolated_buffer=isolated_buffer, property_name=property_name + data=sizes, property_name=property_name ) def _fix_sizes( @@ -488,7 +487,6 @@ def __init__( self, sizes: int | float | np.ndarray | Sequence[int | float], n_datapoints: int, - isolated_buffer: bool = True, property_name: str = "sizes", ): """ @@ -496,7 +494,7 @@ def __init__( """ sizes = self._fix_sizes(sizes, n_datapoints) super().__init__( - data=sizes, isolated_buffer=isolated_buffer, property_name=property_name + data=sizes, property_name=property_name ) def _fix_sizes( diff --git a/fastplotlib/graphics/features/_vectors.py b/fastplotlib/graphics/features/_vectors.py index 9c86d25fc..729562b06 100644 --- a/fastplotlib/graphics/features/_vectors.py +++ b/fastplotlib/graphics/features/_vectors.py @@ -22,7 +22,6 @@ class VectorPositions(GraphicFeature): def __init__( self, positions: np.ndarray, - isolated_buffer: bool = True, property_name: str = "positions", ): """ @@ -111,7 +110,6 @@ class VectorDirections(GraphicFeature): def __init__( self, directions: np.ndarray, - isolated_buffer: bool = True, property_name: str = "directions", ): """Manages vector field positions by managing the mesh instance buffer's full transform matrix""" diff --git a/fastplotlib/graphics/features/_volume.py b/fastplotlib/graphics/features/_volume.py index ec4c4052a..532065fb7 100644 --- a/fastplotlib/graphics/features/_volume.py +++ b/fastplotlib/graphics/features/_volume.py @@ -34,7 +34,7 @@ class TextureArrayVolume(GraphicFeature): }, ] - def __init__(self, data, isolated_buffer: bool = True): + def __init__(self, data): super().__init__(property_name="data") data = self._fix_data(data) @@ -43,13 +43,9 @@ def __init__(self, data, isolated_buffer: bool = True): self._texture_size_limit = shared.device.limits["max-texture-dimension-3d"] - 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 + # create a new buffer that will be used for the texture data + self._value = np.zeros(data.shape, dtype=data.dtype) + self.value[:] = data[:] # data start indices for each Texture self._row_indices = np.arange( diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 1eaf54bb6..531c832a1 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -93,7 +93,6 @@ def __init__( cmap: str = "plasma", interpolation: str = "nearest", cmap_interpolation: str = "linear", - isolated_buffer: bool = True, **kwargs, ): """ @@ -121,12 +120,6 @@ def __init__( 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 - 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 - useful if the - array is large. - kwargs: additional keyword arguments passed to :class:`.Graphic` @@ -142,7 +135,7 @@ def __init__( else: # create new texture array to manage buffer # texture array that manages the multiple textures on the GPU that represent this image - self._data = TextureArray(data, isolated_buffer=isolated_buffer) + self._data = TextureArray(data) if (vmin is None) or (vmax is None): _vmin, _vmax = quick_min_max(self.data.value) diff --git a/fastplotlib/graphics/image_volume.py b/fastplotlib/graphics/image_volume.py index db616b30d..0f0e5f23c 100644 --- a/fastplotlib/graphics/image_volume.py +++ b/fastplotlib/graphics/image_volume.py @@ -113,7 +113,6 @@ def __init__( substep_size: float = 0.1, emissive: str | tuple | np.ndarray = (0, 0, 0), shininess: int = 30, - isolated_buffer: bool = True, **kwargs, ): """ @@ -170,11 +169,6 @@ def __init__( How shiny the specular highlight is; a higher value gives a sharper highlight. Used only if `mode` = "iso" - 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 - useful if the array is large. - kwargs additional keyword arguments passed to :class:`.Graphic` @@ -196,7 +190,7 @@ def __init__( else: # create new texture array to manage buffer # texture array that manages the textures on the GPU that represent this image volume - self._data = TextureArrayVolume(data, isolated_buffer=isolated_buffer) + self._data = TextureArrayVolume(data) if (vmin is None) or (vmax is None): _vmin, _vmax = quick_min_max(self.data.value) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index f2d862067..ed7a95490 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -39,7 +39,6 @@ def __init__( uniform_color: bool = False, cmap: str = None, cmap_transform: np.ndarray | Sequence = None, - isolated_buffer: bool = True, size_space: str = "screen", **kwargs, ): @@ -87,7 +86,6 @@ def __init__( uniform_color=uniform_color, cmap=cmap, cmap_transform=cmap_transform, - isolated_buffer=isolated_buffer, size_space=size_space, **kwargs, ) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 275cc1e47..2087ada62 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -135,7 +135,6 @@ def __init__( names: list[str] = None, metadata: Any = None, metadatas: Sequence[Any] | np.ndarray = None, - isolated_buffer: bool = True, kwargs_lines: list[dict] = None, **kwargs, ): @@ -324,7 +323,6 @@ def __init__( cmap=_cmap, name=_name, metadata=_m, - isolated_buffer=isolated_buffer, **kwargs_lines, ) @@ -560,7 +558,6 @@ def __init__( 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, @@ -634,7 +631,6 @@ def __init__( names=names, metadata=metadata, metadatas=metadatas, - isolated_buffer=isolated_buffer, kwargs_lines=kwargs_lines, **kwargs, ) diff --git a/fastplotlib/graphics/mesh.py b/fastplotlib/graphics/mesh.py index 2e5a11851..d569de783 100644 --- a/fastplotlib/graphics/mesh.py +++ b/fastplotlib/graphics/mesh.py @@ -38,7 +38,6 @@ def __init__( mapcoords: Any = None, cmap: str | dict | pygfx.Texture | pygfx.TextureMap | np.ndarray = None, clim: tuple[float, float] = None, - isolated_buffer: bool = True, **kwargs, ): """ @@ -77,12 +76,6 @@ def __init__( Both 1D and 2D colormaps are supported, though the mapcoords has to match the dimensionality. An image can also be used, this is basically a 2D colormap. - 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 - useful if the - array is large. In almost all cases this should be ``True``. - **kwargs passed to :class:`.Graphic` @@ -94,14 +87,14 @@ def __init__( self._positions = positions else: self._positions = VertexPositions( - positions, isolated_buffer=isolated_buffer, property_name="positions" + positions, property_name="positions" ) if isinstance(positions, MeshIndices): self._indices = indices else: self._indices = MeshIndices( - indices, isolated_buffer=isolated_buffer, property_name="indices" + indices, property_name="indices" ) self._cmap = MeshCmap(cmap) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index a2e696a82..3f9a530ef 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -56,7 +56,6 @@ def __init__( sizes: float | np.ndarray | Sequence[float] = 1, uniform_size: bool = False, size_space: str = "screen", - isolated_buffer: bool = True, **kwargs, ): """ @@ -154,10 +153,6 @@ def __init__( size_space: str, default "screen" coordinate space in which the size is expressed, one of ("screen", "world", "model") - 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 if you have large arrays. - kwargs passed to :class:`.Graphic` @@ -169,7 +164,6 @@ def __init__( uniform_color=uniform_color, cmap=cmap, cmap_transform=cmap_transform, - isolated_buffer=isolated_buffer, size_space=size_space, **kwargs, ) From 7163c96d192b2abecfa6aee11f3093dd4005e0d9 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 12 Jan 2026 23:44:27 -0500 Subject: [PATCH 02/10] remove isolated_buffer from mixin --- fastplotlib/layouts/_graphic_methods_mixin.py | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index 06a4c7517..2d8aa2eab 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -33,7 +33,6 @@ def add_image( cmap: str = "plasma", interpolation: str = "nearest", cmap_interpolation: str = "linear", - isolated_buffer: bool = True, **kwargs, ) -> ImageGraphic: """ @@ -62,12 +61,6 @@ def add_image( 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 - 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 - useful if the - array is large. - kwargs: additional keyword arguments passed to :class:`.Graphic` @@ -81,7 +74,6 @@ def add_image( cmap, interpolation, cmap_interpolation, - isolated_buffer, **kwargs, ) @@ -100,7 +92,6 @@ def add_image_volume( substep_size: float = 0.1, emissive: str | tuple | numpy.ndarray = (0, 0, 0), shininess: int = 30, - isolated_buffer: bool = True, **kwargs, ) -> ImageVolumeGraphic: """ @@ -158,11 +149,6 @@ def add_image_volume( How shiny the specular highlight is; a higher value gives a sharper highlight. Used only if `mode` = "iso" - 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 - useful if the array is large. - kwargs additional keyword arguments passed to :class:`.Graphic` @@ -183,7 +169,6 @@ def add_image_volume( substep_size, emissive, shininess, - isolated_buffer, **kwargs, ) @@ -199,7 +184,6 @@ def add_line_collection( names: list[str] = None, metadata: Any = None, metadatas: Union[Sequence[Any], numpy.ndarray] = None, - isolated_buffer: bool = True, kwargs_lines: list[dict] = None, **kwargs, ) -> LineCollection: @@ -268,7 +252,6 @@ def add_line_collection( names, metadata, metadatas, - isolated_buffer, kwargs_lines, **kwargs, ) @@ -281,7 +264,6 @@ def add_line( uniform_color: bool = False, cmap: str = None, cmap_transform: Union[numpy.ndarray, Sequence] = None, - isolated_buffer: bool = True, size_space: str = "screen", **kwargs, ) -> LineGraphic: @@ -332,7 +314,6 @@ def add_line( uniform_color, cmap, cmap_transform, - isolated_buffer, size_space, **kwargs, ) @@ -348,7 +329,6 @@ def add_line_stack( 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, @@ -425,7 +405,6 @@ def add_line_stack( names, metadata, metadatas, - isolated_buffer, separation, separation_axis, kwargs_lines, @@ -448,7 +427,6 @@ def add_mesh( | numpy.ndarray ) = None, clim: tuple[float, float] = None, - isolated_buffer: bool = True, **kwargs, ) -> MeshGraphic: """ @@ -488,12 +466,6 @@ def add_mesh( Both 1D and 2D colormaps are supported, though the mapcoords has to match the dimensionality. An image can also be used, this is basically a 2D colormap. - 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 - useful if the - array is large. In almost all cases this should be ``True``. - **kwargs passed to :class:`.Graphic` @@ -509,7 +481,6 @@ def add_mesh( mapcoords, cmap, clim, - isolated_buffer, **kwargs, ) @@ -592,7 +563,6 @@ def add_scatter( sizes: Union[float, numpy.ndarray, Sequence[float]] = 1, uniform_size: bool = False, size_space: str = "screen", - isolated_buffer: bool = True, **kwargs, ) -> ScatterGraphic: """ @@ -691,10 +661,6 @@ def add_scatter( size_space: str, default "screen" coordinate space in which the size is expressed, one of ("screen", "world", "model") - 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 if you have large arrays. - kwargs passed to :class:`.Graphic` @@ -720,7 +686,6 @@ def add_scatter( sizes, uniform_size, size_space, - isolated_buffer, **kwargs, ) From 426ee5cd081ee1266a72ad880a6347401d2107cc Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 13 Jan 2026 00:04:16 -0500 Subject: [PATCH 03/10] basics works for positions data --- fastplotlib/graphics/_positions_base.py | 8 ++--- fastplotlib/graphics/features/_base.py | 14 ++++---- fastplotlib/graphics/features/_positions.py | 39 +++++++++++++++++++-- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 4754c3372..a341b4077 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -23,7 +23,7 @@ def data(self) -> VertexPositions: @data.setter def data(self, value): - self._data[:] = value + self._data.set_value(self, value) @property def colors(self) -> VertexColors | pygfx.Color: @@ -36,11 +36,7 @@ def colors(self) -> VertexColors | pygfx.Color: @colors.setter def colors(self, value: str | np.ndarray | Sequence[float] | Sequence[str]): - if isinstance(self._colors, VertexColors): - self._colors[:] = value - - elif isinstance(self._colors, UniformColor): - self._colors.set_value(self, value) + self._colors.set_value(self, value) @property def cmap(self) -> VertexCmap: diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index e41414cd2..05b9da6d7 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -1,5 +1,5 @@ from warnings import warn -from typing import Literal +from typing import Callable import numpy as np from numpy.typing import NDArray @@ -78,7 +78,7 @@ def block_events(self, val: bool): """ self._block_events = val - def add_event_handler(self, handler: callable): + def add_event_handler(self, handler: Callable): """ Add an event handler. All added event handlers are called when this feature changes. @@ -89,7 +89,7 @@ def add_event_handler(self, handler: callable): Parameters ---------- - handler: callable + handler: Callable a function to call when this feature changes """ @@ -102,7 +102,7 @@ def add_event_handler(self, handler: callable): self._event_handlers.append(handler) - def remove_event_handler(self, handler: callable): + def remove_event_handler(self, handler: Callable): """ Remove a registered event ``handler``. @@ -148,12 +148,12 @@ def __init__( self._buffer = data else: # create a buffer - bdata = np.zeros(data.shape, dtype=data.dtype) + bdata = np.empty(data.shape, dtype=data.dtype) bdata[:] = data[:] self._buffer = pygfx.Buffer(bdata) - self._event_handlers: list[callable] = list() + self._event_handlers: list[Callable] = list() @property def value(self) -> np.ndarray: @@ -165,7 +165,7 @@ def set_value(self, graphic, value): self[:] = value @property - def buffer(self) -> pygfx.Buffer | pygfx.Texture: + def buffer(self) -> pygfx.Buffer: """managed buffer""" return self._buffer diff --git a/fastplotlib/graphics/features/_positions.py b/fastplotlib/graphics/features/_positions.py index 6c8a47c5a..242d926e8 100644 --- a/fastplotlib/graphics/features/_positions.py +++ b/fastplotlib/graphics/features/_positions.py @@ -60,6 +60,21 @@ def __init__( data=data, property_name=property_name ) + def set_value(self, graphic, value: str | pygfx.Color | np.ndarray | Sequence[float] | Sequence[str]): + """set the entire array, create new buffer if necessary""" + if isinstance(value, (np.ndarray, list, tuple)): + # check if the number of elements matches current buffer size + if self.buffer.data.shape[0] != len(value): + # create new buffer + new_colors = parse_colors(value, len(value)) + self._buffer = pygfx.Buffer(new_colors) + graphic.world_object.geometry.colors = self.buffer + else: + # buffer size unchanged + self[:] = value + else: + self[:] = value + @block_reentrance def __setitem__( self, @@ -265,10 +280,28 @@ def set_value(self, graphic, value): if isinstance(value, np.ndarray): if self.buffer.data.shape[0] != value.shape[0]: # number of items doesn't match, create a new buffer - bdata = np.zeros(value.shape, dtype=np.float32) - bdata[:] = value[:] + + # if data is not 3D + if value.ndim == 1: + # this is already a newly allocated buffer + # _fix_data creates a new array so we don't need to re-allocate with np.zeros + bdata = self._fix_data(value) + + elif value.shape[1] == 2: + # this is already a newly allocated buffer + bdata = self._fix_data(value) + + elif value.shape[1] == 3: + # need to allocate a buffer to use here + bdata = np.empty(value.shape, dtype=np.float32) + bdata[:] = value[:] + + # create the new buffer self._buffer = pygfx.Buffer(bdata) - graphic.world_object.geometry.position = self.buffer + graphic.world_object.geometry.positions = self.buffer + else: + # buffer size unchanged + self[:] = value else: self[:] = value From 23622b0f3b50186fd7171029cb9820efcb08fb48 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 13 Jan 2026 01:04:12 -0500 Subject: [PATCH 04/10] replaceable buffers for all positions related features --- fastplotlib/graphics/features/_positions.py | 29 +++-- fastplotlib/graphics/features/_scatter.py | 117 ++++++++++++++------ 2 files changed, 102 insertions(+), 44 deletions(-) diff --git a/fastplotlib/graphics/features/_positions.py b/fastplotlib/graphics/features/_positions.py index 242d926e8..db1836539 100644 --- a/fastplotlib/graphics/features/_positions.py +++ b/fastplotlib/graphics/features/_positions.py @@ -69,11 +69,21 @@ def set_value(self, graphic, value: str | pygfx.Color | np.ndarray | Sequence[fl new_colors = parse_colors(value, len(value)) self._buffer = pygfx.Buffer(new_colors) graphic.world_object.geometry.colors = self.buffer - else: - # buffer size unchanged - self[:] = value - else: - self[:] = value + + if len(self._event_handlers) < 1: + return + + event_info = { + "key": slice(None), + "value": new_colors, + "user_value": value, + } + + event = GraphicFeatureEvent(self._property_name, info=event_info) + self._call_event_handlers(event) + return + + self[:] = value @block_reentrance def __setitem__( @@ -299,11 +309,10 @@ def set_value(self, graphic, value): # create the new buffer self._buffer = pygfx.Buffer(bdata) graphic.world_object.geometry.positions = self.buffer - else: - # buffer size unchanged - self[:] = value - else: - self[:] = value + self._emit_event(self._property_name, key=slice(None), value=value) + return + + self[:] = value @block_reentrance def __setitem__( diff --git a/fastplotlib/graphics/features/_scatter.py b/fastplotlib/graphics/features/_scatter.py index 1d9cb153d..faedbb9ae 100644 --- a/fastplotlib/graphics/features/_scatter.py +++ b/fastplotlib/graphics/features/_scatter.py @@ -100,6 +100,43 @@ def searchsorted_markers_to_int_array(markers_str_array: np.ndarray[str]): return marker_int_searchsorted_vals[indices] +def parse_markers_init(markers: str | Sequence[str] | np.ndarray, n_datapoints: int): + # first validate then allocate buffers + + if isinstance(markers, str): + markers = user_input_to_marker(markers) + + elif isinstance(markers, (tuple, list, np.ndarray)): + validate_user_markers_array(markers) + + # allocate buffers + markers_int_array = np.zeros(n_datapoints, dtype=np.int32) + + marker_str_length = max(map(len, list(pygfx.MarkerShape))) + + markers_readable_array = np.empty( + n_datapoints, dtype=f" Date: Tue, 13 Jan 2026 02:14:34 -0500 Subject: [PATCH 05/10] image data buffer can change --- fastplotlib/graphics/image.py | 42 +++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 531c832a1..5895160f5 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -127,7 +127,7 @@ def __init__( super().__init__(**kwargs) - world_object = pygfx.Group() + group = pygfx.Group() if isinstance(data, TextureArray): # share buffer @@ -149,6 +149,7 @@ def __init__( self._vmax = ImageVmax(vmax) self._interpolation = ImageInterpolation(interpolation) + self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation) # set map to None for RGB images if self._data.value.ndim > 2: @@ -157,7 +158,6 @@ def __init__( else: # use TextureMap for grayscale images self._cmap = ImageCmap(cmap) - self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation) _map = pygfx.TextureMap( self._cmap.texture, @@ -173,6 +173,14 @@ def __init__( pick_write=True, ) + # create the _ImageTile world objects, add to group + for tile in self._create_image_tiles(): + group.add(tile) + + self._set_world_object(group) + + def _create_image_tiles(self) -> list[_ImageTile]: + tiles = list() # 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: @@ -193,9 +201,9 @@ def __init__( img.world.x = data_col_start img.world.y = data_row_start - world_object.add(img) + tiles.append(img) - self._set_world_object(world_object) + return tiles @property def data(self) -> TextureArray: @@ -204,6 +212,32 @@ def data(self) -> TextureArray: @data.setter def data(self, data): + # check if a new buffer is required + if self._data.value.shape != data.shape: + # create new TextureArray + self._data = TextureArray(data) + + # cmap based on if rgb or grayscale + if self._data.value.ndim > 2: + self._cmap = None + + # must be None if RGB(A) + self._material.map = None + else: + if self.cmap is None: # have switched from RGBA -> grayscale image + # create default cmap + self._cmap = ImageCmap("plasma") + self._material.map = pygfx.TextureMap(self._cmap.texture, filter=self._cmap_interpolation.value, wrap="clamp-to-edge") + + self._material.clim = quick_min_max(self.data.value) + + # clear image tiles + self.world_object.clear() + + # create new tiles + for tile in self._create_image_tiles(): + self.world_object.add(tile) + self._data[:] = data @property From d9f5ca9c35df287f6f7ad9ff569af3d4210db457 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 13 Jan 2026 02:23:45 -0500 Subject: [PATCH 06/10] resizeable buffers for volume --- fastplotlib/graphics/image.py | 8 +++++--- fastplotlib/graphics/image_volume.py | 29 +++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 5895160f5..c53520eac 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -174,12 +174,12 @@ def __init__( ) # create the _ImageTile world objects, add to group - for tile in self._create_image_tiles(): + for tile in self._create_tiles(): group.add(tile) self._set_world_object(group) - def _create_image_tiles(self) -> list[_ImageTile]: + def _create_tiles(self) -> list[_ImageTile]: tiles = list() # iterate through each texture chunk and create # an _ImageTile, offset the tile using the data indices @@ -235,9 +235,11 @@ def data(self, data): self.world_object.clear() # create new tiles - for tile in self._create_image_tiles(): + for tile in self._create_tiles(): self.world_object.add(tile) + return + self._data[:] = data @property diff --git a/fastplotlib/graphics/image_volume.py b/fastplotlib/graphics/image_volume.py index 0f0e5f23c..2bc4c4b5e 100644 --- a/fastplotlib/graphics/image_volume.py +++ b/fastplotlib/graphics/image_volume.py @@ -182,7 +182,7 @@ def __init__( super().__init__(**kwargs) - world_object = pygfx.Group() + group = pygfx.Group() if isinstance(data, TextureArrayVolume): # share existing buffer @@ -231,6 +231,15 @@ def __init__( self._mode = VolumeRenderMode(mode) + # create tiles + for tile in self._create_tiles(): + group.add(tile) + + self._set_world_object(group) + + def _create_tiles(self) -> list[_VolumeTile]: + tiles = list() + # iterate through each texture chunk and create # a _VolumeTile, offset the tile using the data indices for texture, chunk_index, data_slice in self._data: @@ -253,9 +262,9 @@ def __init__( vol.world.x = data_col_start vol.world.y = data_row_start - world_object.add(vol) + tiles.append(vol) - self._set_world_object(world_object) + return tiles @property def data(self) -> TextureArrayVolume: @@ -264,6 +273,20 @@ def data(self) -> TextureArrayVolume: @data.setter def data(self, data): + # check if a new buffer is required + if self._data.value.shape != data.shape: + # create new TextureArray + self._data = TextureArrayVolume(data) + + # clear image tiles + self.world_object.clear() + + # create new tiles + for tile in self._create_tiles(): + self.world_object.add(tile) + + return + self._data[:] = data @property From ea0d791d50243ed51454fd568492ce558780f665 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 13 Jan 2026 02:25:04 -0500 Subject: [PATCH 07/10] black --- fastplotlib/graphics/features/_mesh.py | 8 ++---- fastplotlib/graphics/features/_positions.py | 18 ++++++------ fastplotlib/graphics/features/_scatter.py | 32 ++++++++------------- fastplotlib/graphics/image.py | 6 +++- fastplotlib/graphics/mesh.py | 8 ++---- 5 files changed, 29 insertions(+), 43 deletions(-) diff --git a/fastplotlib/graphics/features/_mesh.py b/fastplotlib/graphics/features/_mesh.py index 586354117..776d77ce4 100644 --- a/fastplotlib/graphics/features/_mesh.py +++ b/fastplotlib/graphics/features/_mesh.py @@ -51,18 +51,14 @@ class MeshIndices(VertexPositions): }, ] - def __init__( - self, data: Any, property_name: str = "indices" - ): + def __init__(self, data: Any, property_name: str = "indices"): """ Manages the vertex indices buffer shown in the graphic. Supports fancy indexing if the data array also supports it. """ data = self._fix_data(data) - super().__init__( - data, property_name=property_name - ) + super().__init__(data, property_name=property_name) def _fix_data(self, data): if data.ndim != 2 or data.shape[1] not in (3, 4): diff --git a/fastplotlib/graphics/features/_positions.py b/fastplotlib/graphics/features/_positions.py index db1836539..9a6e87a73 100644 --- a/fastplotlib/graphics/features/_positions.py +++ b/fastplotlib/graphics/features/_positions.py @@ -56,11 +56,13 @@ def __init__( """ data = parse_colors(colors, n_colors) - super().__init__( - data=data, property_name=property_name - ) + super().__init__(data=data, property_name=property_name) - def set_value(self, graphic, value: str | pygfx.Color | np.ndarray | Sequence[float] | Sequence[str]): + def set_value( + self, + graphic, + value: str | pygfx.Color | np.ndarray | Sequence[float] | Sequence[str], + ): """set the entire array, create new buffer if necessary""" if isinstance(value, (np.ndarray, list, tuple)): # check if the number of elements matches current buffer size @@ -255,18 +257,14 @@ class VertexPositions(BufferManager): }, ] - def __init__( - self, data: Any, property_name: str = "data" - ): + def __init__(self, data: Any, property_name: str = "data"): """ 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, property_name=property_name - ) + super().__init__(data, property_name=property_name) def _fix_data(self, data): if data.ndim == 1: diff --git a/fastplotlib/graphics/features/_scatter.py b/fastplotlib/graphics/features/_scatter.py index faedbb9ae..d79722ef7 100644 --- a/fastplotlib/graphics/features/_scatter.py +++ b/fastplotlib/graphics/features/_scatter.py @@ -114,9 +114,7 @@ def parse_markers_init(markers: str | Sequence[str] | np.ndarray, n_datapoints: marker_str_length = max(map(len, list(pygfx.MarkerShape))) - markers_readable_array = np.empty( - n_datapoints, dtype=f" np.ndarray[str]: """numpy array of per-vertex marker shapes in human-readable form""" @@ -211,7 +205,9 @@ def set_value(self, graphic, value): if isinstance(value, (np.ndarray, list, tuple)): if self.buffer.data.shape[0] != len(value): # need to create a new buffer - markers_int_array, self._markers_readable_array = parse_markers_init(value, len(value)) + markers_int_array, self._markers_readable_array = parse_markers_init( + value, len(value) + ) self._buffer = pygfx.Buffer(markers_int_array) graphic.geometry.markers = self.buffer @@ -441,9 +437,7 @@ def __init__( Manages rotations buffer of scatter points. """ sizes = self._fix_rotations(rotations, n_datapoints) - super().__init__( - data=sizes, property_name=property_name - ) + super().__init__(data=sizes, property_name=property_name) def _fix_rotations( self, @@ -528,9 +522,7 @@ def __init__( Manages sizes buffer of scatter points. """ sizes = self._fix_sizes(sizes, n_datapoints) - super().__init__( - data=sizes, property_name=property_name - ) + super().__init__(data=sizes, property_name=property_name) def _fix_sizes( self, diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index c53520eac..9a1486f43 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -227,7 +227,11 @@ def data(self, data): if self.cmap is None: # have switched from RGBA -> grayscale image # create default cmap self._cmap = ImageCmap("plasma") - self._material.map = pygfx.TextureMap(self._cmap.texture, filter=self._cmap_interpolation.value, wrap="clamp-to-edge") + self._material.map = pygfx.TextureMap( + self._cmap.texture, + filter=self._cmap_interpolation.value, + wrap="clamp-to-edge", + ) self._material.clim = quick_min_max(self.data.value) diff --git a/fastplotlib/graphics/mesh.py b/fastplotlib/graphics/mesh.py index d569de783..bd4eea343 100644 --- a/fastplotlib/graphics/mesh.py +++ b/fastplotlib/graphics/mesh.py @@ -86,16 +86,12 @@ def __init__( if isinstance(positions, VertexPositions): self._positions = positions else: - self._positions = VertexPositions( - positions, property_name="positions" - ) + self._positions = VertexPositions(positions, property_name="positions") if isinstance(positions, MeshIndices): self._indices = indices else: - self._indices = MeshIndices( - indices, property_name="indices" - ) + self._indices = MeshIndices(indices, property_name="indices") self._cmap = MeshCmap(cmap) From 734be0ebec4e4e148feb3fd257cd1989f25f3d37 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 13 Jan 2026 02:30:35 -0500 Subject: [PATCH 08/10] buffer resize condition checked only if new value is an array --- fastplotlib/graphics/image.py | 64 ++++++++++++++-------------- fastplotlib/graphics/image_volume.py | 21 ++++----- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 9a1486f43..291128f04 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -1,6 +1,7 @@ import math from typing import * +import numpy as np import pygfx from ..utils import quick_min_max @@ -212,37 +213,38 @@ def data(self) -> TextureArray: @data.setter def data(self, data): - # check if a new buffer is required - if self._data.value.shape != data.shape: - # create new TextureArray - self._data = TextureArray(data) - - # cmap based on if rgb or grayscale - if self._data.value.ndim > 2: - self._cmap = None - - # must be None if RGB(A) - self._material.map = None - else: - if self.cmap is None: # have switched from RGBA -> grayscale image - # create default cmap - self._cmap = ImageCmap("plasma") - self._material.map = pygfx.TextureMap( - self._cmap.texture, - filter=self._cmap_interpolation.value, - wrap="clamp-to-edge", - ) - - self._material.clim = quick_min_max(self.data.value) - - # clear image tiles - self.world_object.clear() - - # create new tiles - for tile in self._create_tiles(): - self.world_object.add(tile) - - return + if isinstance(data, np.ndarray): + # check if a new buffer is required + if self._data.value.shape != data.shape: + # create new TextureArray + self._data = TextureArray(data) + + # cmap based on if rgb or grayscale + if self._data.value.ndim > 2: + self._cmap = None + + # must be None if RGB(A) + self._material.map = None + else: + if self.cmap is None: # have switched from RGBA -> grayscale image + # create default cmap + self._cmap = ImageCmap("plasma") + self._material.map = pygfx.TextureMap( + self._cmap.texture, + filter=self._cmap_interpolation.value, + wrap="clamp-to-edge", + ) + + self._material.clim = quick_min_max(self.data.value) + + # clear image tiles + self.world_object.clear() + + # create new tiles + for tile in self._create_tiles(): + self.world_object.add(tile) + + return self._data[:] = data diff --git a/fastplotlib/graphics/image_volume.py b/fastplotlib/graphics/image_volume.py index 2bc4c4b5e..c41c19eab 100644 --- a/fastplotlib/graphics/image_volume.py +++ b/fastplotlib/graphics/image_volume.py @@ -273,19 +273,20 @@ def data(self) -> TextureArrayVolume: @data.setter def data(self, data): - # check if a new buffer is required - if self._data.value.shape != data.shape: - # create new TextureArray - self._data = TextureArrayVolume(data) + if isinstance(data, np.ndarray): + # check if a new buffer is required + if self._data.value.shape != data.shape: + # create new TextureArray + self._data = TextureArrayVolume(data) - # clear image tiles - self.world_object.clear() + # clear image tiles + self.world_object.clear() - # create new tiles - for tile in self._create_tiles(): - self.world_object.add(tile) + # create new tiles + for tile in self._create_tiles(): + self.world_object.add(tile) - return + return self._data[:] = data From 7974436bddea58a04d2b2ce22aa2c7a88b3a732e Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 13 Jan 2026 18:45:03 -0500 Subject: [PATCH 09/10] gc for buffer managers --- fastplotlib/graphics/features/_positions.py | 10 +++++++++- fastplotlib/graphics/features/_scatter.py | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/fastplotlib/graphics/features/_positions.py b/fastplotlib/graphics/features/_positions.py index 9a6e87a73..df26d92c2 100644 --- a/fastplotlib/graphics/features/_positions.py +++ b/fastplotlib/graphics/features/_positions.py @@ -67,8 +67,13 @@ def set_value( if isinstance(value, (np.ndarray, list, tuple)): # check if the number of elements matches current buffer size if self.buffer.data.shape[0] != len(value): - # create new buffer + # parse the new colors new_colors = parse_colors(value, len(value)) + + # destroy old buffer + self._buffer._wgpu_object.destroy() + + # create new buffer self._buffer = pygfx.Buffer(new_colors) graphic.world_object.geometry.colors = self.buffer @@ -304,6 +309,9 @@ def set_value(self, graphic, value): bdata = np.empty(value.shape, dtype=np.float32) bdata[:] = value[:] + # destroy old buffer + self._buffer._wgpu_object.destroy() + # create the new buffer self._buffer = pygfx.Buffer(bdata) graphic.world_object.geometry.positions = self.buffer diff --git a/fastplotlib/graphics/features/_scatter.py b/fastplotlib/graphics/features/_scatter.py index d79722ef7..aef57c69e 100644 --- a/fastplotlib/graphics/features/_scatter.py +++ b/fastplotlib/graphics/features/_scatter.py @@ -208,6 +208,11 @@ def set_value(self, graphic, value): markers_int_array, self._markers_readable_array = parse_markers_init( value, len(value) ) + + # destroy old buffer + self._buffer._wgpu_object.destroy() + + # set new buffer self._buffer = pygfx.Buffer(markers_int_array) graphic.geometry.markers = self.buffer @@ -475,6 +480,11 @@ def set_value(self, graphic, value): # need to create a new buffer value = self._fix_rotations(value, len(value)) data = np.empty(shape=(len(value),), dtype=np.float32) + + # destroy old buffer + self._buffer._wgpu_object.destroy() + + # set new buffer self._buffer = pygfx.Buffer(data) graphic.world_object.geometry.rotations = self.buffer self._emit_event(self._property_name, key=slice(None), value=value) @@ -565,8 +575,14 @@ def set_value(self, graphic, value): # create new buffer value = self._fix_sizes(value, len(value)) data = np.empty(shape=(len(value),), dtype=np.float32) + + # destroy old buffer + self._buffer._wgpu_object.destroy() + + # set new buffer self._buffer = pygfx.Buffer(data) graphic.geometry.sizes = self.buffer + self._emit_event(self._property_name, key=slice(None), value=value) return From fe749ce3587b5ca007d84d7fd29838f7031145ad Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jan 2026 01:35:17 -0500 Subject: [PATCH 10/10] uniform colors WIP --- examples/gridplot/multigraphic_gridplot.py | 2 +- examples/line/line.py | 4 +- examples/line/line_cmap.py | 6 ++- examples/line/line_cmap_more.py | 30 +++++++++++-- examples/line/line_colorslice.py | 6 ++- examples/line/line_dataslice.py | 4 +- .../line_collection_slicing.py | 1 + examples/scatter/scatter.py | 2 +- examples/scatter/scatter_cmap.py | 2 +- fastplotlib/graphics/_positions_base.py | 6 +-- fastplotlib/graphics/features/_positions.py | 21 ++++++++-- fastplotlib/graphics/features/_scatter.py | 9 ++-- fastplotlib/graphics/line.py | 9 ++-- fastplotlib/graphics/line_collection.py | 4 +- fastplotlib/graphics/scatter.py | 33 ++++++++------- fastplotlib/layouts/_graphic_methods_mixin.py | 42 ++++++++++--------- 16 files changed, 118 insertions(+), 63 deletions(-) diff --git a/examples/gridplot/multigraphic_gridplot.py b/examples/gridplot/multigraphic_gridplot.py index cbf546e2a..2ccb3a6e0 100644 --- a/examples/gridplot/multigraphic_gridplot.py +++ b/examples/gridplot/multigraphic_gridplot.py @@ -106,7 +106,7 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: gaussian_cloud2 = np.random.multivariate_normal(mean, covariance, n_points) # add the scatter graphics to the figure -figure["scatter"].add_scatter(data=gaussian_cloud, sizes=2, cmap="jet") +figure["scatter"].add_scatter(data=gaussian_cloud, sizes=2, cmap="jet", uniform_color=False) figure["scatter"].add_scatter(data=gaussian_cloud2, colors="r", sizes=2) figure.show() diff --git a/examples/line/line.py b/examples/line/line.py index f7839a1c4..e388adb21 100644 --- a/examples/line/line.py +++ b/examples/line/line.py @@ -30,11 +30,11 @@ sine = figure[0, 0].add_line(data=sine_data, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine = figure[0, 0].add_line(data=cosine_data, thickness=12, cmap="autumn") +cosine = figure[0, 0].add_line(data=cosine_data, thickness=12, cmap="autumn", uniform_color=False) # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc = figure[0, 0].add_line(data=sinc_data, thickness=5, colors=colors) +sinc = figure[0, 0].add_line(data=sinc_data, thickness=5, colors=colors, uniform_color=False) figure[0, 0].axes.grids.xy.visible = True figure.show() diff --git a/examples/line/line_cmap.py b/examples/line/line_cmap.py index 3d2b5e8c9..9e07a5aeb 100644 --- a/examples/line/line_cmap.py +++ b/examples/line/line_cmap.py @@ -27,7 +27,8 @@ data=sine_data, thickness=10, cmap="plasma", - cmap_transform=sine_data[:, 1] + cmap_transform=sine_data[:, 1], + uniform_color=False, ) # qualitative colormaps, useful for cluster labels or other types of categorical labels @@ -36,7 +37,8 @@ data=cosine_data, thickness=10, cmap="tab10", - cmap_transform=labels + cmap_transform=labels, + uniform_color=False, ) figure.show() diff --git a/examples/line/line_cmap_more.py b/examples/line/line_cmap_more.py index c7c0d80f4..cb3b22808 100644 --- a/examples/line/line_cmap_more.py +++ b/examples/line/line_cmap_more.py @@ -26,21 +26,43 @@ line0 = figure[0, 0].add_line(sine, thickness=10) # set colormap along line datapoints, use an offset to place it above the previous line -line1 = figure[0, 0].add_line(sine, thickness=10, cmap="jet", offset=(0, 2, 0)) +line1 = figure[0, 0].add_line(sine, thickness=10, cmap="jet", uniform_color=False, offset=(0, 2, 0)) # set colormap by mapping data using a transform # here we map the color using the y-values of the sine data # i.e., the color is a function of sine(x) -line2 = figure[0, 0].add_line(sine, thickness=10, cmap="jet", cmap_transform=sine[:, 1], offset=(0, 4, 0)) +line2 = figure[0, 0].add_line( + sine, + thickness=10, + cmap="jet", + cmap_transform=sine[:, 1], + uniform_color=False, + offset=(0, 4, 0), +) # make a line and change the cmap afterward, here we are using the cosine instead fot the transform -line3 = figure[0, 0].add_line(sine, thickness=10, cmap="jet", cmap_transform=cosine[:, 1], offset=(0, 6, 0)) +line3 = figure[0, 0].add_line( + sine, + thickness=10, + cmap="jet", + cmap_transform=cosine[:, 1], + uniform_color=False, + offset=(0, 6, 0) +) + # change the cmap line3.cmap = "bwr" # use quantitative colormaps with categorical cmap_transforms labels = [0] * 25 + [1] * 5 + [2] * 50 + [3] * 20 -line4 = figure[0, 0].add_line(sine, thickness=10, cmap="tab10", cmap_transform=labels, offset=(0, 8, 0)) +line4 = figure[0, 0].add_line( + sine, + thickness=10, + cmap="tab10", + cmap_transform=labels, + uniform_color=False, + offset=(0, 8, 0), +) # some text labels for i in range(5): diff --git a/examples/line/line_colorslice.py b/examples/line/line_colorslice.py index b6865eadb..2a74c73cf 100644 --- a/examples/line/line_colorslice.py +++ b/examples/line/line_colorslice.py @@ -30,7 +30,8 @@ sine = figure[0, 0].add_line( data=sine_data, thickness=5, - colors="magenta" + colors="magenta", + uniform_color=False, # initialize with same color across vertices, but we will change the per-vertex colors later ) # you can also use colormaps for lines! @@ -38,6 +39,7 @@ data=cosine_data, thickness=12, cmap="autumn", + uniform_color=False, offset=(0, 3, 0) # places the graphic at a y-axis offset of 3, offsets don't affect data ) @@ -47,6 +49,7 @@ data=sinc_data, thickness=5, colors=colors, + uniform_color=False, offset=(0, 6, 0) ) @@ -56,6 +59,7 @@ data=zeros_data, thickness=8, colors="w", + uniform_color=False, # initialize with same color across vertices, but we will change the per-vertex colors later offset=(0, 10, 0) ) diff --git a/examples/line/line_dataslice.py b/examples/line/line_dataslice.py index 6ef9d0d90..def3f678f 100644 --- a/examples/line/line_dataslice.py +++ b/examples/line/line_dataslice.py @@ -30,11 +30,11 @@ sine = figure[0, 0].add_line(data=sine_data, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine = figure[0, 0].add_line(data=cosine_data, thickness=12, cmap="autumn") +cosine = figure[0, 0].add_line(data=cosine_data, thickness=12, cmap="autumn", uniform_color=False) # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc = figure[0, 0].add_line(data=sinc_data, thickness=5, colors=colors) +sinc = figure[0, 0].add_line(data=sinc_data, thickness=5, colors=colors, uniform_color=False) figure.show() diff --git a/examples/line_collection/line_collection_slicing.py b/examples/line_collection/line_collection_slicing.py index f829a53c6..022ca094a 100644 --- a/examples/line_collection/line_collection_slicing.py +++ b/examples/line_collection/line_collection_slicing.py @@ -26,6 +26,7 @@ multi_data, thickness=[2, 10, 2, 5, 5, 5, 8, 8, 8, 9, 3, 3, 3, 4, 4], separation=4, + uniform_color=False, metadatas=list(range(15)), # some metadata names=list("abcdefghijklmno"), # unique name for each line ) diff --git a/examples/scatter/scatter.py b/examples/scatter/scatter.py index 838199ecb..973a55512 100644 --- a/examples/scatter/scatter.py +++ b/examples/scatter/scatter.py @@ -36,7 +36,7 @@ colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points # use an alpha value since this will be a lot of points -figure[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) +figure[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, uniform_color=False, alpha=0.6) figure.show() diff --git a/examples/scatter/scatter_cmap.py b/examples/scatter/scatter_cmap.py index 3c7bd0e21..24f5e00ab 100644 --- a/examples/scatter/scatter_cmap.py +++ b/examples/scatter/scatter_cmap.py @@ -36,7 +36,7 @@ colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points # use an alpha value since this will be a lot of points -figure[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) +figure[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, uniform_color=False, alpha=0.6) figure.show() diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index a341b4077..1b639a127 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -71,7 +71,7 @@ def __init__( self, data: Any, colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", - uniform_color: bool = False, + uniform_color: bool = True, cmap: str | VertexCmap = None, cmap_transform: np.ndarray = None, size_space: str = "screen", @@ -89,7 +89,7 @@ def __init__( if cmap is not None: # if a cmap is specified it overrides colors argument if uniform_color: - raise TypeError("Cannot use cmap if uniform_color=True") + raise TypeError("Cannot use `cmap` if `uniform_color=True`, pass `uniform_color=False` to use `cmap`.") if isinstance(cmap, str): # make colors from cmap @@ -127,7 +127,7 @@ def __init__( 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`" + "Must pass `uniform_colors=False` if using multiple colors" ) self._colors = UniformColor(colors) self._cmap = None diff --git a/fastplotlib/graphics/features/_positions.py b/fastplotlib/graphics/features/_positions.py index df26d92c2..1187b4c88 100644 --- a/fastplotlib/graphics/features/_positions.py +++ b/fastplotlib/graphics/features/_positions.py @@ -65,13 +65,27 @@ def set_value( ): """set the entire array, create new buffer if necessary""" if isinstance(value, (np.ndarray, list, tuple)): + # TODO: Refactor this triage so it's more elegant + + # first make sure it's not representing one color + skip = False + if isinstance(value, np.ndarray): + if (value.shape in ((3,), (4,))) and (np.issubdtype(value.dtype, np.floating) or np.issubdtype(value.dtype, np.integer)): + # represents one color + skip = True + elif isinstance(value, (list, tuple)): + if len(value) in (3, 4) and all([isinstance(v, (float, int)) for v in value]): + # represents one color + skip = True + # check if the number of elements matches current buffer size - if self.buffer.data.shape[0] != len(value): + if not skip and self.buffer.data.shape[0] != len(value): # parse the new colors new_colors = parse_colors(value, len(value)) # destroy old buffer - self._buffer._wgpu_object.destroy() + if self._buffer._wgpu_object is not None: + self._buffer._wgpu_object.destroy() # create new buffer self._buffer = pygfx.Buffer(new_colors) @@ -310,7 +324,8 @@ def set_value(self, graphic, value): bdata[:] = value[:] # destroy old buffer - self._buffer._wgpu_object.destroy() + if self._buffer._wgpu_object is not None: + self._buffer._wgpu_object.destroy() # create the new buffer self._buffer = pygfx.Buffer(bdata) diff --git a/fastplotlib/graphics/features/_scatter.py b/fastplotlib/graphics/features/_scatter.py index aef57c69e..ce10fe4a8 100644 --- a/fastplotlib/graphics/features/_scatter.py +++ b/fastplotlib/graphics/features/_scatter.py @@ -210,7 +210,8 @@ def set_value(self, graphic, value): ) # destroy old buffer - self._buffer._wgpu_object.destroy() + if self._buffer._wgpu_object is not None: + self._buffer._wgpu_object.destroy() # set new buffer self._buffer = pygfx.Buffer(markers_int_array) @@ -482,7 +483,8 @@ def set_value(self, graphic, value): data = np.empty(shape=(len(value),), dtype=np.float32) # destroy old buffer - self._buffer._wgpu_object.destroy() + if self._buffer._wgpu_object is not None: + self._buffer._wgpu_object.destroy() # set new buffer self._buffer = pygfx.Buffer(data) @@ -577,7 +579,8 @@ def set_value(self, graphic, value): data = np.empty(shape=(len(value),), dtype=np.float32) # destroy old buffer - self._buffer._wgpu_object.destroy() + if self._buffer._wgpu_object is not None: + self._buffer._wgpu_object.destroy() # set new buffer self._buffer = pygfx.Buffer(data) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index ed7a95490..93d1aa4c1 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -36,7 +36,7 @@ def __init__( data: Any, thickness: float = 2.0, colors: str | np.ndarray | Sequence = "w", - uniform_color: bool = False, + uniform_color: bool = True, cmap: str = None, cmap_transform: np.ndarray | Sequence = None, size_space: str = "screen", @@ -60,9 +60,10 @@ def __init__( specify colors as a single human-readable string, a single RGBA array, or a Sequence (array, tuple, or list) 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 + uniform_color: bool, default ``True`` + if ``True``, uses a uniform buffer for the line color, + basically saves GPU VRAM when the entire line has a single color. + If ``False``, you can set per-vertex colors. cmap: str, optional Apply a colormap to the line instead of assigning colors manually, this diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 2087ada62..8ef1c4d8e 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -128,7 +128,7 @@ def __init__( 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, + uniform_color: bool = True, cmap: Sequence[str] | str = None, cmap_transform: np.ndarray | List = None, name: str = None, @@ -319,7 +319,7 @@ def __init__( data=d, thickness=_s, colors=_c, - uniform_color=uniform_colors, + uniform_color=uniform_color, cmap=_cmap, name=_name, metadata=_m, diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 3f9a530ef..0464fd528 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -40,12 +40,12 @@ def __init__( self, data: Any, colors: str | np.ndarray | Sequence[float] | Sequence[str] = "w", - uniform_color: bool = False, + uniform_color: bool = True, cmap: str = None, cmap_transform: np.ndarray = None, mode: Literal["markers", "simple", "gaussian", "image"] = "markers", markers: str | np.ndarray | Sequence[str] = "o", - uniform_marker: bool = False, + uniform_marker: bool = True, custom_sdf: str = None, edge_colors: str | np.ndarray | pygfx.Color | Sequence[float] = "black", uniform_edge_color: bool = True, @@ -71,14 +71,15 @@ def __init__( specify colors as a single human-readable string, a single RGBA array, or a Sequence (array, tuple, or list) of strings or RGBA arrays - uniform_color: bool, default False - if True, uses a uniform buffer for the scatter point colors. Useful if you need to - save GPU VRAM when all points have the same color. + uniform_color: bool, default ``True`` + if ``True``, uses a uniform buffer for the scatter point colors. Useful if you need to + save GPU VRAM when all points have the same color. If ``False``, you can set per-vertex colors. cmap: str, optional apply a colormap to the scatter instead of assigning colors manually, this - overrides any argument passed to "colors". For supported colormaps see the - ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ + overrides any argument passed to "colors". + For supported colormaps see the ``cmap`` library catalogue: + https://cmap-docs.readthedocs.io/en/stable/catalog/ cmap_transform: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap @@ -102,9 +103,10 @@ def __init__( * Emojis: "❤️♠️♣️♦️💎💍✳️📍". * A string containing the value "custom". In this case, WGSL code defined by ``custom_sdf`` will be used. - uniform_marker: bool, default False - Use the same marker for all points. Only valid when `mode` is "markers". Useful if you need to use - the same marker for all points and want to save GPU RAM. + uniform_marker: bool, default ``True`` + If ``True``, use the same marker for all points. Only valid when `mode` is "markers". + Useful if you need to use the same marker for all points and want to save GPU RAM. If ``False``, you can + set per-vertex markers. custom_sdf: str = None, The SDF code for the marker shape when the marker is set to custom. @@ -124,8 +126,9 @@ def __init__( edge_colors: str | np.ndarray | pygfx.Color | Sequence[float], default "black" edge color of the markers, used when `mode` is "markers" - uniform_edge_color: bool, default True - Set the same edge color for all markers. Useful for saving GPU RAM. + uniform_edge_color: bool, default ``True`` + Set the same edge color for all markers. Useful for saving GPU RAM. Set to ``False`` for per-vertex edge + colors edge_width: float = 1.0, Width of the marker edges. used when `mode` is "markers". @@ -146,9 +149,9 @@ def __init__( sizes: float or iterable of float, optional, default 1.0 sizes of the scatter points - uniform_size: bool, default False - if True, uses a uniform buffer for the scatter point sizes. Useful if you need to - save GPU VRAM when all points have the same size. + uniform_size: bool, default ``False`` + if ``True``, uses a uniform buffer for the scatter point sizes. Useful if you need to + save GPU VRAM when all points have the same size. Set to ``False`` if you need per-vertex sizes. size_space: str, default "screen" coordinate space in which the size is expressed, one of ("screen", "world", "model") diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index 2d8aa2eab..b6610f456 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -261,7 +261,7 @@ def add_line( data: Any, thickness: float = 2.0, colors: Union[str, numpy.ndarray, Sequence] = "w", - uniform_color: bool = False, + uniform_color: bool = True, cmap: str = None, cmap_transform: Union[numpy.ndarray, Sequence] = None, size_space: str = "screen", @@ -286,9 +286,10 @@ def add_line( specify colors as a single human-readable string, a single RGBA array, or a Sequence (array, tuple, or list) 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 + uniform_color: bool, default ``True`` + if ``True``, uses a uniform buffer for the line color, + basically saves GPU VRAM when the entire line has a single color. + If ``False``, you can set per-vertex colors. cmap: str, optional Apply a colormap to the line instead of assigning colors manually, this @@ -545,12 +546,12 @@ def add_scatter( self, data: Any, colors: Union[str, numpy.ndarray, Sequence[float], Sequence[str]] = "w", - uniform_color: bool = False, + uniform_color: bool = True, cmap: str = None, cmap_transform: numpy.ndarray = None, mode: Literal["markers", "simple", "gaussian", "image"] = "markers", markers: Union[str, numpy.ndarray, Sequence[str]] = "o", - uniform_marker: bool = False, + uniform_marker: bool = True, custom_sdf: str = None, edge_colors: Union[ str, pygfx.utils.color.Color, numpy.ndarray, Sequence[float] @@ -579,14 +580,15 @@ def add_scatter( specify colors as a single human-readable string, a single RGBA array, or a Sequence (array, tuple, or list) of strings or RGBA arrays - uniform_color: bool, default False - if True, uses a uniform buffer for the scatter point colors. Useful if you need to - save GPU VRAM when all points have the same color. + uniform_color: bool, default ``True`` + if ``True``, uses a uniform buffer for the scatter point colors. Useful if you need to + save GPU VRAM when all points have the same color. If ``False``, you can set per-vertex colors. cmap: str, optional apply a colormap to the scatter instead of assigning colors manually, this - overrides any argument passed to "colors". For supported colormaps see the - ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ + overrides any argument passed to "colors". + For supported colormaps see the ``cmap`` library catalogue: + https://cmap-docs.readthedocs.io/en/stable/catalog/ cmap_transform: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap @@ -610,9 +612,10 @@ def add_scatter( * Emojis: "❤️♠️♣️♦️💎💍✳️📍". * A string containing the value "custom". In this case, WGSL code defined by ``custom_sdf`` will be used. - uniform_marker: bool, default False - Use the same marker for all points. Only valid when `mode` is "markers". Useful if you need to use - the same marker for all points and want to save GPU RAM. + uniform_marker: bool, default ``True`` + If ``True``, use the same marker for all points. Only valid when `mode` is "markers". + Useful if you need to use the same marker for all points and want to save GPU RAM. If ``False``, you can + set per-vertex markers. custom_sdf: str = None, The SDF code for the marker shape when the marker is set to custom. @@ -632,8 +635,9 @@ def add_scatter( edge_colors: str | np.ndarray | pygfx.Color | Sequence[float], default "black" edge color of the markers, used when `mode` is "markers" - uniform_edge_color: bool, default True - Set the same edge color for all markers. Useful for saving GPU RAM. + uniform_edge_color: bool, default ``True`` + Set the same edge color for all markers. Useful for saving GPU RAM. Set to ``False`` for per-vertex edge + colors edge_width: float = 1.0, Width of the marker edges. used when `mode` is "markers". @@ -654,9 +658,9 @@ def add_scatter( sizes: float or iterable of float, optional, default 1.0 sizes of the scatter points - uniform_size: bool, default False - if True, uses a uniform buffer for the scatter point sizes. Useful if you need to - save GPU VRAM when all points have the same size. + uniform_size: bool, default ``False`` + if ``True``, uses a uniform buffer for the scatter point sizes. Useful if you need to + save GPU VRAM when all points have the same size. Set to ``False`` if you need per-vertex sizes. size_space: str, default "screen" coordinate space in which the size is expressed, one of ("screen", "world", "model")