Source code for braincell.vis.hooks
# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Interactive pick / hover hooks for the vis backends.
This module defines a backend-agnostic :class:`VisHooks` container plus the
:class:`PickInfo` payload delivered to user callbacks. The matplotlib and
PyVista backends (``backend_matplotlib.py`` / ``backend_pyvista.py``) consume
these at render time and register the appropriate mouse-event callbacks.
The hooks are intentionally coarse: a callback receives enough context to
identify *which branch / segment / x-coordinate* was picked, plus the
underlying scalar value when the scene was built with colour-by-values.
Callers that need the raw matplotlib artist or PyVista mesh can read them
from :attr:`PickInfo.artist`.
Typical usage::
def on_pick(info):
print(f"picked branch {info.branch_name} at x={info.x:.3f}")
plot2d(morpho, hooks=VisHooks(on_pick=on_pick))
"""
from dataclasses import dataclass
from typing import Any, Callable
import numpy as np
PickCallback = Callable[["PickInfo"], None]
LeaveCallback = Callable[[], None]
[docs]
@dataclass(frozen=True)
class PickInfo:
"""Payload delivered to a :class:`VisHooks` callback.
Parameters
----------
branch_index : int
Index of the branch inside ``morpho.branches``.
branch_name : str
Human-readable branch name.
branch_type : str
Branch type string (``"soma"``, ``"apical_dendrite"``, …).
segment_index : int or None
Segment index within the branch (``0`` for the proximal segment),
or ``None`` if the artist does not resolve to a single segment.
x : float or None
Fractional arc-length coordinate along the branch in ``[0, 1]``,
or ``None`` if the pick cannot be placed precisely.
value : float or None
Scalar at the picked segment when the scene carries a
colour-by-values overlay. ``None`` when no values were supplied.
position_um : numpy.ndarray or None
Pick location in scene coordinates (2-D for matplotlib, 3-D for
PyVista). ``None`` when the backend did not report a location.
artist : object or None
The underlying backend artist (e.g. ``LineCollection``,
``pyvista.PolyData``). Opaque to generic code but useful for
tests and advanced callers.
"""
branch_index: int
branch_name: str
branch_type: str
segment_index: int | None = None
x: float | None = None
value: float | None = None
position_um: np.ndarray | None = None
artist: Any = None
[docs]
@dataclass(frozen=True)
class VisHooks:
"""Bundle of interactive callbacks understood by the vis backends.
Hooks are optional; passing ``VisHooks()`` (the default) wires
nothing. When any callback is set the backend takes an interactive
path — for matplotlib that means enabling ``pick_event`` and/or
``motion_notify_event`` on the figure canvas; for PyVista it means
calling :meth:`pyvista.Plotter.enable_point_picking`.
Parameters
----------
on_pick : callable or None
Called with a :class:`PickInfo` whenever the user clicks a
branch. Use this for "click to inspect" tooling.
on_hover : callable or None
Called with a :class:`PickInfo` while the mouse hovers over a
branch. Only delivered by the matplotlib backend at present;
PyVista does not currently expose a hover event.
on_leave : callable or None
Called with no arguments when the mouse moves off a branch
after having hovered over it. Matplotlib only.
Notes
-----
Callbacks are invoked synchronously from the event loop. Keep them
cheap; heavy work belongs in a ``QueueTimer`` or async task.
"""
on_pick: PickCallback | None = None
on_hover: PickCallback | None = None
on_leave: LeaveCallback | None = None
[docs]
def is_active(self) -> bool:
"""Return ``True`` if any callback is wired."""
return any(cb is not None for cb in (self.on_pick, self.on_hover, self.on_leave))