Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions fastplotlib/graphics/_positions_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,18 @@ 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.
"""
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
Expand All @@ -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
Expand Down
163 changes: 62 additions & 101 deletions fastplotlib/graphics/features/_positions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import numpy as np
import pygfx
import cmap as cmap_lib

from ...utils import (
parse_cmap_values,
Expand Down Expand Up @@ -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 <str>, 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):
Expand Down