From c221cff0e10a96b2553142264e57949b47bdd5af Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Aug 2026 02:25:43 -0400 Subject: [PATCH 1/2] refactor cmap and cmap_transform for positional graphics --- fastplotlib/graphics/features/_positions.py | 163 ++++++++------------ 1 file changed, 62 insertions(+), 101 deletions(-) diff --git a/fastplotlib/graphics/features/_positions.py b/fastplotlib/graphics/features/_positions.py index 2ede10b8b..c6e238026 100644 --- a/fastplotlib/graphics/features/_positions.py +++ b/fastplotlib/graphics/features/_positions.py @@ -2,6 +2,7 @@ import numpy as np import pygfx +import cmap as cmap_lib from ...utils import ( parse_cmap_values, @@ -339,138 +340,98 @@ def __len__(self): return len(self.buffer.data) -class VertexCmap(BufferManager): +class VertexCmap(GraphicFeature): event_info_spec = [ - { - "dict key": "key", - "type": "slice", - "description": "key at cmap colors were sliced", - }, { "dict key": "value", - "type": "str", - "description": "new cmap to set at given slice", + "type": "cmap.Colormap", + "description": "new colormap", }, ] def __init__( self, - vertex_colors: VertexColors, - cmap_name: str | None, - transform: np.ndarray | None, - property_name: str = "colors", + value: cmap_lib.ColormapLike, + property_name: str = "cmap", ): """ - Sliceable colormap feature, manages a VertexColors instance and - provides a way to set colormaps with arbitrary transforms + colormap feature, manages a VertexColors instance and provides a way to set colormaps. """ + self._value = cmap_lib.Colormap(value) - super().__init__(data=None, property_name=property_name) - - self._vertex_colors = vertex_colors - self._cmap_name = cmap_name - self._transform = transform - - if self._cmap_name is not None: - if not isinstance(self._cmap_name, str): - 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) - - n_datapoints = vertex_colors.value.shape[0] - - colors = parse_cmap_values( - n_colors=n_datapoints, - cmap_name=self._cmap_name, - transform=self._transform, - ) - # set vertex colors from cmap - self._vertex_colors[:] = colors - - @property - def buffer(self) -> pygfx.Buffer: - return self._vertex_colors.buffer + super().__init__(property_name=property_name) @property - def value(self) -> np.ndarray: - # mirror the managed colors feature, whose length is the number of color entries - # (this is per-line, not per-vertex, for an InfLineColors) - return self._vertex_colors.value + def value(self) -> cmap_lib.Colormap: + return self._value @block_reentrance - 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 range are supported for applying a cmap" - ) - if key.step is not None: - raise TypeError( - "step sized indexing not currently supported for setting VertexCmap, " - "slices must be a continuous range" - ) + def set_value(self, graphic, value: cmap_lib.ColormapLike): + self._value = cmap_lib.Colormap(value) + pygfx.TextureMap + + # directly set the material map using the TextureMap + graphic.world_object.material.map = self._value.to_pygfx() + graphic.world_object.geometry.texcoords - # parse slice - start, stop, step = key.indices(self.value.shape[0]) - n_elements = len(range(start, stop, step)) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) + self._call_event_handlers(event) + self.value.__rich_repr__() - colors = parse_cmap_values( - n_colors=n_elements, cmap_name=cmap_name, transform=self._transform - ) + def __repr__(self): + return self.value.__repr__() - self._cmap_name = cmap_name - self._vertex_colors[key] = colors + def _repr_html_(self): + return self.value._repr_html_() - # 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(self._property_name, key, cmap_name) + def _repr_png(self): + return self.value._repr_png_() - @property - def name(self) -> str: - return self._cmap_name - @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 +class VertexCmapTransform(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray", + "description": "colormap transform", + }, + ] - @transform.setter - def transform( - self, - values: np.ndarray | list[float | int], - indices: slice | list | np.ndarray = None, + def __init__( + self, + value: np.ndarray, + property_name: str = "cmap_transform" ): - if self._cmap_name is None: - raise AttributeError( - "cmap name is not set, set the cmap name before setting the transform" - ) + """colormap transform""" - values = np.asarray(values) - - colors = parse_cmap_values( - n_colors=self.value.shape[0], cmap_name=self._cmap_name, transform=values - ) + self._value = np.asarray(value) + super().__init__(property_name=property_name) - self._transform = values + @property + def valeu(self) -> np.ndarray: + return self._value - if indices is None: - indices = slice(None) + @block_reentrance + def set_value(self, graphic, value: np.ndarray): + value = np.asarray(value).squeeze() - self._vertex_colors[indices] = colors + # make sure transform value is provided for every datapoint + n_datapoints = len(graphic.world_object.geometry.positions.data) + if value.size != n_datapoints: + raise ValueError( + f"`cmap_transform` must be a 1D array with a size that matches the number of datapoints\n" + f"you provided a `cmap_transform` with {value.size} elements but you have {n_datapoints} datapoints." + ) - self._emit_event("cmap.transform", indices, values) + if graphic.world_object.geometry.texcoords is not None: + graphic.world_object.geometry.texcoords[:] = value + else: + graphic.world_object.geometry.texcoords = pygfx.Buffer(self.value) - def __len__(self): - raise NotImplementedError( - "len not implemented for `cmap`, use len(colors) instead" - ) + self._value = graphic.world_object.geometry.texcoords.data - def __repr__(self): - return f"{self.__class__.__name__} | cmap: {self.name}\ntransform: {self.transform}" + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) + self._call_event_handlers(event) class InfLineAxisData(VertexPositions): From 125a128f7d21165bcb201093e2cf3f3d353d3126 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Aug 2026 02:31:32 -0400 Subject: [PATCH 2/2] color mode stuff --- fastplotlib/graphics/_positions_base.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 426079730..8673299a9 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -52,7 +52,7 @@ def colors(self, value: str | np.ndarray | Sequence[float] | Sequence[str]): self._colors.set_value(self, value) @property - def color_mode(self) -> Literal["uniform", "vertex"]: + def color_mode(self) -> pygfx.enums.ColorMode: """ Get or set the color mode. Note that after setting the color_mode, you will have to set the `colors` as well for switching between 'uniform' and 'vertex' modes. @@ -60,10 +60,10 @@ def color_mode(self) -> Literal["uniform", "vertex"]: return self.world_object.material.color_mode @color_mode.setter - def color_mode(self, mode: Literal["uniform", "vertex"]): - valid = ("uniform", "vertex") - if mode not in valid: - raise ValueError(f"`color_mode` must be one of : {valid}") + def color_mode(self, mode: pygfx.enums.ColorMode): + if mode not in pygfx.enums.ColorMode: + raise ValueError(f"`color_mode` must be one of : {pygfx.enums.ColorMode}, not {mode!r}") + if mode == "vertex" and isinstance(self._colors, UniformColor): # uniform -> vertex # need to make a new vertex buffer and get rid of uniform buffer @@ -87,6 +87,10 @@ def color_mode(self, mode: Literal["uniform", "vertex"]): self._cmap.clear_event_handlers() self._cmap = None + elif mode == "vertex_map": + # TODO: handle new cmap stuff + pass + else: # no change, return return