From 21ec11c13d8e05895a138180f3986d5d3bc340f6 Mon Sep 17 00:00:00 2001 From: AnniekStok Date: Fri, 29 Nov 2024 17:07:40 +0100 Subject: [PATCH 1/3] implement button to flip the tree view axes. Only changing the mode, feature, or a new run are allowed to reset the axes. Edits will update the treeview without flipping the axes back to the defaults --- .../views/tree_view/flip_axes_widget.py | 29 +++++++ .../data_views/views/tree_view/tree_widget.py | 86 +++++++++++++------ 2 files changed, 89 insertions(+), 26 deletions(-) create mode 100644 src/motile_plugin/data_views/views/tree_view/flip_axes_widget.py diff --git a/src/motile_plugin/data_views/views/tree_view/flip_axes_widget.py b/src/motile_plugin/data_views/views/tree_view/flip_axes_widget.py new file mode 100644 index 0000000..15339d0 --- /dev/null +++ b/src/motile_plugin/data_views/views/tree_view/flip_axes_widget.py @@ -0,0 +1,29 @@ +from psygnal import Signal +from qtpy.QtWidgets import QGroupBox, QPushButton, QVBoxLayout, QWidget + + +class FlipTreeWidget(QWidget): + """Widget to switch between viewing all nodes versus nodes of one or more lineages in the tree widget""" + + flip_tree = Signal() + + def __init__(self): + super().__init__() + + flip_layout = QVBoxLayout() + display_box = QGroupBox("Plot axes [F]") + flip_button = QPushButton("Flip") + flip_button.clicked.connect(self.flip) + flip_layout.addWidget(flip_button) + display_box.setLayout(flip_layout) + + layout = QVBoxLayout() + layout.addWidget(display_box) + self.setLayout(layout) + display_box.setMaximumWidth(90) + display_box.setMaximumHeight(82) + + def flip(self): + """Send a signal to flip the axes of the plot""" + + self.flip_tree.emit() diff --git a/src/motile_plugin/data_views/views/tree_view/tree_widget.py b/src/motile_plugin/data_views/views/tree_view/tree_widget.py index fc3a590..b22c777 100644 --- a/src/motile_plugin/data_views/views/tree_view/tree_widget.py +++ b/src/motile_plugin/data_views/views/tree_view/tree_widget.py @@ -19,6 +19,7 @@ from motile_plugin.data_views.views_coordinator.tracks_viewer import TracksViewer +from .flip_axes_widget import FlipTreeWidget from .navigation_widget import NavigationWidget from .tree_view_feature_widget import TreeViewFeatureWidget from .tree_view_mode_widget import TreeViewModeWidget @@ -124,6 +125,7 @@ def update( feature: str, selected_nodes: list[Any], reset_view: bool | None = False, + allow_flip: bool | None = True, ): """Update the entire view, including the data, view direction, and selected nodes @@ -136,11 +138,15 @@ def update( """ self.set_data(track_df, feature) self._update_viewed_data(view_direction) # this can be expensive - self.set_view(view_direction, feature, reset_view) + self.set_view(view_direction, feature, reset_view, allow_flip) self.set_selection(selected_nodes, feature) def set_view( - self, view_direction: str, feature: str, reset_view: bool | None = False + self, + view_direction: str, + feature: str, + reset_view: bool | None = False, + allow_flip: bool | None = True, ): """Set the view direction, saving the new value as an attribute and changing the axes labels. Shortcuts if the view direction is already @@ -153,31 +159,32 @@ def set_view( """ if view_direction == self.view_direction and feature == self.feature: - if view_direction == "horizontal" or reset_view: + if reset_view: self.autoRange() return - if view_direction == "vertical": - self.setLabel("left", text="Time Point") - self.getAxis("left").setStyle(showValues=True) - if feature == "tree": - self.getAxis("bottom").setStyle(showValues=False) - self.setLabel("bottom", text="") - else: # should this actually ever happen? - self.getAxis("bottom").setStyle(showValues=True) - self.setLabel("bottom", text="Object size in calibrated units") - self.autoRange() - self.invertY(True) # to show tracks from top to bottom - elif view_direction == "horizontal": - self.setLabel("bottom", text="Time Point") - self.getAxis("bottom").setStyle(showValues=True) - if feature == "tree": - self.setLabel("left", text="") - self.getAxis("left").setStyle(showValues=False) - else: - self.setLabel("left", text="Object size in calibrated units") + if allow_flip: + if view_direction == "vertical": + self.setLabel("left", text="Time Point") self.getAxis("left").setStyle(showValues=True) - self.autoRange() - self.invertY(False) + if feature == "tree": + self.getAxis("bottom").setStyle(showValues=False) + self.setLabel("bottom", text="") + else: # should this actually ever happen? + self.getAxis("bottom").setStyle(showValues=True) + self.setLabel("bottom", text="Object size in calibrated units") + self.autoRange() + self.invertY(True) # to show tracks from top to bottom + elif view_direction == "horizontal": + self.setLabel("bottom", text="Time Point") + self.getAxis("bottom").setStyle(showValues=True) + if feature == "tree": + self.setLabel("left", text="") + self.getAxis("left").setStyle(showValues=False) + else: + self.setLabel("left", text="Object size in calibrated units") + self.getAxis("left").setStyle(showValues=True) + self.autoRange() + self.invertY(False) if ( self.view_direction != view_direction or self.feature != feature @@ -439,18 +446,23 @@ def __init__(self, viewer: napari.Viewer): self.feature, ) + # Add widget to flip the axes + self.flip_widget = FlipTreeWidget() + self.flip_widget.flip_tree.connect(self._flip_axes) + # Construct a toolbar and set main layout panel_layout = QHBoxLayout() panel_layout.addWidget(self.mode_widget) panel_layout.addWidget(self.feature_widget) panel_layout.addWidget(self.navigation_widget) + panel_layout.addWidget(self.flip_widget) panel_layout.setSpacing(0) panel_layout.setContentsMargins(0, 0, 0, 0) panel = QWidget() panel.setLayout(panel_layout) - panel.setMaximumWidth(820) - panel.setMaximumHeight(78) + panel.setMaximumWidth(930) + panel.setMaximumHeight(82) # Make a collapsible for TreeView widgets collapsable_widget = QCollapsible("Show/Hide Tree View Controls") @@ -476,6 +488,7 @@ def keyPressEvent(self, event: QKeyEvent) -> None: Qt.Key_R: self.redo, Qt.Key_Q: self.toggle_display_mode, Qt.Key_W: self.toggle_feature_mode, + Qt.Key_F: self._flip_axes, Qt.Key_X: lambda: self.set_mouse_enabled(x=True, y=False), Qt.Key_Y: lambda: self.set_mouse_enabled(x=False, y=True), } @@ -525,6 +538,22 @@ def toggle_feature_mode(self): """Toggle feature mode.""" self.feature_widget._toggle_feature_mode() + def _flip_axes(self): + """Flip the axes of the plot""" + + if self.view_direction == "horizontal": + self.view_direction = "vertical" + else: + self.view_direction = "horizontal" + + self.navigation_widget.view_direction = self.view_direction + self.tree_widget._update_viewed_data(self.view_direction) + self.tree_widget.set_view( + view_direction=self.view_direction, + feature=self.tree_widget.feature, + reset_view=False, + ) + def set_mouse_enabled(self, x: bool, y: bool): """Enable or disable mouse zoom scrolling in X or Y direction.""" self.tree_widget.setMouseEnabled(x=x, y=y) @@ -593,6 +622,9 @@ def _update_track_data(self, reset_view: bool | None = None) -> None: self.view_direction = "vertical" self.feature = "tree" self.feature_widget.show_tree_radio.setChecked(True) + allow_flip = True + else: + allow_flip = False # also update the navigation widget self.navigation_widget.track_df = self.track_df @@ -607,6 +639,7 @@ def _update_track_data(self, reset_view: bool | None = None) -> None: self.feature, self.selected_nodes, reset_view=reset_view, + allow_flip=allow_flip, ) else: @@ -616,6 +649,7 @@ def _update_track_data(self, reset_view: bool | None = None) -> None: self.feature, self.selected_nodes, reset_view=reset_view, + allow_flip=allow_flip, ) def _set_mode(self, mode: str) -> None: From 0bc651ff32a359d5d7a8ed16b5507a227b12abca Mon Sep 17 00:00:00 2001 From: Caroline Malin-Mayor Date: Mon, 9 Dec 2024 11:03:55 -0500 Subject: [PATCH 2/3] Update FlipTreeWidgt docstring --- .../data_views/views/tree_view/flip_axes_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/motile_plugin/data_views/views/tree_view/flip_axes_widget.py b/src/motile_plugin/data_views/views/tree_view/flip_axes_widget.py index 15339d0..8721e77 100644 --- a/src/motile_plugin/data_views/views/tree_view/flip_axes_widget.py +++ b/src/motile_plugin/data_views/views/tree_view/flip_axes_widget.py @@ -3,7 +3,7 @@ class FlipTreeWidget(QWidget): - """Widget to switch between viewing all nodes versus nodes of one or more lineages in the tree widget""" + """Widget to flip the axis of the tree view""" flip_tree = Signal() From a03baf152de301edacc7167cdea4a1e63d552d11 Mon Sep 17 00:00:00 2001 From: Caroline Malin-Mayor Date: Mon, 9 Dec 2024 11:23:27 -0500 Subject: [PATCH 3/3] Refactor axis label function to be more generic --- .../data_views/views/tree_view/tree_widget.py | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/motile_plugin/data_views/views/tree_view/tree_widget.py b/src/motile_plugin/data_views/views/tree_view/tree_widget.py index b22c777..d634caf 100644 --- a/src/motile_plugin/data_views/views/tree_view/tree_widget.py +++ b/src/motile_plugin/data_views/views/tree_view/tree_widget.py @@ -162,29 +162,31 @@ def set_view( if reset_view: self.autoRange() return + + axis_titles = { + "time": "Time Point", + "area": "Object size in calibrated units", + "tree": "", + } if allow_flip: if view_direction == "vertical": - self.setLabel("left", text="Time Point") - self.getAxis("left").setStyle(showValues=True) - if feature == "tree": - self.getAxis("bottom").setStyle(showValues=False) - self.setLabel("bottom", text="") - else: # should this actually ever happen? - self.getAxis("bottom").setStyle(showValues=True) - self.setLabel("bottom", text="Object size in calibrated units") - self.autoRange() + time_axis = "left" # time is on y axis + feature_axis = "bottom" self.invertY(True) # to show tracks from top to bottom - elif view_direction == "horizontal": - self.setLabel("bottom", text="Time Point") - self.getAxis("bottom").setStyle(showValues=True) - if feature == "tree": - self.setLabel("left", text="") - self.getAxis("left").setStyle(showValues=False) - else: - self.setLabel("left", text="Object size in calibrated units") - self.getAxis("left").setStyle(showValues=True) - self.autoRange() + else: + time_axis = "bottom" # time is on y axis + feature_axis = "left" self.invertY(False) + self.setLabel(time_axis, text=axis_titles["time"]) + self.getAxis(time_axis).setStyle(showValues=True) + + self.setLabel(feature_axis, text=axis_titles[feature]) + if feature == "tree": + self.getAxis(feature).setStyle(showValues=False) + else: + self.getAxis(feature_axis).setStyle(showValues=True) + self.autoRange() # not sure if this is necessary or not + if ( self.view_direction != view_direction or self.feature != feature