diff --git a/examples/scatter/scatter_cmap_transform.py b/examples/scatter/scatter_cmap_transform.py new file mode 100644 index 000000000..f3c5e79b6 --- /dev/null +++ b/examples/scatter/scatter_cmap_transform.py @@ -0,0 +1,68 @@ +""" +Scatter custom cmap +=================== + +Use a cmap_transform to define how to map colors to scatter points from a custom defined cmap. +This is also valid for line graphics. + +This is identical to the scatter.py example but the cmap_transform is sometimes a better way to define the colors. +It may also be more performant if millions of strings for each point do not have to be parsed into colors. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import numpy as np + +figure = fpl.Figure(size=(700, 560)) + +# create a random distribution of 15,000 xyz coordinates +n_points = 5_000 + +# dimensions always have to be [n_points, xyz] +dims = (n_points, 3) + +clouds_offset = 15 + +# create some random clouds +normal = np.random.normal(size=dims, scale=5) +# stack the data into a single array +cloud = np.vstack( + [ + normal - clouds_offset, + normal, + normal + clouds_offset, + ] +) + +# we have 3 clouds, create a 1D array where the value indicates cloud membership +# this will be used to map the colors from the cmap onto the corresponding point +cmap_transform = np.empty(cloud.shape[0]) + +# first cloud is given a value of 0 +cmap_transform[:n_points] = 0 + +# second cloud is given a value of 1 +cmap_transform[n_points: 2 * n_points] = 1 + +# 3rd cloud given a value of 2 +cmap_transform[2 * n_points:] = 2 + + +figure[0, 0].add_scatter( + data=cloud, + sizes=3, + cmap=["green", "purple", "blue"], # custom cmap + cmap_transform=cmap_transform, # each element of the cmap_transform maps to the corresponding datapoint + alpha=0.6 +) + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 5d98d16d1..654a4387e 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -74,7 +74,7 @@ def __init__( colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", uniform_color: bool = False, alpha: float = 1.0, - cmap: str | VertexCmap = None, + cmap: str | list[str] | tuple[str] | VertexCmap = None, cmap_transform: np.ndarray = None, isolated_buffer: bool = True, size_space: str = "screen", @@ -94,7 +94,13 @@ def __init__( if uniform_color: raise TypeError("Cannot use cmap if uniform_color=True") - if isinstance(cmap, str): + if isinstance(cmap, (str, list, tuple)): + if isinstance(cmap, (list, tuple)): + if not all(isinstance(s, str) for s in cmap): + raise TypeError( + "`cmap` argument must be a cmap name, a list/tuple of " + "defining a custom cmap, or an existing `VertexCmap` instance" + ) # make colors from cmap if isinstance(colors, VertexColors): # share buffer with existing colors instance for the cmap @@ -116,7 +122,8 @@ def __init__( self._colors = cmap._vertex_colors else: raise TypeError( - "`cmap` argument must be a cmap name or an existing `VertexCmap` instance" + "`cmap` argument must be a cmap name, a list/tuple of " + "defining a custom cmap, or an existing `VertexCmap` instance" ) else: # no cmap given diff --git a/fastplotlib/graphics/features/_positions_graphics.py b/fastplotlib/graphics/features/_positions_graphics.py index 868701079..b9a5f2fbf 100644 --- a/fastplotlib/graphics/features/_positions_graphics.py +++ b/fastplotlib/graphics/features/_positions_graphics.py @@ -417,7 +417,7 @@ class VertexCmap(BufferManager): }, { "dict key": "value", - "type": "str", + "type": "str | list[str] | tuple[str]", "description": "new cmap to set at given slice", }, ] @@ -425,7 +425,7 @@ class VertexCmap(BufferManager): def __init__( self, vertex_colors: VertexColors, - cmap_name: str | None, + cmap_name: str | list[str] | tuple[str] | None, transform: np.ndarray | None, alpha: float = 1.0, ): @@ -442,10 +442,17 @@ def __init__( self._alpha = alpha if self._cmap_name is not None: - if not isinstance(self._cmap_name, str): + if not isinstance(self._cmap_name, (str, list, tuple)): raise TypeError( - f"cmap name must be of type , you have passed: {self._cmap_name} of type: {type(self._cmap_name)}" + f"cmap name must be of type , or list/tuple of str to define a custom cmap. " + f"You have passed: {self._cmap_name} of type: {type(self._cmap_name)}" ) + if isinstance(cmap_name, (list, tuple)): + if not all(isinstance(s, str) for s in cmap_name): + raise TypeError( + f"cmap name must be of type , or list/tuple of str to define a custom cmap. " + f"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/utils/functions.py b/fastplotlib/utils/functions.py index 6ad365e40..f88ce607c 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -341,7 +341,7 @@ def normalize_min_max(a): def parse_cmap_values( n_colors: int, - cmap_name: str, + cmap_name: str | list[str] | tuple[str], transform: np.ndarray | list[int | float] = None, ) -> np.ndarray: """