Source code for fastplotlib.tools._tooltip

from functools import partial

import numpy as np
import pygfx

from ..graphics import LineGraphic, ImageGraphic, ScatterGraphic, Graphic
from ..graphics.features import GraphicFeatureEvent


class MeshMasks:
    """Used set the x0, x1, y0, y1 positions of the plane mesh"""

    x0 = np.array(
        [
            [False, False, False],
            [True, False, False],
            [False, False, False],
            [True, False, False],
        ]
    )

    x1 = np.array(
        [
            [True, False, False],
            [False, False, False],
            [True, False, False],
            [False, False, False],
        ]
    )

    y0 = np.array(
        [
            [False, True, False],
            [False, True, False],
            [False, False, False],
            [False, False, False],
        ]
    )

    y1 = np.array(
        [
            [False, False, False],
            [False, False, False],
            [False, True, False],
            [False, True, False],
        ]
    )


masks = MeshMasks


[docs] class Tooltip: def __init__(self): # text object self._text = pygfx.Text( text="", font_size=12, screen_space=False, anchor="bottom-left", material=pygfx.TextMaterial( color="w", outline_color="w", outline_thickness=0.0, pick_write=False, ), ) # plane for the background of the text object geometry = pygfx.plane_geometry(1, 1) material = pygfx.MeshBasicMaterial(color=(0.1, 0.1, 0.3, 0.95)) self._plane = pygfx.Mesh(geometry, material) # else text not visible self._plane.world.z = 0.5 # line to outline the plane mesh self._line = pygfx.Line( geometry=pygfx.Geometry( positions=np.array( [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], ], dtype=np.float32, ) ), material=pygfx.LineThinMaterial(thickness=1.0, color=(0.8, 0.8, 1.0, 1.0)), ) self._world_object = pygfx.Group() self._world_object.add(self._plane, self._text, self._line) # padded to bbox so the background box behind the text extends a bit further # making the text easier to read self._padding = np.array([[5, 5, 0], [-5, -5, 0]], dtype=np.float32) self._registered_graphics = dict() @property def world_object(self) -> pygfx.Group: return self._world_object @property def font_size(self): """Get or set font size""" return self._text.font_size @font_size.setter def font_size(self, size: float): self._text.font_size = size @property def text_color(self): """Get or set text color using a str or RGB(A) array""" return self._text.material.color @text_color.setter def text_color(self, color: str | tuple | list | np.ndarray): self._text.material.color = color @property def background_color(self): """Get or set background color using a str or RGB(A) array""" return self._plane.material.color @background_color.setter def background_color(self, color: str | tuple | list | np.ndarray): self._plane.material.color = color @property def outline_color(self): """Get or set outline color using a str or RGB(A) array""" return self._line.material.color @outline_color.setter def outline_color(self, color: str | tuple | list | np.ndarray): self._line.material.color = color @property def padding(self) -> np.ndarray: """ Get or set the background padding in number of pixels. The padding defines the number of pixels around the tooltip text that the background is extended by. """ return self.padding[0, :2].copy() @padding.setter def padding(self, padding_xy: tuple[float, float]): self._padding[0, :2] = padding_xy self._padding[1, :2] = -np.asarray(padding_xy) def _set_position(self, pos: tuple[float, float]): """ Set the position of the tooltip Parameters ---------- pos: [float, float] position in screen space """ # need to flip due to inverted y x, y = pos[0], pos[1] # put the tooltip slightly to the top right of the cursor positoin x += 8 y -= 8 self._text.world.position = (x, -y, 0) bbox = self._text.get_world_bounding_box() - self._padding [[x0, y0, _], [x1, y1, _]] = bbox self._plane.geometry.positions.data[masks.x0] = x0 self._plane.geometry.positions.data[masks.x1] = x1 self._plane.geometry.positions.data[masks.y0] = y0 self._plane.geometry.positions.data[masks.y1] = y1 self._plane.geometry.positions.update_range() # line points pts = [[x0, y0], [x0, y1], [x1, y1], [x1, y0], [x0, y0]] self._line.geometry.positions.data[:, :2] = pts self._line.geometry.positions.update_range() def _event_handler(self, custom_tooltip: callable, ev: pygfx.PointerEvent): """Handles the tooltip appear event, determines the text to be set in the tooltip""" if custom_tooltip is not None: info = custom_tooltip(ev) elif isinstance(ev.graphic, ImageGraphic): col, row = ev.pick_info["index"] if ev.graphic.data.value.ndim == 2: info = str(ev.graphic.data[row, col]) else: info = "\n".join( f"{channel}: {val}" for channel, val in zip("rgba", ev.graphic.data[row, col]) ) elif isinstance(ev.graphic, (LineGraphic, ScatterGraphic)): index = ev.pick_info["vertex_index"] info = "\n".join( f"{dim}: {val}" for dim, val in zip("xyz", ev.graphic.data[index]) ) else: raise TypeError("Unsupported graphic") # make the tooltip object visible self.world_object.visible = True # set the text and top left position of the tooltip self._text.set_text(info) self._set_position((ev.x, ev.y)) def _clear(self, ev): self._text.set_text("") self.world_object.visible = False
[docs] def register( self, graphic: Graphic, appear_event: str = "pointer_move", disappear_event: str = "pointer_leave", custom_info: callable = None, ): """ Register a Graphic to display tooltips. **Note:** if the passed graphic is already registered then it first unregistered and then re-registered using the given arguments. Parameters ---------- graphic: Graphic Graphic to register appear_event: str, default "pointer_move" the pointer that triggers the tooltip to appear. Usually one of "pointer_move" | "click" | "double_click" disappear_event: str, default "pointer_leave" the event that triggers the tooltip to disappear, does not have to be a pointer event. custom_info: callable, default None a custom function that takes the pointer event defined as the `appear_event` and returns the text to display in the tooltip """ if graphic in list(self._registered_graphics.keys()): # unregister first and then re-register self.unregister(graphic) pfunc = partial(self._event_handler, custom_info) graphic.add_event_handler(pfunc, appear_event) graphic.add_event_handler(self._clear, disappear_event) self._registered_graphics[graphic] = (pfunc, appear_event, disappear_event) # automatically unregister when graphic is deleted graphic.add_event_handler(self.unregister, "deleted")
[docs] def unregister(self, graphic: Graphic): """ Unregister a Graphic to no longer display tooltips for this graphic. **Note:** if the passed graphic is not registered then it is just ignored without raising any exception. Parameters ---------- graphic: Graphic Graphic to unregister """ if isinstance(graphic, GraphicFeatureEvent): # this happens when the deleted event is triggered graphic = graphic.graphic if graphic not in self._registered_graphics: return # get pfunc and event names pfunc, appear_event, disappear_event = self._registered_graphics.pop(graphic) # remove handlers from graphic graphic.remove_event_handler(pfunc, appear_event) graphic.remove_event_handler(self._clear, disappear_event)
[docs] def unregister_all(self): """unregister all graphics""" for graphic in self._registered_graphics.keys(): self.unregister(graphic)