Skip to content

Commit a58b50b

Browse files
authored
Merge pull request #272 from funkelab/95-visualize-by-features-other-than-area-in-the-tree-view
95 visualize-by-features-other-than-area-in-the-tree-view
2 parents 57a4063 + 9d6e358 commit a58b50b

File tree

5 files changed

+214
-119
lines changed

5 files changed

+214
-119
lines changed

src/motile_tracker/data_views/_tests/test_tree_widget_utils.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import napari
44
import networkx as nx
5+
import numpy as np
56
import pandas as pd
67
import pytest
78
from funtracks.data_model import EdgeAttr, NodeAttr, SolutionTracks
9+
from funtracks.features import Feature
810

911
from motile_tracker.data_views.views.tree_view.tree_widget_utils import (
1012
extract_sorted_tracks,
@@ -127,10 +129,14 @@ def assign_tracklet_ids(graph: nx.DiGraph) -> tuple[nx.DiGraph, list[Any], int]:
127129

128130
def test_track_df(graph_2d):
129131
tracks = SolutionTracks(graph=graph_2d, ndim=3)
130-
del tracks.graph.nodes[2]["area"]
131-
132-
assert tracks.get_area(1) == 1245
133-
assert tracks.get_area(2) is None
132+
for node in tracks.graph.nodes():
133+
if node != 2:
134+
tracks.graph.nodes[node]["custom_attr"] = node * 10
135+
tracks.features["custom_attr"] = Feature(
136+
feature_type="node",
137+
value_type="int",
138+
num_values=1,
139+
)
134140

135141
tracks.graph, _, _ = assign_tracklet_ids(tracks.graph)
136142

@@ -142,6 +148,5 @@ def test_track_df(graph_2d):
142148

143149
track_df, _ = extract_sorted_tracks(tracks, colormap)
144150
assert isinstance(track_df, pd.DataFrame)
145-
assert track_df.loc[track_df["node_id"] == 1, "area"].values[0] == 1245
146-
assert track_df.loc[track_df["node_id"] == 2, "area"].values[0] == 0
147-
assert track_df["area"].notna().all()
151+
assert track_df.loc[track_df["node_id"] == 1, "custom_attr"].values[0] == 10
152+
assert np.isnan(track_df.loc[track_df["node_id"] == 2, "custom_attr"].values[0])

src/motile_tracker/data_views/views/tree_view/navigation_widget.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,24 @@ def __init__(
2121
lineage_df: pd.DataFrame,
2222
view_direction: str,
2323
selected_nodes: NodeSelectionList,
24-
feature: str,
24+
plot_type: str,
2525
):
2626
"""Widget for controlling navigation in the tree widget
2727
2828
Args:
2929
track_df (pd.DataFrame): The dataframe holding the track information
30-
view_direction (str): The view direction of the tree widget. Options:
31-
"vertical", "horizontal".
30+
view_direction (str): The view direction of the tree widget. Options: "vertical", "horizontal".
3231
selected_nodes (NodeSelectionList): The list of selected nodes.
33-
feature (str): The feature currently being displayed
32+
plot_type (str): either 'tree' or 'feature'
3433
"""
3534

3635
super().__init__()
3736
self.track_df = track_df
3837
self.lineage_df = lineage_df
3938
self.view_direction = view_direction
4039
self.selected_nodes = selected_nodes
41-
self.feature = feature
40+
self.plot_type = plot_type
41+
self.feature = None
4242

4343
navigation_box = QGroupBox("Navigation [\u2b05 \u27a1 \u2b06 \u2b07]")
4444
navigation_layout = QHBoxLayout()
@@ -102,8 +102,8 @@ def move(self, direction: str) -> None:
102102
next_node = self.get_next_track_node(
103103
self.lineage_df, node_id, forward=False
104104
)
105-
# if not found, look in the whole dataframe to enable jumping to the
106-
# next node outside the current tree view content
105+
# if not found, look in the whole dataframe
106+
# to enable jumping to the next node outside the current tree view content
107107
if next_node is None:
108108
next_node = self.get_next_track_node(
109109
self.track_df, node_id, forward=False
@@ -123,14 +123,13 @@ def get_next_track_node(
123123
"""Get the node at the same time point in an adjacent track.
124124
125125
Args:
126-
df (pd.DataFrame): The dataframe to be used (full track_df or subset
127-
lineage_df).
126+
df (pd.DataFrame): The dataframe to be used (full track_df or subset lineage_df).
128127
node_id (str): The current node ID to get the next from.
129128
forward (bool, optional): If true, pick the next track (right/down).
130129
Otherwise, pick the previous track (left/up). Defaults to True.
131130
"""
132131
# Determine which axis to use for finding neighbors
133-
axis_label = "area" if self.feature == "area" else "x_axis_pos"
132+
axis_label = self.feature if self.plot_type == "feature" else "x_axis_pos"
134133

135134
if df.empty:
136135
return None
@@ -153,7 +152,6 @@ def get_next_track_node(
153152
)
154153
neighbor = neighbors.loc[closest_index_label, "node_id"]
155154
return neighbor
156-
return None
157155

158156
def get_predecessor(self, node_id: str) -> str | None:
159157
"""Get the predecessor node of the given node_id
@@ -171,7 +169,6 @@ def get_predecessor(self, node_id: str) -> str | None:
171169
parent_row = self.track_df.loc[self.track_df["node_id"] == parent_id]
172170
if not parent_row.empty:
173171
return parent_row["node_id"].values[0]
174-
return None
175172

176173
def get_successor(self, node_id: str) -> str | None:
177174
"""Get the successor node of the given node_id. If there are two children,
@@ -188,4 +185,3 @@ def get_successor(self, node_id: str) -> str | None:
188185
if not children.empty:
189186
child = children.to_dict("records")[0]
190187
return child["node_id"]
191-
return None
Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from psygnal import Signal
22
from qtpy.QtWidgets import (
33
QButtonGroup,
4+
QComboBox,
45
QGroupBox,
56
QHBoxLayout,
67
QRadioButton,
@@ -10,53 +11,97 @@
1011

1112

1213
class TreeViewFeatureWidget(QWidget):
13-
"""Widget to switch between viewing all nodes versus nodes of one or more lineages
14-
in the tree widget
15-
"""
14+
"""Widget to switch between viewing all nodes versus nodes of one or more lineages in the tree widget"""
1615

17-
change_feature = Signal(str)
16+
change_plot_type = Signal(str)
1817

19-
def __init__(self):
18+
def __init__(self, features: list[str]):
2019
super().__init__()
2120

22-
self.feature = "tree"
21+
self.plot_type = "tree"
2322

24-
display_box = QGroupBox("Feature [W]")
23+
display_box = QGroupBox("Plot [W]")
2524
display_layout = QHBoxLayout()
2625
button_group = QButtonGroup()
2726
self.show_tree_radio = QRadioButton("Lineage Tree")
2827
self.show_tree_radio.setChecked(True)
29-
self.show_tree_radio.clicked.connect(lambda: self._set_feature("tree"))
30-
self.show_area_radio = QRadioButton("Object size")
31-
self.show_area_radio.clicked.connect(lambda: self._set_feature("area"))
28+
self.show_tree_radio.clicked.connect(lambda: self._set_plot_type("tree"))
29+
self.show_area_radio = QRadioButton("Feature")
30+
self.show_area_radio.clicked.connect(lambda: self._set_plot_type("feature"))
3231
button_group.addButton(self.show_tree_radio)
3332
button_group.addButton(self.show_area_radio)
3433
display_layout.addWidget(self.show_tree_radio)
3534
display_layout.addWidget(self.show_area_radio)
35+
36+
self.feature_dropdown = QComboBox()
37+
for feature in features:
38+
self.feature_dropdown.addItem(feature)
39+
self.feature_dropdown.currentIndexChanged.connect(self._update_feature)
40+
if len(features) > 0:
41+
self.current_feature = features[0]
42+
else:
43+
self.current_feature = None
44+
self.show_area_radio.setEnabled(False)
45+
display_layout.addWidget(self.feature_dropdown)
46+
3647
display_box.setLayout(display_layout)
37-
display_box.setMaximumWidth(250)
48+
display_box.setMaximumWidth(400)
3849
display_box.setMaximumHeight(60)
3950

4051
layout = QVBoxLayout()
4152
layout.addWidget(display_box)
4253

4354
self.setLayout(layout)
4455

45-
def _toggle_feature_mode(self, event=None) -> None:
56+
def _toggle_plot_type(self, event=None) -> None:
4657
"""Toggle display mode"""
4758

4859
if (
4960
self.show_area_radio.isEnabled
5061
): # if button is disabled, toggle is not allowed
51-
if self.feature == "area":
52-
self._set_feature("tree")
62+
if self.plot_type == "feature":
63+
self._set_plot_type("tree")
5364
self.show_tree_radio.setChecked(True)
5465
else:
55-
self._set_feature("area")
66+
self._set_plot_type("feature")
5667
self.show_area_radio.setChecked(True)
5768

58-
def _set_feature(self, mode: str):
69+
def _set_plot_type(self, plot_type: str):
5970
"""Emit signal to change the display mode"""
6071

61-
self.feature = mode
62-
self.change_feature.emit(mode)
72+
self.plot_type = plot_type
73+
self.change_plot_type.emit(plot_type)
74+
75+
def _update_feature(self) -> None:
76+
"""Update the feature to be plotted if the plot_type == 'feature'"""
77+
78+
self.current_feature = self.feature_dropdown.currentText()
79+
self.change_plot_type.emit(self.plot_type)
80+
81+
def get_current_feature(self):
82+
"""Return the current feature that is being plotted"""
83+
84+
return self.current_feature
85+
86+
def update_feature_dropdown(self, feature_list: list[str]) -> None:
87+
"""Update the list of features in the dropdown"""
88+
89+
self.feature_dropdown.currentIndexChanged.disconnect(self._update_feature)
90+
self.feature_dropdown.clear()
91+
self.show_area_radio.setEnabled(True)
92+
for feature in feature_list:
93+
self.feature_dropdown.addItem(feature)
94+
95+
if self.current_feature not in feature_list:
96+
if len(feature_list) > 0:
97+
self.current_feature = feature_list[0]
98+
self.feature_dropdown.setCurrentIndex(0)
99+
else:
100+
self.current_feature = None
101+
self.show_area_radio.setEnabled(False)
102+
else:
103+
self.feature_dropdown.setCurrentIndex(
104+
self.feature_dropdown.findText(self.current_feature)
105+
)
106+
107+
self.feature_dropdown.currentIndexChanged.connect(self._update_feature)

0 commit comments

Comments
 (0)