Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
97442f5
new cursor tool, basics work
kushalkolar Nov 19, 2025
034f719
lint and update api docs
clewis7 Nov 19, 2025
9a18381
add custom tooltip to figure
kushalkolar Nov 20, 2025
b08e744
Merge branch 'cursor-simpler' of https://github.com/fastplotlib/fastp…
kushalkolar Nov 20, 2025
cf0c6dd
example WIP
kushalkolar Nov 20, 2025
86807c2
manual picking, world obj -> graphic mapping
kushalkolar Nov 20, 2025
506fa2c
fix
kushalkolar Nov 20, 2025
f69ccc9
fix gc
kushalkolar Nov 20, 2025
c0c1203
stuff
kushalkolar Nov 25, 2025
cbc5127
Merge branch 'main' into cursor-simpler
kushalkolar Dec 6, 2025
5630f23
Merge branch 'cursor-simpler' of https://github.com/fastplotlib/fastp…
kushalkolar Dec 6, 2025
c5f8eac
cursor and tooltips refactor, basically works
kushalkolar Dec 6, 2025
9323e3a
much simpler tooltip and cursor
kushalkolar Dec 7, 2025
a59f686
move TextBox and Tooltip, update API docs
kushalkolar Dec 7, 2025
c60f1fe
cursor manages tooltips
kushalkolar Dec 7, 2025
32493b1
update examples
kushalkolar Dec 7, 2025
ed297f5
black
kushalkolar Dec 7, 2025
40f3357
cleanup
kushalkolar Dec 7, 2025
a3252eb
docstrings
kushalkolar Dec 7, 2025
c6133cd
update example
kushalkolar Dec 7, 2025
75d13fe
update example
kushalkolar Dec 7, 2025
6d535e7
add image space transforms examples
kushalkolar Dec 17, 2025
00b51a9
finish space transforms examples
kushalkolar Dec 17, 2025
1cafa74
typing
kushalkolar Dec 17, 2025
f585007
black
kushalkolar Dec 17, 2025
a64ce67
docstrings
kushalkolar Dec 17, 2025
a0f74c2
update examples
kushalkolar Dec 17, 2025
6a648e7
textbox constructor takes args, docstrings
kushalkolar Dec 17, 2025
dd65092
True -> true
kushalkolar Dec 17, 2025
cd0f216
update names
kushalkolar Dec 17, 2025
70c8088
update cursor examples
kushalkolar Dec 17, 2025
d4c3eb7
new ground truth screenshots for transforms
kushalkolar Dec 17, 2025
edca694
black
kushalkolar Dec 17, 2025
3570fba
update
kushalkolar Dec 17, 2025
a7bf44c
revert persist kwarg in PlotArea animations stuff
kushalkolar Dec 17, 2025
fba5247
fix
kushalkolar Dec 22, 2025
201b75c
message for image volumes
kushalkolar Dec 22, 2025
32c7d5f
Update fastplotlib/graphics/_base.py
clewis7 Jan 14, 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
42 changes: 42 additions & 0 deletions docs/source/api/tools/Cursor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.. _api.Cursor:

Cursor
******

======
Cursor
======
.. currentmodule:: fastplotlib

Constructor
~~~~~~~~~~~
.. autosummary::
:toctree: Cursor_api

Cursor

Properties
~~~~~~~~~~
.. autosummary::
:toctree: Cursor_api

Cursor.alpha
Cursor.color
Cursor.edge_color
Cursor.edge_width
Cursor.marker
Cursor.mode
Cursor.pause
Cursor.position
Cursor.size
Cursor.size_space

Methods
~~~~~~~
.. autosummary::
:toctree: Cursor_api

Cursor.add_subplot
Cursor.clear
Cursor.remove_subplot

40 changes: 40 additions & 0 deletions docs/source/api/tools/GraphicTooltip.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.. _api.GraphicTooltip:

GraphicTooltip
**************

==============
GraphicTooltip
==============
.. currentmodule:: fastplotlib

Constructor
~~~~~~~~~~~
.. autosummary::
:toctree: GraphicTooltip_api

GraphicTooltip

Properties
~~~~~~~~~~
.. autosummary::
:toctree: GraphicTooltip_api

GraphicTooltip.background_color
GraphicTooltip.font_size
GraphicTooltip.outline_color
GraphicTooltip.padding
GraphicTooltip.text_color
GraphicTooltip.world_object

Methods
~~~~~~~
.. autosummary::
:toctree: GraphicTooltip_api

GraphicTooltip.clear
GraphicTooltip.display
GraphicTooltip.register
GraphicTooltip.unregister
GraphicTooltip.unregister_all

5 changes: 2 additions & 3 deletions docs/source/api/tools/Tooltip.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ Methods
.. autosummary::
:toctree: Tooltip_api

Tooltip.register
Tooltip.unregister
Tooltip.unregister_all
Tooltip.clear
Tooltip.display

2 changes: 2 additions & 0 deletions docs/source/api/tools/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ Tools

HistogramLUTTool
Tooltip
GraphicTooltip
Cursor
61 changes: 61 additions & 0 deletions examples/misc/cursor_custom_tooltip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
Cursor tool with tooltips
=========================

Cursor tool example that also displays tooltips
"""

import numpy as np
import fastplotlib as fpl
import imageio.v3 as iio
from pylinalg import vec_transform, mat_combine

img1 = iio.imread("imageio:camera.png")
img2 = iio.imread("imageio:astronaut.png")

scatter_data = np.random.normal(loc=256, scale=(50), size=(500)).reshape(250, 2)
line_data = np.random.rand(100, 2) * 512

figure = fpl.Figure(shape=(2, 2), size=(500, 500))

img = figure[0, 0].add_image(img1, cmap="viridis")
figure[0, 1].add_image(img2, metadata="image metadata")
figure[1, 0].add_scatter(scatter_data, sizes=5, metadata="scatter metadata")
figure[1, 1].add_line(line_data, metadata="line metadata")

cursor = fpl.Cursor(mode="crosshair", color="w")

for subplot in figure:
cursor.add_subplot(subplot)

figure.show_tooltips = True

tooltips2 = fpl.Tooltip()
tooltips2.world_object.visible = True
figure.add_tooltip(tooltips2)

@figure.renderer.add_event_handler("pointer_move")
def update(ev):
pos = figure[0, 0].map_screen_to_world(ev)
if pos is None:
return

x, y = figure[0, 1].map_world_to_screen(pos)
pick = subplot.get_pick_info((x, y))

if pick is None:
tooltips2.visible = False
return

info = pick["graphic"].metadata
tooltips2.display((x, y), str(info))

print((img.world_object.children[0].uniform_buffer.data["global_id"]).item())
figure.show()


# NOTE: fpl.loop.run() should not be used for interactive sessions
# See the "JupyterLab and IPython" section in the user guide
if __name__ == "__main__":
print(__doc__)
fpl.loop.run()
48 changes: 48 additions & 0 deletions examples/misc/cursors.py
Comment thread
kushalkolar marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
Cursor tool
===========

Example with multiple subplots and an interactive cursor that marks the same position in each subplot.
"""

# test_example = False
# sphinx_gallery_pygfx_docs = 'screenshot'

import numpy as np
import fastplotlib as fpl
import imageio.v3 as iio
from pylinalg import vec_transform, mat_combine


# get some data
img1 = iio.imread("imageio:camera.png")
img2 = iio.imread("imageio:wikkie.png")
scatter_data = np.random.normal(loc=256, scale=(50), size=(500)).reshape(250, 2)
line_data = np.random.rand(100, 2) * 512

# create a figure
figure = fpl.Figure(shape=(2, 2), size=(700, 750))

# plot data
figure[0, 0].add_image(img1, cmap="viridis")
figure[0, 1].add_image(img2)
figure[1, 0].add_scatter(scatter_data, sizes=5, colors="r")
figure[1, 1].add_line(line_data, colors="r")

# creator a cursor in crosshair mode
cursor = fpl.Cursor(mode="crosshair", color="w")

# add all subplots to the cursor
for subplot in figure:
cursor.add_subplot(subplot)

# you can also set the cursor position programmatically
cursor.position = (256, 256)

figure.show()

# NOTE: fpl.loop.run() should not be used for interactive sessions
# See the "JupyterLab and IPython" section in the user guide
if __name__ == "__main__":
print(__doc__)
fpl.loop.run()
17 changes: 17 additions & 0 deletions fastplotlib/graphics/_base.py
Comment thread
kushalkolar marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@
from ._axes import Axes

HexStr: TypeAlias = str
WorldObjectID: TypeAlias = int

# dict that holds all world objects for a given python kernel/session
# Graphic objects only use proxies to WorldObjects
WORLD_OBJECTS: dict[HexStr, pygfx.WorldObject] = dict() #: {hex id str: WorldObject}

# maps world object to the graphic which owns it, useful when manually picking from the renderer and we
# need to know the graphic associated with the target world object
WORLD_OBJECT_TO_GRAPHIC: dict[WorldObjectID, "Graphic"] = dict()


PYGFX_EVENTS = [
"key_down",
Expand Down Expand Up @@ -251,6 +256,18 @@ def world_object(self) -> pygfx.WorldObject:
def _set_world_object(self, wo: pygfx.WorldObject):
WORLD_OBJECTS[self._fpl_address] = wo

# add to world object -> graphic mapping
if isinstance(wo, pygfx.Group):
for child in wo.children:
if isinstance(child, (pygfx.Image, pygfx.Volume, pygfx.Points, pygfx.Line)):
# need to call int() on it since it's a numpy array with 1 element
# and numpy arrays aren't hashable
Comment thread
clewis7 marked this conversation as resolved.
Outdated
global_id = int(child.uniform_buffer.data["global_id"])
WORLD_OBJECT_TO_GRAPHIC[global_id] = self
else:
global_id = int(wo.uniform_buffer.data["global_id"])
WORLD_OBJECT_TO_GRAPHIC[global_id] = self

wo.visible = self.visible
if "Image" in self.__class__.__name__:
# Image and ImageVolume use tiling and share one material
Expand Down
15 changes: 12 additions & 3 deletions fastplotlib/layouts/_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from ._subplot import Subplot
from ._engine import GridLayout, WindowLayout, ScreenSpaceCamera
from .. import ImageGraphic
from ..tools import Tooltip
from ..tools import Tooltip, GraphicTooltip


class Figure:
Expand Down Expand Up @@ -461,7 +461,7 @@ def __init__(
self._overlay_scene = pygfx.Scene()

# tooltip in overlay render pass
self._tooltip_manager = Tooltip()
self._tooltip_manager = GraphicTooltip()
self._overlay_scene.add(self._tooltip_manager.world_object)

self._show_tooltips = show_tooltips
Expand Down Expand Up @@ -534,7 +534,7 @@ def names(self) -> np.ndarray[str]:
return names

@property
def tooltip_manager(self) -> Tooltip:
def tooltip_manager(self) -> GraphicTooltip:
"""manage tooltips"""
return self._tooltip_manager

Expand All @@ -561,6 +561,15 @@ def show_tooltips(self, val: bool):
elif not val:
self._tooltip_manager.unregister_all()

def add_tooltip(self, tooltip: Tooltip):
if not isinstance(tooltip, Tooltip):
raise TypeError(f"tooltip must be a `Tooltip` instance, you passed: {tooltip}")

self._overlay_scene.add(tooltip.world_object)

def remove_tooltip(self, tooltip):
self._overlay_scene.remove(tooltip)

def _render(self, draw=True):
# draw the underlay planes
self.renderer.render(self._underlay_scene, self._underlay_camera, flush=False)
Expand Down
67 changes: 65 additions & 2 deletions fastplotlib/layouts/_plot_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from rendercanvas import BaseRenderCanvas

from ._utils import create_controller
from ..graphics._base import Graphic
from ..graphics._base import Graphic, WORLD_OBJECT_TO_GRAPHIC
from ..graphics import ImageGraphic
from ..graphics.selectors._base_selector import BaseSelector
from ._graphic_methods_mixin import GraphicMethodsMixin
Expand Down Expand Up @@ -283,13 +283,18 @@ def map_screen_to_world(
self, pos: tuple[float, float] | pygfx.PointerEvent, allow_outside: bool = False
) -> np.ndarray | None:
"""
Map screen position to world position
Map screen (canvas) position to world position

Parameters
----------
pos: (float, float) | pygfx.PointerEvent
``(x, y)`` screen coordinates, or ``pygfx.PointerEvent``

Returns
-------
(float, float, float)
(x, y, z) position in world space, z is always 0

"""
if isinstance(pos, pygfx.PointerEvent):
pos = pos.x, pos.y
Expand All @@ -315,6 +320,64 @@ def map_screen_to_world(
# default z is zero for now
return np.array([*pos_world[:2], 0])

def map_world_to_screen(self, pos: tuple[float, float, float]):
"""
Map world position to screen (canvas) posiition

Parameters
----------
pos: (x, y, z)
world space position

Returns
-------
(float, float)
(x, y) position in screen (canvas) space

"""

if not len(pos) == 3:
raise ValueError(f"must pass 3d (x, y, z) position, you passed: {pos}")

# apply camera transform and get NDC position
ndc = vec_transform(np.asarray(pos), self.camera.camera_matrix)

# get viewport rect
x_offset, y_offset, w, h = self.viewport.rect

# ndc to screen position
x_screen = x_offset + (ndc[0] + 1) * 0.5 * w
y_screen = y_offset + (1 - ndc[1]) * 0.5 * h

return x_screen, y_screen

def get_pick_info(self, pos):
"""
Get pick info at this screen position

Parameters
----------
pos: (x, y)
screen space position

Returns
-------
dict | None
pick info if a graphic is at this position, else None

"""

info = self.renderer.get_pick_info(pos)

if info["world_object"] is not None:
try:
graphic = WORLD_OBJECT_TO_GRAPHIC[info["world_object"]._global_id]
info["graphic"] = graphic
return info

except KeyError:
pass # this world obj is not owned by a graphic

def _render(self):
self._call_animate_functions(self._animate_funcs_pre)

Expand Down
5 changes: 4 additions & 1 deletion fastplotlib/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from ._histogram_lut import HistogramLUTTool
from ._tooltip import Tooltip
from ._tooltip import Tooltip, GraphicTooltip
from ._cursor import Cursor

__all__ = [
"HistogramLUTTool",
"Tooltip",
"GraphicTooltip",
"Cursor",
]
Loading
Loading