napari_track_edit.data_views.views.tree_view.tree_plot_fpl

Attributes

_SELECT_COLOR

_EDGE_NONE

_SELECT_EDGE_WIDTH

_BASE_SIZE

_SELECT_BUMP

_SELECT_BUMP_MIN

_MIN_SIZE

_MAX_SIZE

_MARKER_FILL

_SIZE_SCALE_EPS

_NODE_LABEL_MIN_SIZE

_NODE_LABEL_MAX

_NODE_LABEL_COLOR

_NODE_LABEL_FONT_FRACTION

_NODE_LABEL_DIGIT_WIDTH

_DOCK_TRACK_ID_PX

_TRACK_LABEL_MIN_PX

_TRACK_LABEL_MAX

_TOOLTIP_OFFSET

_SYMBOL_SIZE_FACTOR

_RIGHT_CLICK_DRAG_PX

_MARKER_CODES

_DEFAULT_MARKER

_AXIS_COLOR

_DOCK_LEFT_PX

_DOCK_BOTTOM_PX

_DOCK_MINIMAL_PX

_DOCK_WORLD

_AXIS_LINE

_AXIS_EDGE

_DOCK_LEFT_MAX_PX

_TICK_LABEL_CHAR_PX

_TICK_LABEL_MARGIN_PX

_DOCK_RESIZE_EPS_PX

Classes

TreePlot

fastplotlib (pygfx/wgpu) canvas for the lineage tree.

Functions

_flag_canvas_closed(→ None)

Mark a rendercanvas canvas as closed once its C++ widget is gone.

Module Contents

napari_track_edit.data_views.views.tree_view.tree_plot_fpl._SELECT_COLOR
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._EDGE_NONE
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._SELECT_EDGE_WIDTH = 2.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._BASE_SIZE = 10.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._SELECT_BUMP = 6.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._SELECT_BUMP_MIN = 2.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._MIN_SIZE = 3.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._MAX_SIZE = 100.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._MARKER_FILL = 0.9
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._SIZE_SCALE_EPS = 0.02
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._NODE_LABEL_MIN_SIZE = 20.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._NODE_LABEL_MAX = 400
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._NODE_LABEL_COLOR = (0.0, 0.0, 0.0, 1.0)
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._NODE_LABEL_FONT_FRACTION = 0.5
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._NODE_LABEL_DIGIT_WIDTH = 0.55
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._DOCK_TRACK_ID_PX = 22.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._TRACK_LABEL_MIN_PX = 30.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._TRACK_LABEL_MAX = 200
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._TOOLTIP_OFFSET
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._SYMBOL_SIZE_FACTOR
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._RIGHT_CLICK_DRAG_PX = 4.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._MARKER_CODES
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._DEFAULT_MARKER = 101
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._AXIS_COLOR = (0.6, 0.6, 0.6, 1.0)
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._DOCK_LEFT_PX = 54.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._DOCK_BOTTOM_PX = 34.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._DOCK_MINIMAL_PX = 2.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._DOCK_WORLD = 100.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._AXIS_LINE = 88.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._AXIS_EDGE = 10.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._DOCK_LEFT_MAX_PX = 120.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._TICK_LABEL_CHAR_PX = 0.55
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._TICK_LABEL_MARGIN_PX = 14.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._DOCK_RESIZE_EPS_PX = 2.0
napari_track_edit.data_views.views.tree_view.tree_plot_fpl._flag_canvas_closed(canvas_ref: weakref.ref) → None

Mark a rendercanvas canvas as closed once its C++ widget is gone.

class napari_track_edit.data_views.views.tree_view.tree_plot_fpl.TreePlot

Bases: qtpy.QtWidgets.QWidget

fastplotlib (pygfx/wgpu) canvas for the lineage tree.

Drop-in replacement for the pyqtgraph TreePlot: exposes the same signals (node_clicked, jump_to_node, pick_track_id, nodes_selected, update_selection) and the same public methods (update, set_selection, set_view, _update_viewed_data, center_on_node, setMouseEnabled) so TreeWidget needs no changes beyond which class it instantiates.

node_clicked
jump_to_node
pick_track_id
nodes_selected
update_selection
_closed = False
_scrolled = False
view_direction = 'vertical'
plot_type = 'tree'
feature = None
track_df
_node_ids
_id_to_row: dict[int, int]
_positions
_base_colors
_base_sizes
_edge_colors = None
_selected_rows: list[int] = []
_size_scale = 1.0
_row_track_ids
_row_lineage_ids = None
_row_times
_lane_track_ids: dict[int, int]
show_track_ids = False
show_hover_info = True
_drag_start: tuple[float, float] | None = None
_rubber = None
_rpress_xy: tuple | None = None
_figure
_subplot
_scatter = None
_edges = None
_dock_left
_dock_bottom
_ruler_time
_ruler_feature
_time_label
_track_labels
_node_labels
_animations
_canvas_handlers
property scrolled: bool

Whether the user scrolled since the last reset_scrolled call.

X and Y double as zoom modifiers (held down while scrolling) and as plain keyboard shortcuts, so TreeWidget resets this when such a key goes down and checks it when the key comes back up.

reset_scrolled() → None

Forget any scrolling seen so far.

_on_canvas_wheel(ev) → None

Record that the user scrolled over the canvas.

setMouseEnabled(x: bool, y: bool) → None

Restrict zoom/pan to the given axes (X or Y key held). Reconfigures the subplot’s own PanZoomController so it stays correctly event-registered.

close_figure() → None

Close the fastplotlib figure while its Qt widgets are still alive. Closing the figure ourselves, before the widgets are deleted, unregisters the canvas and releases the wgpu/pygfx resources, so it does not crash with “wrapped C/C++ object of type QRenderWidget has been deleted”.

closeEvent(event) → None
update(track_df: pandas.DataFrame, view_direction: str, plot_type: str, feature: str, selected_nodes: list[Any], reset_view: bool | None = False, allow_flip: bool | None = True) → None
set_view(view_direction: str, plot_type: str, reset_view: bool | None = False, allow_flip: bool | None = True) → None

Store the view direction / plot type. Axis rulers update themselves each frame in _update_rulers; allow_flip is accepted for interface parity with the old pyqtgraph TreePlot but is not needed here (orientation is derived from view_direction in _compute_positions).

_update_viewed_data(view_direction: str) → None

Re-apply positions for the given view direction (used by flip_axes).

The rebuild drops the scatter and its selection buffers, so carry the selected node ids across it — a flip changes the layout, not what is selected.

_configure_docks() → None

Assign the two rulers to their docks and size both docks. Both axes are always shown (the perpendicular axis shows tick marks even in tree mode; its number labels are toggled per-mode in _update_rulers). Time axis: left dock (vertical) / bottom dock (horizontal); feature axis: the other dock.

_track_id_axis_shown() → bool

The track-id axis is a tree-mode thing: a feature plot’s perpendicular axis is the feature value, which needs its own numbers.

set_show_hover_info(show: bool) → None

Enable/disable the hover tooltip (Visualization tab toggle).

set_show_track_ids(show: bool) → None

Show/hide track ids along the lane axis (Visualization tab toggle).

_update_rulers() → None

Each frame, sync both rulers’ dock cameras to the main camera’s matching range and lay each ruler along its dock edge next to the plot. The dock is a separate viewport, so the tree is clipped at the axis for free. Number labels show on the time axis always, and on the feature axis only in feature mode (tree mode shows the perpendicular axis as tick marks only, like pyqtgraph).

_sync_left(ruler, bottom, top, minimal, label=None, negated=True) → None

Lay a vertical ruler in the left dock. Normal: axis line just inside the right edge (next to the plot), tick marks toward the plot, numbers in the margin to the left. Minimal (tree-mode perpendicular axis): no line, no numbers, just tick marks near the far-left edge of the canvas.

The ruler’s value always increases from start_pos toward end_pos. negated=True (time axis / tree lanes, whose world y = -value) lays it top->bottom so values increase downward (parent-at-top). negated=False (a numeric feature axis, whose world y = +value) lays it bottom->top so feature values increase upward.

_prepare_ruler(dock, ruler, minimal, rect)

Set up a dock to draw one of the two rulers: frame its camera on rect (the visible range along the ruler, across the dock’s own world width) and pick where the axis sits inside it — next to the plot for a numbered axis, at the canvas edge for a minimal one.

Returns (viewport_size, axis_position), or None if the dock’s viewport is transiently degenerate, in which case the caller must leave the ruler alone (keep the last-good geometry rather than hide it).

_finish_ruler(dock, ruler, dv, minimal)

Lay out the ruler’s ticks for the range its dock camera now shows, and drop the parts a minimal axis does without (its line and its numbers). Returns the stats dict from Ruler.update (tick_step and tick_values).

_fit_left_dock(ruler, tick_values) → None

Widen the left dock when its tick labels no longer fit in it (and let it shrink back when they do). Only the vertical rulers need this: their labels grow sideways, into the dock’s narrow dimension.

Resizing here cannot feed back on itself — the dock’s width does not affect the vertical span the ruler labels, so the labels stay put and the size settles after one frame.

_sync_bottom(ruler, left, right, minimal, label=None) → None

Lay a horizontal ruler in the bottom dock. Normal: axis line just inside the top edge (next to the plot), tick marks toward the plot, numbers in the margin below. Minimal (tree-mode perpendicular axis): no line, no numbers, just tick marks near the bottom edge of the canvas.

_update_track_labels(left, right, bottom, top) → None

Lay one track id per visible lane in the perpendicular axis’ dock, thinning them out so neighbouring labels stay at least _TRACK_LABEL_MIN_PX apart.

Lanes are one unit apart, so the step is a lane count, and lanes that fall between two labels are simply skipped rather than drawn on top of each other.

static _ensure_text_blocks(multi_text, count: int) → None

Grow a MultiText to at least count blocks. Blocks are never removed — they are reused across frames, and surplus ones are blanked by the caller.

_update_node_labels() → None

Write each on-screen node’s id inside its marker, once the markers are drawn big enough to hold the text and few enough of them are visible.

_axis_value_column() → str
_compute_positions(df: pandas.DataFrame) → numpy.ndarray

(N, 3) float32 positions. Vertical: (axis_value, -t). Horizontal swaps.

In horizontal view the perpendicular (y) axis is negated so tree lanes read top-to-bottom like the old pyqtgraph tree. But for a numeric feature the y-axis should increase upward (larger values higher), so feature positions are NOT negated. Feature plots are always horizontal (see TreeWidget), so this only affects the horizontal branch.

_rebuild() → None

Full rebuild of the scene from self.track_df. Only called when the data or view direction changes — not on selection.

_build_track_lookups(df: pandas.DataFrame) → None

Per-row track ids (and lineage ids, where derivable) for the hover tooltip, plus the lane -> track id map behind the track-id axis.

A lineage id is the track id at the root of the lineage: follow parent_track_id (0 means “no parent”) up from each track. Deriving it here keeps the tree view independent of funtracks’ optional lineage feature.

_build_edges(df: pandas.DataFrame, colors: numpy.ndarray)

Vectorized edge build for a single NaN-separated gfx.Line.

Returns (positions, colors) where positions is (3E, 3) laid out as [parent, child, NaN] per edge (the NaN row breaks the polyline into independent segments), and colors matches. Returns (None, None) if there are no edges.

_compute_size_scale() → float

Multiplier on _base_sizes at the current zoom, clamped to [_MIN_SIZE, _MAX_SIZE]. Tree mode only; feature mode returns 1.0, and a transiently degenerate viewport/camera keeps the current scale.

The two axes are not symmetric. Lanes are what must not collide, so the lane spacing alone decides how far the markers shrink — a track densely sampled in time is meant to read as a continuous run of overlapping dots, and letting the time spacing shrink things too would leave the markers permanently at the floor. Growing is the other way round: a marker only grows past its base size once both spacings can take it, so zooming x alone stretches the tree without inflating the dots into their vertical neighbours.

_update_marker_scale() → None

Per frame: resize the markers if the zoom changed the lane width enough.

_select_bump() → float

Extra size for a selected node: shrinks with the markers, but never below _SELECT_BUMP_MIN, so the selection stays visible when zoomed out.

set_selection(selected_nodes: list[Any], plot_type: str) → None
_set_edge_color(row: int, rgba: numpy.ndarray) → None

Set one node’s per-vertex outline color and upload just that element.

_on_canvas_pointer_down(ev) → None
_on_canvas_pointer_move(ev) → None
_on_canvas_pointer_up(ev) → None
_ensure_rubber()
_update_rubber(start: tuple, cur: tuple) → None
_clear_rubber() → None
_reset_view() → None

Fit the view to the node positions, filling the canvas.

Framed directly from the scatter data via camera.show_rect rather than auto_scale: auto_scale unions the bounds of all scene objects (the rulers, the NaN-separated edge line, the idle rubber rectangle), so it can’t reliably tighten to the tree — e.g. when zoomed out, the rulers sit at the wide viewport edges. Framing from self._positions ignores everything else. maintain_aspect=False fills the wide canvas; request_draw is required because a camera change doesn’t repaint on its own between interactions.

_on_click(ev) → None
_on_hover(ev) → None

Tooltip with the identity of the node under the pointer.

_on_hover_leave(ev) → None
_describe_node(row: int) → str

Tooltip text for one row: node, track, lineage (when derivable) and time.

select_points_in_rect(x0, x1, y0, y1) → None

Box-select: emit all node ids whose positions fall in the rect.

center_on_node(node_id: int) → None
_rows_fit(rows: list[int]) → bool

Whether all the given rows are inside the current viewport. True (nothing to do) for an empty list, and if the camera can’t be read.

_center_on_rows(rows: list[int]) → None

Bring the given rows into view.

Keeps the current zoom if the rows already fit in the viewport; otherwise zooms out just enough (per axis) to fit them, and never zooms in. Used for single-node centering and for multi-node selection — e.g. the two endpoints of a just-broken edge, which may now sit far apart: rather than pan to their midpoint at the current zoom (leaving both off-screen), we widen the view so both are visible.