From 7ed812a417e1d3027f0f075470b20c66d8b7f054 Mon Sep 17 00:00:00 2001 From: Kyle Ma Date: Thu, 24 Apr 2025 14:14:03 -0400 Subject: [PATCH 1/3] disable crosshair in titled plot --- .../workbench/plotting/figuremanager.py | 36 +++++++++++++++++++ .../workbench/workbench/plotting/toolbar.py | 21 +++++++++++ 2 files changed, 57 insertions(+) diff --git a/qt/applications/workbench/workbench/plotting/figuremanager.py b/qt/applications/workbench/workbench/plotting/figuremanager.py index 9ee04b7ef3a3..fb5d882417b2 100644 --- a/qt/applications/workbench/workbench/plotting/figuremanager.py +++ b/qt/applications/workbench/workbench/plotting/figuremanager.py @@ -232,6 +232,7 @@ def __init__(self, canvas, num): self.toolbar.sig_waterfall_conversion.connect(self.update_toolbar_waterfall_plot) self.toolbar.sig_change_line_collection_colour_triggered.connect(self.change_line_collection_colour) self.toolbar.sig_hide_plot_triggered.connect(self.hide_plot) + self.toolbar.sig_crosshair_toggle_triggered.connect(self.crosshair_toggle) self.toolbar.setFloatable(False) tbs_height = self.toolbar.sizeHint().height() else: @@ -603,6 +604,41 @@ def _reverse_axis_lines(ax): line.remove() ax.add_line(line) + def crosshair_toggle(self, on): + cid = self.canvas.mpl_connect("motion_notify_event", self.crosshair) + if not on: + self.canvas.mpl_disconnect(cid) + + def crosshair(self, event): + axes = self.canvas.figure.gca() + + # create a crosshair made from horizontal and verticle lines. + self.horizontal_line = axes.axhline(color="r", lw=1.0, ls="-") + self.vertical_line = axes.axvline(color="r", lw=1.0, ls="-") + + def set_cross_hair_visible(visible): + need_redraw = self.horizontal_line.get_visible() != visible + self.horizontal_line.set_visible(visible) + self.vertical_line.set_visible(visible) + return need_redraw + + # if event is out-of-bound we update + if not event.inaxes: + need_redraw = set_cross_hair_visible(False) + if need_redraw: + axes.figure.canvas.draw() + + else: + set_cross_hair_visible(True) + x, y = event.xdata, event.ydata + self.horizontal_line.set_ydata([y]) + self.vertical_line.set_xdata([x]) + self.canvas.draw() + + # after update we remove + self.horizontal_line.remove() + self.vertical_line.remove() + # ----------------------------------------------------------------------------- # Figure control diff --git a/qt/applications/workbench/workbench/plotting/toolbar.py b/qt/applications/workbench/workbench/plotting/toolbar.py index 0e15905a7851..2d3d5ad1d14a 100644 --- a/qt/applications/workbench/workbench/plotting/toolbar.py +++ b/qt/applications/workbench/workbench/plotting/toolbar.py @@ -35,6 +35,7 @@ def _create_script_action(self, text, tooltip_text, mdi_icon, *args): class WorkbenchNavigationToolbar(MantidNavigationToolbar): sig_home_clicked = QtCore.Signal() sig_grid_toggle_triggered = QtCore.Signal(bool) + sig_crosshair_toggle_triggered = QtCore.Signal(bool) sig_active_triggered = QtCore.Signal() sig_hold_triggered = QtCore.Signal() sig_toggle_fit_triggered = QtCore.Signal() @@ -84,6 +85,7 @@ class WorkbenchNavigationToolbar(MantidNavigationToolbar): MantidNavigationTool("Fill Area", "Fill area under curves", "mdi.format-color-fill", "waterfall_fill_area", None), MantidStandardNavigationTools.SEPARATOR, MantidNavigationTool("Help", "Open plotting help documentation", "mdi.help", "launch_plot_help", None), + MantidNavigationTool("Crosshair", "Toggle crosshair", "mdi.plus", "toggle_crosshair", False), MantidNavigationTool("Hide", "Hide the plot", "mdi.eye", "hide_plot", None), ) @@ -94,6 +96,20 @@ def __init__(self, canvas, parent, coordinates=True): dpi_ratio = QtWidgets.QApplication.instance().desktop().physicalDpiX() / 100 self.setIconSize(QtCore.QSize(int(24 * dpi_ratio), int(24 * dpi_ratio))) + def toggle_crosshair(self, enable=None): + if enable is None: + enable = self._actions["toggle_crosshair"].isChecked() + else: + self._actions["toggle_crosshair"].setChecked(enable) + self.sig_crosshair_toggle_triggered.emit(enable) + + def set_crosshair_enabled(self, on): + action = self._actions["toggle_crosshair"] + action.setEnabled(on) + action.setVisible(on) + # Show/hide the separator between this button and help button / waterfall options + self.toggle_separator_visibility(action, on) + def hide_plot(self): self.sig_hide_plot_triggered.emit() @@ -217,6 +233,11 @@ def set_buttons_visibility(self, fig): if figure_type(fig) not in [FigureType.Line, FigureType.Errorbar] or len(fig.get_axes()) > 1: self.set_fit_enabled(False) self.set_superplot_enabled(False) + + # disable crosshair in tiled plots but keep it enabled in color contour plot + if len(fig.get_axes()) > 1 and figure_type(fig) not in [FigureType.Contour]: + self.set_crosshair_enabled(False) + for ax in fig.get_axes(): for artist in ax.get_lines(): try: From f404d32e40623a64c875255d14f144430c91512e Mon Sep 17 00:00:00 2001 From: Kyle Ma Date: Fri, 25 Apr 2025 14:38:08 -0400 Subject: [PATCH 2/3] unit tests for tiled plots and contour plots --- .../workbench/plotting/test/test_toolbar.py | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/qt/applications/workbench/workbench/plotting/test/test_toolbar.py b/qt/applications/workbench/workbench/plotting/test/test_toolbar.py index be6768467040..5bbae53c347b 100644 --- a/qt/applications/workbench/workbench/plotting/test/test_toolbar.py +++ b/qt/applications/workbench/workbench/plotting/test/test_toolbar.py @@ -8,6 +8,7 @@ import unittest from unittest.mock import patch +import numpy as np import matplotlib @@ -19,7 +20,7 @@ from workbench.plotting.figuremanager import MantidFigureCanvas, FigureManagerWorkbench from workbench.plotting.toolbar import WorkbenchNavigationToolbar from mantid.plots.plotfunctions import plot -from mantid.simpleapi import CreateSampleWorkspace, CreateMDHistoWorkspace +from mantid.simpleapi import CreateSampleWorkspace, CreateMDHistoWorkspace, Load @start_qapplication @@ -117,6 +118,39 @@ def test_button_checked_for_plot_with_grid(self, mock_qappthread): # Grid button should be ON because we enabled the grid. self.assertTrue(self._is_grid_button_checked(fig)) + @patch("workbench.plotting.figuremanager.QAppThreadCall") + def test_button_checked_for_plot_with_no_crosshair(self, mock_qappthread): + mock_qappthread.return_value = mock_qappthread + + fig, axes = plt.subplots(subplot_kw={"projection": "mantid"}) + axes.plot([-10, 10], [1, 2]) + # crosshair button should be OFF because we have not enabled the crosshair. + self.assertFalse(self._is_crosshair_button_checked(fig)) + # crosshair button should be visible when there is only 1 axis + self.assertTrue(self._is_crosshair_button_visible(fig)) + + @patch("workbench.plotting.figuremanager.QAppThreadCall") + def test_button_hiden_for_tiled_plots(self, mock_qappthread): + mock_qappthread.return_value = mock_qappthread + + fig, axes = plt.subplots(2) + axes[0].plot([-10, 10], [1, 2]) + axes[1].plot([3, 2, 1], [1, 2, 3]) + # crosshair button should be hidden because this is a tiled plot + self.assertFalse(self._is_crosshair_button_visible(fig)) + self.assertFalse(self._is_crosshair_button_checked(fig)) + + @patch("workbench.plotting.figuremanager.QAppThreadCall") + def test_button_enabled_for_contour_plots(self, mock_qappthread): + mock_qappthread.return_value = mock_qappthread + data = Load("SANSLOQCan2D.nxs") + fig, axes = plt.subplots(subplot_kw={"projection": "mantid"}) + axes.imshow(data, origin="lower", cmap="viridis", aspect="auto") + axes.contour(data, levels=np.linspace(10, 60, 6), colors="yellow", alpha=0.5) + # crosshair button should be visible because this is a contour plot + self.assertTrue(self._is_crosshair_button_visible(fig)) + self.assertFalse(self._is_crosshair_button_checked(fig)) + @patch("workbench.plotting.figuremanager.QAppThreadCall") def test_button_checked_for_plot_with_grid_using_kwargs(self, mock_qappthread): mock_qappthread.return_value = mock_qappthread @@ -266,6 +300,32 @@ def _is_button_enabled(cls, fig, button): fig_manager.toolbar.set_buttons_visibility(fig) return fig_manager.toolbar._actions[button].isEnabled() + @classmethod + def _is_crosshair_button_checked(cls, fig): + """ + Create the figure manager and check whether its toolbar is toggled on or off for the given figure. + We have to explicitly call set_button_visibility() here, which would otherwise be called within the show() + function. + """ + canvas = MantidFigureCanvas(fig) + fig_manager = FigureManagerWorkbench(canvas, 1) + # This is only called when show() is called on the figure manager, so we have to manually call it here. + fig_manager.toolbar.set_buttons_visibility(fig) + return fig_manager.toolbar._actions["toggle_crosshair"].isChecked() + + @classmethod + def _is_crosshair_button_visible(cls, fig): + """ + Create the figure manager and check whether its toolbar is visible for the given figure. + We have to explicitly call set_button_visibility() here, which would otherwise be called within the show() + function. + """ + canvas = MantidFigureCanvas(fig) + fig_manager = FigureManagerWorkbench(canvas, 1) + # This is only called when show() is called on the figure manager, so we have to manually call it here. + fig_manager.toolbar.set_buttons_visibility(fig) + return fig_manager.toolbar._actions["toggle_crosshair"].isVisible() + if __name__ == "__main__": unittest.main() From 20a53d7472568b161d14a5972f69f8d2f7ebc8dd Mon Sep 17 00:00:00 2001 From: Kyle Ma Date: Mon, 28 Apr 2025 11:17:44 -0400 Subject: [PATCH 3/3] release note --- docs/source/release/v6.13.0/Workbench/New_features/39242.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/source/release/v6.13.0/Workbench/New_features/39242.rst diff --git a/docs/source/release/v6.13.0/Workbench/New_features/39242.rst b/docs/source/release/v6.13.0/Workbench/New_features/39242.rst new file mode 100644 index 000000000000..fc4026c2d532 --- /dev/null +++ b/docs/source/release/v6.13.0/Workbench/New_features/39242.rst @@ -0,0 +1 @@ +- A crosshair toggle option has been added in mantidplots on the top right of the toolbar area. THe crosshair button is disabled (invisible) in titled plots.