Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 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
0a4ee77
Merge branch 'main' into iw-array
kushalkolar Nov 16, 2025
219ea3c
bugfix
kushalkolar Nov 17, 2025
dd792f9
Merge branch 'iw-array' of https://github.com/fastplotlib/fastplotlib…
kushalkolar Dec 3, 2025
706c835
Merge branch 'main' into iw-array
kushalkolar Dec 4, 2025
08ee98f
hist specify quantile
kushalkolar Dec 8, 2025
31e3260
Merge branch 'iw-array' of https://github.com/fastplotlib/fastplotlib…
kushalkolar Dec 12, 2025
0d754f5
fix typos (#991)
kushalkolar Feb 6, 2026
68c7c23
merge main into iw-array
kushalkolar Feb 14, 2026
3a52d01
nd-iw backup
kushalkolar Feb 14, 2026
4ec5f6f
Merge branch 'ndwidget' into iw-array
kushalkolar Feb 14, 2026
83e6f12
correct ImageGraphic w.r.t. ndw
kushalkolar Feb 15, 2026
4cc82ea
last fixes in ndi
kushalkolar Feb 16, 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
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
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,6 @@
"numpy": ("https://numpy.org/doc/stable", None),
"pygfx": ("https://docs.pygfx.org/stable", None),
"wgpu": ("https://wgpu-py.readthedocs.io/en/latest", None),
"rendercanvas": ("https://rendercanvas.readthedocs.io/stable/", None),
# "fastplotlib": ("https://www.fastplotlib.org/", None),
}
11 changes: 5 additions & 6 deletions docs/source/developer_notes/graphics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,14 @@ For example let's look at ``LineGraphic`` in ``fastplotlib/graphics/line.py``. E
``"data", "colors", "cmap", "thickness"`` in addition to properties common to all graphics, such as ``"name", "offset", "rotation", and "visible"``

Now look at the constructor for the ``LineGraphic`` base class ``PositionsGraphic``, it first creates an instance of ``VertexPositions``.
This is a class that manages vertex positions buffer. For the user, it defines the line data, and provides additional useful functionality.
It defines the line, and provides additional useful functionality.
This is a class that manages vertex positions buffer. For the user, it defines the line vertex positions, and provides additional useful functionality.
For example, every time that the ``data`` is changed, the new data will be marked for upload to the GPU before the next draw.
In addition, event handlers will be called if any event handlers are registered.

``VertexColors`` behaves similarly, but it can perform additional parsing that can create the colors buffer from different
``VertexColors`` behaves similarly, but it can perform additional parsing that can create or set the colors buffer from different
forms of user input. For example if a user runs: ``line_graphic.colors = "blue"``, then ``VertexColors.__setitem__()`` will
create a buffer that corresponds to what ``pygfx.Color`` thinks is "blue". Users can also take advantage of fancy indexing,
ex: ``line_graphics.colors[bool_array] = "red"`` 😊
set the buffer that corresponds to what ``pygfx.Color`` thinks is "blue", i.e the RGBA array `[0, 0, 1, 1]. Users can also take advantage of fancy indexing,
ex: ``line_graphics.colors[bool_array] = "red"`` 😊 to set the color of specific vertices.

``LineGraphic`` also has a ``VertexCmap``, this manages the line ``VertexColors`` instance to parse colormaps, for example:
``line_graphic.cmap = "jet"`` or even ``line_graphic.cmap[50:] = "viridis"``.
Expand All @@ -73,4 +72,4 @@ ex: ``line_graphics.colors[bool_array] = "red"`` 😊
callbacks to indicate that the graphic has been deleted (for example, removing references to a graphic from a legend).

Other graphics have properties that are relevant to them, for example ``ImageGraphic`` has ``cmap``, ``vmin``, ``vmax``,
properties unique to images.
properties unique to images.
9 changes: 9 additions & 0 deletions fastplotlib/graphics/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,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 @@ -274,6 +275,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 @@ -440,6 +446,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 @@ -318,7 +318,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 @@ -334,7 +334,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
15 changes: 10 additions & 5 deletions fastplotlib/graphics/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,11 @@ def __init__(
self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation)

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

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

Expand All @@ -174,6 +175,12 @@ def __init__(
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 @@ -275,8 +282,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 @@ -312,7 +317,7 @@ def interpolation(self, value: str):

@property
def cmap_interpolation(self) -> str:
"""cmap interpolation method"""
"""cmap interpolation method, 'linear' or 'nearest'. Used only for grayscale images"""
return self._cmap_interpolation.value

@cmap_interpolation.setter
Expand Down
14 changes: 10 additions & 4 deletions fastplotlib/graphics/image_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,18 +204,24 @@ def __init__(
self._vmax = ImageVmax(vmax)

self._interpolation = ImageInterpolation(interpolation)
self._cmap_interpolation = ImageCmapInterpolation(cmap_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 not in (3, 4):
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)
self._step_size = VolumeIsoStepSize(step_size)
Expand Down Expand Up @@ -301,7 +307,7 @@ def mode(self, mode: str):

@property
def cmap(self) -> str:
"""Get or set colormap name"""
"""Get or set colormap name, only used for grayscale images"""
return self._cmap.value

@cmap.setter
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
34 changes: 18 additions & 16 deletions fastplotlib/layouts/_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,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 @@ -606,14 +606,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 @@ -622,7 +622,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 @@ -646,15 +646,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 @@ -770,7 +770,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 @@ -929,18 +929,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 @@ -955,6 +957,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 @@ -233,7 +233,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