Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
c221cff
refactor cmap and cmap_transform for positional graphics
kushalkolar Aug 8, 2026
125a128
color mode stuff
kushalkolar Aug 8, 2026
5e0bd95
WIP
kushalkolar Aug 13, 2026
b269f07
more WIP
kushalkolar Aug 14, 2026
3914db6
better add_graphic autogen
kushalkolar Aug 19, 2026
419069a
more progress
kushalkolar Aug 19, 2026
8fe4054
mostly done refactoring positions graphics cmap stuff
kushalkolar Aug 20, 2026
ad2e1d0
forgot to update mixin
kushalkolar Aug 20, 2026
782017f
cmap_transform tweaks
kushalkolar Aug 24, 2026
b102462
wip, JaggedArray
kushalkolar Aug 30, 2026
1f3def7
mostly works, writing tests
kushalkolar Sep 1, 2026
42aceb9
remove a print
kushalkolar Sep 1, 2026
0284cfa
image collection examples
kushalkolar Sep 2, 2026
c9fe59d
fix
kushalkolar Sep 2, 2026
21c9f68
feature inheritance in PositionsGraphic and cmap_range
kushalkolar Sep 2, 2026
b1f8d2e
cmap handling
kushalkolar Sep 2, 2026
ebfa98a
cmap tweaks
kushalkolar Sep 3, 2026
bff755b
remove parse_cmap_values, rely on cmap lib
kushalkolar Sep 3, 2026
f9c23f2
fix example, remove unused line of code
kushalkolar Sep 3, 2026
665952c
Image just uses cmap lib now too
kushalkolar Sep 3, 2026
f43d25f
delete_ndgraphic()
kushalkolar Sep 3, 2026
e3035c6
ndtimeseries cmap example
kushalkolar Sep 3, 2026
a997728
steps for stacks
kushalkolar Sep 3, 2026
565a689
ndtimeseries sets stack steps
kushalkolar Sep 3, 2026
467029f
docstrings
kushalkolar Sep 5, 2026
bc844c0
docstrings
kushalkolar Sep 5, 2026
72371e1
comments
kushalkolar Sep 5, 2026
7fb798b
renames
kushalkolar Sep 5, 2026
d51d19c
docstring
kushalkolar Sep 5, 2026
29c95f2
fixes
kushalkolar Sep 8, 2026
687e10a
update tests/test_colors_buffer_manager.py
kushalkolar Sep 9, 2026
a23cfb0
update test_markers_buffer_manager.py
kushalkolar Sep 9, 2026
a9d9444
update plot_helpers test
kushalkolar Sep 9, 2026
c52e829
update test_point_rotations_buffer_manager.py
kushalkolar Sep 9, 2026
7246c96
update test_positions_graphics.py
kushalkolar Sep 9, 2026
3225331
update another test
kushalkolar Sep 9, 2026
ebbae33
more tests
kushalkolar Sep 9, 2026
a568853
update yet more tests
kushalkolar Sep 9, 2026
aec89c9
remove unused
kushalkolar Sep 9, 2026
a32d3af
infline tests
kushalkolar Sep 9, 2026
650fd29
collections tests
kushalkolar Sep 9, 2026
c024321
updating examples, wip
kushalkolar Sep 9, 2026
45ea886
more examples updates
kushalkolar Sep 9, 2026
4a8f444
more examples updates
kushalkolar Sep 9, 2026
92e34d9
more example updates
kushalkolar Sep 9, 2026
f0ddde1
more example updates
kushalkolar Sep 9, 2026
7b04a0d
example and docstring
kushalkolar Sep 9, 2026
55864b0
docstring
kushalkolar Sep 9, 2026
2554a3d
remove complex line collection slicing example, stick with real usecases
kushalkolar Sep 9, 2026
4148e3c
better line stack example
kushalkolar Sep 9, 2026
b6b80ba
update example
kushalkolar Sep 9, 2026
d6a8fb8
update example
kushalkolar Sep 9, 2026
712d94b
update link
kushalkolar Sep 9, 2026
2949c81
update more examples
kushalkolar Sep 9, 2026
a5dd607
examples and fixes
kushalkolar Sep 9, 2026
2d59e6a
fix
kushalkolar Sep 9, 2026
b57d0a4
more fixes
kushalkolar Sep 9, 2026
df7c823
fix
kushalkolar Sep 9, 2026
cfdfdca
updates
kushalkolar Sep 9, 2026
cad9ee4
Fix
kushalkolar Sep 9, 2026
dee42ec
update docs
kushalkolar Sep 9, 2026
05acb46
docs
kushalkolar Sep 9, 2026
87f74b0
add image collection to docs conf.py
kushalkolar Sep 9, 2026
911570f
update docs
kushalkolar Sep 9, 2026
d5f1da9
Merge branch 'ndwidget' into flat-gfeatures2
kushalkolar Sep 9, 2026
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
Prev Previous commit
Next Next commit
cmap_transform tweaks
  • Loading branch information
kushalkolar committed Aug 24, 2026
commit 782017ff25432b610f65fd7745160db07ba3ab73
26 changes: 15 additions & 11 deletions fastplotlib/graphics/_positions_base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Any
from collections.abc import Iterable
from typing import Any, Literal

import numpy as np
import cmap as cmap_lib
Expand Down Expand Up @@ -29,7 +30,10 @@ def __init__(
data: Any,
colors: ColorLike | MultiColorLike = "w",
cmap: ColormapLike | None = None,
cmap_transform: np.ndarray | None = None,
cmap_transform: np.ndarray | Iterable[int | float] | None = None,
cmap_vmin: float | None = None,
cmap_vmax: float | None = None,
cmap_gamma: float = 1.0,
size_space: str = "screen",
*args,
**kwargs,
Expand All @@ -49,9 +53,9 @@ def __init__(

# the cmap normalization params are created once and persist for the lifetime of the
# graphic; vmin, vmax default to None so the transform's own range is used
self._cmap_vmin = CmapTranformNormParam(None, "cmap_vmin")
self._cmap_vmax = CmapTranformNormParam(None, "cmap_vmax")
self._cmap_gamma = CmapTranformNormParam(1.0, "cmap_gamma")
self._cmap_vmin = CmapTranformNormParam(cmap_vmin, "cmap_vmin")
self._cmap_vmax = CmapTranformNormParam(cmap_vmax, "cmap_vmax")
self._cmap_gamma = CmapTranformNormParam(cmap_gamma, "cmap_gamma")

if cmap is not None:
# if a cmap is specified it overrides colors argument
Expand Down Expand Up @@ -208,7 +212,7 @@ def cmap_vmax(self, value: float):
self._cmap_vmax.set_value(self, value)

@property
def cmap_gamma(self) -> float | None:
def cmap_gamma(self) -> float:
"""Get or set the gamma applied when normalizing the cmap_transform"""
if self._cmap_gamma is not None:
return self._cmap_gamma.value
Expand Down Expand Up @@ -255,16 +259,16 @@ def _create_cmap_buffers(self, cmap, cmap_transform) -> tuple[VertexCmap, Vertex

if cmap_transform is None:
# default transform is just a linspace along the datapoints
cmap_transform = np.linspace(0, 1, len(self))
else:
if len(cmap_transform) != len(self):
raise ValueError("`cmap_transform` must be a 1D array of the same size as the number of datapoints")
# this gets interpolated based on the number of datapoints
cmap_transform = np.array([0, 1])

# normalize the transform; the texcoords index into the colormap
cmap_transform = VertexCmapTransform(
normalize_min_max(
cmap_transform, self.cmap_vmin, self.cmap_vmax, self.cmap_gamma
)
),
# use buffer array length since len(self.data) returns half for inflines
n_datapoints=len(self.data.buffer.data)
)

return cmap, cmap_transform
Expand Down
84 changes: 45 additions & 39 deletions fastplotlib/graphics/features/_positions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
import pygfx
import cmap as cmap_lib

from ...utils import (
parse_cmap_values,
)

from ._base import (
GraphicFeature,
BufferManager,
Expand Down Expand Up @@ -38,10 +36,10 @@ class VertexColors(BufferManager):
]

def __init__(
self,
colors: ColorLike | MultiColorLike,
n_colors: int,
property_name: str = "colors",
self,
colors: ColorLike | MultiColorLike,
n_colors: int,
property_name: str = "colors",
):
"""
Manages the vertex color buffer for :class:`PositionsGraphic`
Expand All @@ -61,16 +59,16 @@ def __init__(
super().__init__(data=data, property_name=property_name)

def set_value(
self,
graphic,
value: ColorLike | MultiColorLike,
self,
graphic,
value: ColorLike | MultiColorLike,
):
"""set the entire array, create new buffer if necessary"""
# a sequence of colors whose length differs from the current buffer requires a new buffer
if (
isinstance(value, (np.ndarray, list, tuple))
and not is_single_color(value)
and self.buffer.data.shape[0] != len(value)
isinstance(value, (np.ndarray, list, tuple))
and not is_single_color(value)
and self.buffer.data.shape[0] != len(value)
):
# parse the new colors
new_colors = parse_colors(value, len(value))
Expand Down Expand Up @@ -99,9 +97,9 @@ def set_value(

@block_reentrance
def __setitem__(
self,
key: int | slice | np.ndarray[int | bool] | tuple[slice, ...],
user_value: ColorLike | MultiColorLike,
self,
key: int | slice | np.ndarray[int | bool] | tuple[slice, ...],
user_value: ColorLike | MultiColorLike,
):
user_key = key

Expand Down Expand Up @@ -192,9 +190,9 @@ class UniformColor(GraphicFeature):
]

def __init__(
self,
value: ColorLike,
property_name: str = "colors",
self,
value: ColorLike,
property_name: str = "colors",
):
"""Manages uniform color for line or scatter material"""

Expand All @@ -207,7 +205,7 @@ def value(self) -> pygfx.Color:

@block_reentrance
def set_value(
self, graphic, value: ColorLike
self, graphic, value: ColorLike
):
value = pygfx.Color(value)
graphic.world_object.material.color = value
Expand Down Expand Up @@ -317,16 +315,20 @@ def set_value(self, graphic, value):
self._fpl_buffer = pygfx.Buffer(bdata)
graphic.world_object.geometry.positions = self._fpl_buffer

# reset the cmap transform because the number of datapoints has changed
if graphic.cmap is not None:
graphic.cmap_transform = graphic.cmap_transform

self._emit_event(self._property_name, key=slice(None), value=value)
return

self[:] = value

@block_reentrance
def __setitem__(
self,
key: int | slice | np.ndarray[tuple[int, ...], np.dtype[np.integer | np.bool]] | tuple[slice, ...],
value: np.ndarray | float | list[float],
self,
key: int | slice | np.ndarray[tuple[int, ...], np.dtype[np.integer | np.bool]] | tuple[slice, ...],
value: np.ndarray | float | list[float],
):
# directly use the key to slice the buffer and set the values
self.buffer.data[key] = value
Expand All @@ -351,9 +353,9 @@ class VertexCmap(GraphicFeature):
]

def __init__(
self,
value: cmap_lib.ColormapLike,
property_name: str = "cmap",
self,
value: cmap_lib.ColormapLike,
property_name: str = "cmap",
):
"""
colormap feature, manages a VertexColors instance and provides a way to set colormaps.
Expand Down Expand Up @@ -395,33 +397,37 @@ class VertexCmapTransform(GraphicFeature):
},
]

def __init__(self, value: np.ndarray, property_name: str = "cmap_transform"):
def __init__(self, value: np.ndarray, n_datapoints: int, property_name: str = "cmap_transform"):
"""colormap transform"""

self._value = np.asarray(value).astype(np.float32)
value = np.asarray(value)
self._value = self._interpolate(value, n_datapoints)
super().__init__(property_name=property_name)

@property
def value(self) -> np.ndarray:
return self._value

def _interpolate(self, value, n_datapoints):
Comment thread
kushalkolar marked this conversation as resolved.
return np.interp(
np.linspace(0, len(value) - 1, n_datapoints), np.arange(len(value)), value).astype(
np.float32
)

@block_reentrance
def set_value(self, graphic, value: np.ndarray):
value = np.asarray(value).squeeze().astype(np.float32)
value = np.asarray(value).squeeze()

# 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."
)
# interpolate to n_datapoints
value = self._interpolate(value, n_datapoints)

if graphic.world_object.geometry.texcoords is not None:
if graphic.world_object.geometry.texcoords is not None and graphic.world_object.geometry.texcoords.data.size == value.size:
graphic.world_object.geometry.texcoords.data[:] = value
graphic.world_object.geometry.texcoords.update_full()
else:
graphic.world_object.geometry.texcoords = pygfx.Buffer(self.value)
graphic.world_object.geometry.texcoords = pygfx.Buffer(value)

self._value = graphic.world_object.geometry.texcoords.data

Expand All @@ -446,17 +452,17 @@ class CmapTranformNormParam(GraphicFeature):
]

def __init__(
self, value: float, property_name: Literal["cmap_vmin", "cmap_vmax", "cmap_gamma"]
self, value: float | None, property_name: Literal["cmap_vmin", "cmap_vmax", "cmap_gamma"]
):
self._value = value
super().__init__(property_name=property_name)

@property
def value(self) -> float:
def value(self) -> float | None:
return self._value

@block_reentrance
def set_value(self, graphic, value: float):
def set_value(self, graphic, value: float | None):
self._value = value

# this instance manages one of vmin/vmax/gamma, identified by its property_name;
Expand Down
4 changes: 2 additions & 2 deletions fastplotlib/graphics/features/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,11 @@ def get_element_format_from_numpy_array(array):
return array.dtype.str.lstrip("<>=|")


def normalize_min_max(a, vmin: float = None, vmax: float = None, gamma: float = 1.0):
def normalize_min_max(a, vmin: float | None = None, vmax: float | None = None, gamma: float = 1.0):
"""
normalize an array between 0 - 1, clipped to (vmin, vmax)
"""

a = np.asarray(a)
vmin = np.min(a) if vmin is None else vmin
vmax = np.max(a) if vmax is None else vmax

Expand Down
3 changes: 1 addition & 2 deletions fastplotlib/graphics/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from warnings import warn

import numpy as np
import cmap as cmap_lib

import pygfx

Expand Down Expand Up @@ -42,7 +41,7 @@ def __init__(
thickness: float = 2.0,
colors: ColorLike | MultiColorLike = "w",
cmap: ColormapLike | None = None,
cmap_transform: np.ndarray | None = None,
cmap_transform: np.ndarray | Iterable[int | float] | None = None,
size_space: str = "screen",
dash_pattern: str | tuple | list = (),
thin: bool = False,
Expand Down
2 changes: 1 addition & 1 deletion fastplotlib/layouts/_graphic_methods_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ def add_line(
thickness: float = 2.0,
colors: ColorLike | MultiColorLike = "w",
cmap: ColormapLike | None = None,
cmap_transform: np.ndarray | None = None,
cmap_transform: np.ndarray | Iterable[int | float] | None = None,
size_space: str = "screen",
dash_pattern: str | tuple | list = (),
thin: bool = False,
Expand Down