Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
dd502e4
start separating iw plotting and array logic
kushalkolar Jun 29, 2025
4f1fcd9
some more basics down
kushalkolar Aug 11, 2025
20f1878
comment
kushalkolar Aug 11, 2025
330f7f0
collapse into just having a window function, no frame_function
kushalkolar Aug 17, 2025
00aac51
Merge branch 'main' into iw-array
kushalkolar Nov 4, 2025
7aa90b9
progress
kushalkolar Nov 5, 2025
62a8b53
placeholder for computing histogram
kushalkolar Nov 5, 2025
8f48b01
formatting
kushalkolar Nov 5, 2025
7770ee0
remove spaghetti
kushalkolar Nov 5, 2025
b31f549
more progress
kushalkolar Nov 6, 2025
62599a5
basics working :D
kushalkolar Nov 6, 2025
a5877bc
black
kushalkolar Nov 6, 2025
43f0e58
most of the basics work in iw
kushalkolar Nov 6, 2025
43f5423
fix
kushalkolar Nov 6, 2025
9dc1998
progress
kushalkolar Nov 7, 2025
bcdd9b7
progress but still broken
kushalkolar Nov 7, 2025
cb4b6f5
flippin display dims works
kushalkolar Nov 7, 2025
3048682
camera scale must be positive for MIP rendering
kushalkolar Nov 8, 2025
dd9bc84
a very difficult to encounter iterator bug!
kushalkolar Nov 8, 2025
52f0972
patch iterator caveats
kushalkolar Nov 8, 2025
4df72b9
mostly worksgit status
kushalkolar Nov 8, 2025
66ab130
add ArrayProtocol
kushalkolar Nov 8, 2025
ec8b0cc
rename
kushalkolar Nov 8, 2025
d00ebc0
fixes
kushalkolar Nov 8, 2025
85cf6e6
set camera orthogonal to xy plane when going from 3d -> 2d
kushalkolar Nov 8, 2025
6cb6643
naming, cleaning
kushalkolar Nov 8, 2025
5be03b6
cleanup, correct way to push and pop dims
kushalkolar Nov 8, 2025
51ed6b2
quality of life improvements
kushalkolar Nov 8, 2025
6db1714
new histogram lut tool
kushalkolar Nov 9, 2025
50d8e87
new hlut tool
kushalkolar Nov 9, 2025
d9f06e6
imagewidget rgb toggle works
kushalkolar Nov 9, 2025
97f7064
more progress
kushalkolar Nov 10, 2025
bf2226d
support rgb(a) image volumes
kushalkolar Nov 10, 2025
db11abf
ImageGraphic cleanup
kushalkolar Nov 10, 2025
a156410
cleanup, docs
kushalkolar Nov 10, 2025
bebec04
fix
kushalkolar Nov 10, 2025
cc9b027
updates
kushalkolar Nov 10, 2025
52fc715
new per-data array properties work
kushalkolar Nov 10, 2025
48c8c1a
black formatting
kushalkolar Nov 10, 2025
c0b870d
fixes and other things
kushalkolar Nov 10, 2025
6d0d5dd
typing tweaks
kushalkolar Nov 10, 2025
a46e3f5
better iterator, fix bugs
kushalkolar Nov 10, 2025
d86093b
fixes
kushalkolar Nov 10, 2025
db0fcf9
show tooltips in right clck menu
kushalkolar Nov 10, 2025
263def9
ignore nans and inf for histogram
kushalkolar Nov 11, 2025
cb26b3a
histogram of zeros
kushalkolar Nov 11, 2025
a1affbf
docstrings
kushalkolar Nov 11, 2025
81cd5be
fix imgui pixels
kushalkolar Nov 11, 2025
9cf6b6e
iw indices event handlers only get a tuple of the indices
kushalkolar Nov 11, 2025
89f5275
bugfix
kushalkolar Nov 11, 2025
e1cc9b0
fix cmap setter
kushalkolar Nov 12, 2025
dc1dbd2
spatial_func better name
kushalkolar Nov 14, 2025
219ea3c
bugfix
kushalkolar Nov 17, 2025
706c835
Merge branch 'main' into iw-array
kushalkolar Dec 4, 2025
08ee98f
hist specify quantile
kushalkolar Dec 8, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/docs-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
- name: build docs
run: |
cd docs
RTD_BUILD=1 make html SPHINXOPTS="-W --keep-going"
DOCS_BUILD=1 make html SPHINXOPTS="-W --keep-going"

# set environment variable `DOCS_VERSION_DIR` to either the pr-branch name, "dev", or the release version tag
- name: set output pr
Expand Down
9 changes: 9 additions & 0 deletions fastplotlib/graphics/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ def __init__(
self._alpha_mode = AlphaMode(alpha_mode)
self._visible = Visible(visible)
self._block_events = False
self._block_handlers = list()

self._axes: Axes = None

Expand Down Expand Up @@ -242,6 +243,11 @@ def block_events(self) -> bool:
def block_events(self, value: bool):
self._block_events = value

@property
def block_handlers(self) -> list:
"""Used to block event handlers for a graphic and prevent recursion."""
return self._block_handlers

@property
def world_object(self) -> pygfx.WorldObject:
"""Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly."""
Expand Down Expand Up @@ -370,6 +376,9 @@ def _handle_event(self, callback, event: pygfx.Event):
if self.block_events:
return

if callback in self._block_handlers:
return

if event.type in self._features:
# for feature events
event._target = self.world_object
Expand Down
4 changes: 2 additions & 2 deletions fastplotlib/graphics/features/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ def __repr__(self):
def block_reentrance(set_value):
# decorator to block re-entrant set_value methods
# useful when creating complex, circular, bidirectional event graphs
def set_value_wrapper(self: GraphicFeature, graphic_or_key, value):
def set_value_wrapper(self: GraphicFeature, graphic_or_key, value, **kwargs):
"""
wraps GraphicFeature.set_value

Expand All @@ -336,7 +336,7 @@ def set_value_wrapper(self: GraphicFeature, graphic_or_key, value):
try:
# block re-execution of set_value until it has *fully* finished executing
self._reentrant_block = True
set_value(self, graphic_or_key, value)
set_value(self, graphic_or_key, value, **kwargs)
except Exception as exc:
# raise original exception
raise exc # set_value has raised. The line above and the lines 2+ steps below are probably more relevant!
Expand Down
6 changes: 4 additions & 2 deletions fastplotlib/graphics/features/_selection_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def axis(self) -> str:
return self._axis

@block_reentrance
def set_value(self, selector, value: Sequence[float]):
def set_value(self, selector, value: Sequence[float], *, change: str = "full"):
"""
Set start, stop range of selector

Expand Down Expand Up @@ -182,7 +182,9 @@ def set_value(self, selector, value: Sequence[float]):
if len(self._event_handlers) < 1:
return

event = GraphicFeatureEvent(self._property_name, {"value": self.value})
event = GraphicFeatureEvent(
self._property_name, {"value": self.value, "change": change}
)

event.get_selected_indices = selector.get_selected_indices
event.get_selected_data = selector.get_selected_data
Expand Down
22 changes: 14 additions & 8 deletions fastplotlib/graphics/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,19 +158,26 @@ def __init__(
self._interpolation = ImageInterpolation(interpolation)

# set map to None for RGB images
if self._data.value.ndim > 2:
if self._data.value.ndim == 3:
self._cmap = None
self._cmap_interpolation = None
_map = None
else:

elif self._data.value.ndim == 2:
# use TextureMap for grayscale images
self._cmap = ImageCmap(cmap)
self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation)

_map = pygfx.TextureMap(
self._cmap.texture,
filter=self._cmap_interpolation.value,
wrap="clamp-to-edge",
)
else:
raise ValueError(
f"ImageGraphic `data` must have 2 dimensions for grayscale images, or 3 dimensions for RGB(A) images.\n"
f"You have passed a a data array with: {self._data.value.ndim} dimensions, "
f"and of shape: {self._data.value.shape}"
)

# one common material is used for every Texture chunk
self._material = pygfx.ImageBasicMaterial(
Expand Down Expand Up @@ -223,8 +230,6 @@ def cmap(self) -> str | None:
if self._cmap is not None:
return self._cmap.value

return None

@cmap.setter
def cmap(self, name: str):
if self.data.value.ndim > 2:
Expand Down Expand Up @@ -259,9 +264,10 @@ def interpolation(self, value: str):
self._interpolation.set_value(self, value)

@property
def cmap_interpolation(self) -> str:
"""cmap interpolation method"""
return self._cmap_interpolation.value
def cmap_interpolation(self) -> str | None:
"""cmap interpolation method, 'linear' or 'nearest'. `None` if image is RGB(A)"""
if self._cmap_interpolation is not None:
return self._cmap_interpolation.value

@cmap_interpolation.setter
def cmap_interpolation(self, value: str):
Expand Down
42 changes: 28 additions & 14 deletions fastplotlib/graphics/image_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,16 +211,28 @@ def __init__(

self._interpolation = ImageInterpolation(interpolation)

# TODO: I'm assuming RGB volume images aren't supported???
# use TextureMap for grayscale images
self._cmap = ImageCmap(cmap)
self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation)

self._texture_map = pygfx.TextureMap(
self._cmap.texture,
filter=self._cmap_interpolation.value,
wrap="clamp-to-edge",
)
if self._data.value.ndim == 4:
# set map to None for RGB image volumes
self._cmap = None
self._texture_map = None
self._cmap_interpolation = None

elif self._data.value.ndim == 3:
# use TextureMap for grayscale images
self._cmap = ImageCmap(cmap)
self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation)
self._texture_map = pygfx.TextureMap(
self._cmap.texture,
filter=self._cmap_interpolation.value,
wrap="clamp-to-edge",
)
else:
raise ValueError(
f"ImageVolumeGraphic `data` must have 3 dimensions for grayscale images, "
f"or 4 dimensions for RGB(A) images.\n"
f"You have passed a a data array with: {self._data.value.ndim} dimensions, "
f"and of shape: {self._data.value.shape}"
)

self._plane = VolumeSlicePlane(plane)
self._threshold = VolumeIsoThreshold(threshold)
Expand Down Expand Up @@ -282,9 +294,10 @@ def mode(self, mode: str):
self._mode.set_value(self, mode)

@property
def cmap(self) -> str:
def cmap(self) -> str | None:
"""Get or set colormap name"""
return self._cmap.value
if self._cmap is not None:
return self._cmap.value

@cmap.setter
def cmap(self, name: str):
Expand Down Expand Up @@ -318,9 +331,10 @@ def interpolation(self, value: str):
self._interpolation.set_value(self, value)

@property
def cmap_interpolation(self) -> str:
def cmap_interpolation(self) -> str | None:
"""Get or set the cmap interpolation method"""
return self._cmap_interpolation.value
if self._cmap_interpolation is not None:
return self._cmap_interpolation.value

@cmap_interpolation.setter
def cmap_interpolation(self, value: str):
Expand Down
4 changes: 2 additions & 2 deletions fastplotlib/graphics/selectors/_linear_region.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,9 +472,9 @@ def _move_graphic(self, move_info: MoveInfo):
if move_info.source == self._edges[0]:
# change only left or bottom bound
new_min = min(cur_min + delta, cur_max)
self._selection.set_value(self, (new_min, cur_max))
self._selection.set_value(self, (new_min, cur_max), change="min")

elif move_info.source == self._edges[1]:
# change only right or top bound
new_max = max(cur_max + delta, cur_min)
self._selection.set_value(self, (cur_min, new_max))
self._selection.set_value(self, (cur_min, new_max), change="max")
15 changes: 12 additions & 3 deletions fastplotlib/graphics/utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from contextlib import contextmanager
from typing import Callable, Iterable

from ._base import Graphic


@contextmanager
def pause_events(*graphics: Graphic):
def pause_events(*graphics: Graphic, event_handlers: Iterable[Callable] = None):
"""
Context manager for pausing Graphic events.

Optionally pass in only specific event handlers which are blocked. Other events for the graphic will not be blocked.

Examples
--------

Expand All @@ -30,8 +33,14 @@ def pause_events(*graphics: Graphic):
original_vals = [g.block_events for g in graphics]

for g in graphics:
g.block_events = True
if event_handlers is not None:
g.block_handlers.extend([e for e in event_handlers])
else:
g.block_events = True
yield

for g, value in zip(graphics, original_vals):
g.block_events = value
if event_handlers is not None:
g.block_handlers.clear()
else:
g.block_events = value
36 changes: 19 additions & 17 deletions fastplotlib/layouts/_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ def show_tooltips(self, val: bool):

if val:
# register all graphics
for subplot in self:
for subplot in self._subplots.ravel():
for graphic in subplot.graphics:
self._tooltip_manager.register(graphic)

Expand All @@ -572,7 +572,7 @@ def _render(self, draw=True):

# call the animation functions before render
self._call_animate_functions(self._animate_funcs_pre)
for subplot in self:
for subplot in self._subplots.ravel():
subplot._render()

# overlay render pass
Expand Down Expand Up @@ -639,14 +639,14 @@ def show(
sidecar_kwargs = dict()

# flip y-axis if ImageGraphics are present
for subplot in self:
for subplot in self._subplots.ravel():
for g in subplot.graphics:
if isinstance(g, ImageGraphic):
subplot.camera.local.scale_y *= -1
break

if autoscale:
for subplot in self:
for subplot in self._subplots.ravel():
if maintain_aspect is None:
_maintain_aspect = subplot.camera.maintain_aspect
else:
Expand All @@ -655,7 +655,7 @@ def show(

# set axes visibility if False
if not axes_visible:
for subplot in self:
for subplot in self._subplots.ravel():
subplot.axes.visible = False

# parse based on canvas type
Expand All @@ -679,15 +679,15 @@ def show(
elif self.canvas.__class__.__name__ == "OffscreenRenderCanvas":
# for test and docs gallery screenshots
self._fpl_reset_layout()
for subplot in self:
for subplot in self._subplots.ravel():
subplot.axes.update_using_camera()

# render call is blocking only on github actions for some reason,
# but not for rtd build, this is a workaround
# for CI tests, the render call works if it's in test_examples
# but it is necessary for the gallery images too so that's why this check is here
if "RTD_BUILD" in os.environ.keys():
if os.environ["RTD_BUILD"] == "1":
if "DOCS_BUILD" in os.environ.keys():
if os.environ["DOCS_BUILD"] == "1":
self._render()

else: # assume GLFW
Expand Down Expand Up @@ -803,7 +803,7 @@ def clear_animations(self, removal: str = None):

def clear(self):
"""Clear all Subplots"""
for subplot in self:
for subplot in self._subplots.ravel():
subplot.clear()

def export_numpy(self, rgb: bool = False) -> np.ndarray:
Expand Down Expand Up @@ -962,18 +962,20 @@ def __getitem__(self, index: str | int | tuple[int, int]) -> Subplot:
return subplot
raise IndexError(f"no subplot with given name: {index}")

if isinstance(index, (int, np.integer)):
return self._subplots.ravel()[index]

if isinstance(self.layout, GridLayout):
return self._subplots[index[0], index[1]]

return self._subplots[index]
raise TypeError(
f"Can index figure using <str> subplot name, numerical <int> subplot index, or a "
f"tuple[int, int] if the layout is a grid"
)

def __iter__(self):
self._current_iter = iter(range(len(self)))
return self

def __next__(self) -> Subplot:
pos = self._current_iter.__next__()
return self._subplots.ravel()[pos]
for subplot in self._subplots.ravel():
yield subplot

def __len__(self):
"""number of subplots"""
Expand All @@ -988,6 +990,6 @@ def __repr__(self):
return (
f"fastplotlib.{self.__class__.__name__}"
f" Subplots:\n"
f"\t{newline.join(subplot.__str__() for subplot in self)}"
f"\t{newline.join(subplot.__str__() for subplot in self._subplots.ravel())}"
f"\n"
)
5 changes: 4 additions & 1 deletion fastplotlib/layouts/_plot_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,10 @@ def controller(self, new_controller: str | pygfx.Controller):
# pygfx plans on refactoring viewports anyways
if self.parent is not None:
if self.parent.__class__.__name__.endswith("Figure"):
for subplot in self.parent:
# always use figure._subplots.ravel() in internal fastplotlib code
# otherwise if we use `for subplot in figure`, this could conflict
# with a user's iterator where they are doing `for subplot in figure` !!!
for subplot in self.parent._subplots.ravel():
if subplot.camera in cameras_list:
new_controller.register_events(subplot.viewport)
subplot._controller = new_controller
Expand Down
Loading