def test_disconnect_called_for_each_registered_handler(self): fig_manager = MagicMock() canvas = MagicMock() fig_manager.canvas = canvas interactor = FigureInteraction(fig_manager) interactor.disconnect() self.assertEqual(interactor.nevents, canvas.mpl_disconnect.call_count)
def test_disconnect_called_for_each_registered_handler(self): fig_manager = MagicMock() canvas = MagicMock() fig_manager.canvas = canvas interactor = FigureInteraction(fig_manager) interactor.disconnect() self.assertEqual(interactor.nevents, canvas.mpl_disconnect.call_count)
def test_correct_yunit_label_when_overplotting_after_normaliztion_toggle( self): # The earlier version of Matplotlib on RHEL throws an error when performing the second # plot in this test, if the lines have errorbars. The error occurred when it attempted # to draw an interactive legend. Plotting without errors still fulfills the purpose of this # test, so turn them off for old Matplotlib versions. errors = True if int(matplotlib.__version__[0]) < 2: errors = False fig = plot([self.ws], spectrum_nums=[1], errors=errors, plot_kwargs={'distribution': True}) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) ax = fig.axes[0] fig_interactor._toggle_normalization(ax) self.assertEqual("Counts ($\AA$)$^{-1}$", ax.get_ylabel()) plot([self.ws1], spectrum_nums=[1], errors=errors, overplot=True, fig=fig) self.assertEqual("Counts ($\AA$)$^{-1}$", ax.get_ylabel())
def test_right_click_gives_context_menu_for_plot_without_fit_enabled( self, mocked_figure_type, mocked_qmenu_cls): fig_manager = self._create_mock_fig_manager_to_accept_right_click() fig_manager.fit_browser.tool = None interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mocked_figure_type.return_value = FigureType.Line # Expect a call to QMenu() for the outer menu followed by two more calls # for the Axes and Normalization menus qmenu_call1 = MagicMock() qmenu_call2 = MagicMock() qmenu_call3 = MagicMock() mocked_qmenu_cls.side_effect = [qmenu_call1, qmenu_call2, qmenu_call3] with patch('workbench.plotting.figureinteraction.QActionGroup', autospec=True): with patch.object(interactor.toolbar_manager, 'is_tool_active', lambda: False): with patch.object(interactor.errors_manager, 'add_error_bars_menu', MagicMock()): interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, qmenu_call1.addSeparator.call_count) self.assertEqual(0, qmenu_call1.addAction.call_count) expected_qmenu_calls = [ call(), call("Axes", qmenu_call1), call("Normalization", qmenu_call1) ] self.assertEqual(expected_qmenu_calls, mocked_qmenu_cls.call_args_list) # 4 actions in Axes submenu self.assertEqual(4, qmenu_call2.addAction.call_count) # 2 actions in Normalization submenu self.assertEqual(2, qmenu_call3.addAction.call_count)
def test_right_click_gives_context_menu_for_color_plot(self, mocked_figure_type, mocked_qmenu): fig_manager = self._create_mock_fig_manager_to_accept_right_click() interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mocked_figure_type.return_value = FigureType.Image # Expect a call to QMenu() for the outer menu followed by three more calls # for the Axes, Normalization and Colorbar menus qmenu_call1 = MagicMock() qmenu_call2 = MagicMock() qmenu_call3 = MagicMock() qmenu_call4 = MagicMock() mocked_qmenu.side_effect = [qmenu_call1, qmenu_call2, qmenu_call3, qmenu_call4] with patch('workbench.plotting.figureinteraction.QActionGroup', autospec=True): with patch.object(interactor.toolbar_manager, 'is_tool_active', lambda: False): interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, qmenu_call1.addAction.call_count) expected_qmenu_calls = [call(), call("Axes", qmenu_call1), call("Normalization", qmenu_call1), call("Color bar", qmenu_call1)] self.assertEqual(expected_qmenu_calls, mocked_qmenu.call_args_list) # 4 actions in Axes submenu self.assertEqual(4, qmenu_call2.addAction.call_count) # 2 actions in Normalization submenu self.assertEqual(2, qmenu_call3.addAction.call_count) # 2 actions in Colorbar submenu self.assertEqual(2, qmenu_call4.addAction.call_count)
def test_right_click_gives_no_context_menu_for_color_plot( self, mocked_figure_type, mocked_qmenu): fig_manager = self._create_mock_fig_manager_to_accept_right_click() interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mocked_figure_type.return_value = FigureType.Image interactor.on_mouse_button_press(mouse_event) mocked_qmenu.assert_not_called()
def test_right_click_gives_no_context_menu_for_color_plot(self, mocked_figure_type, mocked_qmenu): fig_manager = self._create_mock_fig_manager_to_accept_right_click() interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mocked_figure_type.return_value = FigureType.Image interactor.on_mouse_button_press(mouse_event) mocked_qmenu.assert_not_called()
def test_right_click_gives_no_context_menu_for_empty_figure(self, mocked_figure_type, mocked_qmenu): fig_manager = self._create_mock_fig_manager_to_accept_right_click() interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mocked_figure_type.return_value = FigureType.Empty with patch.object(interactor.toolbar_manager, 'is_tool_active', lambda: False): interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, mocked_qmenu.call_count)
def test_toggle_normalisation_on_contour_plot_maintains_contour_line_colour(self): from mantid.plots.legend import convert_color_to_hex ws = CreateWorkspace(DataX=[1, 2, 3, 4, 2, 4, 6, 8], DataY=[2] * 8, NSpec=2, OutputWorkspace="test_ws") fig = plot_contour([ws]) for col in fig.get_axes()[0].collections: col.set_color("#ff9900") mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) fig_interactor._toggle_normalization(fig.axes[0]) self.assertTrue(all(convert_color_to_hex(col.get_color()[0]) == "#ff9900" for col in fig.get_axes()[0].collections))
def test_context_menu_change_axis_scale_is_axis_aware(self): fig = plot([self.ws, self.ws1], spectrum_nums=[1, 1], tiled=True) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) scale_types = ("log", "log") ax = fig.axes[0] ax1 = fig.axes[1] current_scale_types = (ax.get_xscale(), ax.get_yscale()) current_scale_types1 = (ax1.get_xscale(), ax1.get_yscale()) self.assertEqual(current_scale_types, current_scale_types1) fig_interactor._quick_change_axes(scale_types, ax) current_scale_types2 = (ax.get_xscale(), ax.get_yscale()) self.assertNotEqual(current_scale_types2, current_scale_types1)
def test_right_click_gives_context_menu_for_plot_without_fit_enabled( self, mocked_figure_type, mocked_qmenu_cls): fig_manager = self._create_mock_fig_manager_to_accept_right_click() fig_manager.fit_browser.tool = None interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mouse_event.inaxes.get_xlim.return_value = (1, 2) mouse_event.inaxes.get_ylim.return_value = (1, 2) mouse_event.inaxes.lines = [] mocked_figure_type.return_value = FigureType.Line # Expect a call to QMenu() for the outer menu followed by two more calls # for the Axes, Normalization, and Markers menus outer_qmenu_call = MagicMock() qmenu_call2 = MagicMock() qmenu_call3 = MagicMock() qmenu_call4 = MagicMock() mocked_qmenu_cls.side_effect = [ outer_qmenu_call, qmenu_call2, qmenu_call3, qmenu_call4 ] with patch('workbench.plotting.figureinteraction.QActionGroup', autospec=True): with patch('workbench.plotting.figureinteraction.QAction'): with patch.object(interactor.toolbar_manager, 'is_tool_active', lambda: False): with patch.object(interactor, 'add_error_bars_menu', MagicMock()): interactor.on_mouse_button_press(mouse_event) self.assertEqual( 0, outer_qmenu_call.addSeparator.call_count) self.assertEqual(1, outer_qmenu_call.addAction. call_count) # Show/hide legend action expected_qmenu_calls = [ call(), call("Axes", outer_qmenu_call), call("Normalization", outer_qmenu_call), call("Markers", outer_qmenu_call) ] self.assertEqual(expected_qmenu_calls, mocked_qmenu_cls.call_args_list) # 4 actions in Axes submenu self.assertEqual(4, qmenu_call2.addAction.call_count) # 2 actions in Normalization submenu self.assertEqual(2, qmenu_call3.addAction.call_count) # 3 actions in Markers submenu self.assertEqual(3, qmenu_call4.addAction.call_count)
def test_toggle_normalisation_applies_to_all_images_if_one_colorbar(self): fig = pcolormesh([self.ws, self.ws]) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) # there should be 3 axes, 2 colorplots and 1 colorbar self.assertEqual(3, len(fig.axes)) fig.axes[0].tracked_workspaces.values() self.assertTrue(fig.axes[0].tracked_workspaces['ws'][0].is_normalized) self.assertTrue(fig.axes[1].tracked_workspaces['ws'][0].is_normalized) fig_interactor._toggle_normalization(fig.axes[0]) self.assertFalse(fig.axes[0].tracked_workspaces['ws'][0].is_normalized) self.assertFalse(fig.axes[1].tracked_workspaces['ws'][0].is_normalized)
def test_correct_yunit_label_when_overplotting_after_normaliztion_toggle( self): fig = plot([self.ws], spectrum_nums=[1], errors=True, plot_kwargs={'distribution': True}) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) ax = fig.axes[0] fig_interactor._toggle_normalization(ax) self.assertEqual("Counts ($\AA$)$^{-1}$", ax.get_ylabel()) plot([self.ws1], spectrum_nums=[1], errors=True, overplot=True, fig=fig) self.assertEqual("Counts ($\AA$)$^{-1}$", ax.get_ylabel())
def _test_toggle_normalization(self, errorbars_on, plot_kwargs): fig = plot([self.ws], spectrum_nums=[1], errors=errorbars_on, plot_kwargs=plot_kwargs) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) # Earlier versions of matplotlib do not store the data assciated with a # line with high precision and hence we need to set a lower tolerance # when making comparisons of this data if matplotlib.__version__ < "2": decimal_tol = 1 else: decimal_tol = 7 ax = fig.axes[0] fig_interactor._toggle_normalization(ax) assert_almost_equal(ax.lines[0].get_xdata(), [15, 25]) assert_almost_equal(ax.lines[0].get_ydata(), [0.2, 0.3], decimal=decimal_tol) self.assertEqual("Counts ($\\AA$)$^{-1}$", ax.get_ylabel()) fig_interactor._toggle_normalization(ax) assert_almost_equal(ax.lines[0].get_xdata(), [15, 25]) assert_almost_equal(ax.lines[0].get_ydata(), [2, 3], decimal=decimal_tol) self.assertEqual("Counts", ax.get_ylabel())
def test_right_click_gives_context_menu_for_plot_without_fit_enabled(self, mocked_figure_type, mocked_qmenu_cls): fig_manager = self._create_mock_fig_manager_to_accept_right_click() fig_manager.fit_browser.tool = None interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mocked_figure_type.return_value = FigureType.Line # Expect a call to QMenu() for the outer menu followed by a call with the first # as its parent to generate the Axes menu. qmenu_call1 = MagicMock() qmenu_call2 = MagicMock() mocked_qmenu_cls.side_effect = [qmenu_call1, qmenu_call2] with patch('workbench.plotting.figureinteraction.QActionGroup', autospec=True): interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, qmenu_call1.addSeparator.call_count) self.assertEqual(0, qmenu_call1.addAction.call_count) expected_qmenu_calls = [call(), call("Axes", qmenu_call1)] self.assertEqual(expected_qmenu_calls, mocked_qmenu_cls.call_args_list) self.assertEqual(4, qmenu_call2.addAction.call_count)
def test_right_click_gives_context_menu_for_plot_without_fit_enabled( self, mocked_figure_type, mocked_qmenu_cls): fig_manager = self._create_mock_fig_manager_to_accept_right_click() fig_manager.fit_browser.tool = None interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mocked_figure_type.return_value = FigureType.Line # Expect a call to QMenu() for the outer menu followed by a call with the first # as its parent to generate the Axes menu. qmenu_call1 = MagicMock() qmenu_call2 = MagicMock() mocked_qmenu_cls.side_effect = [qmenu_call1, qmenu_call2] with patch('workbench.plotting.figureinteraction.QActionGroup', autospec=True): interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, qmenu_call1.addSeparator.call_count) self.assertEqual(0, qmenu_call1.addAction.call_count) expected_qmenu_calls = [call(), call("Axes", qmenu_call1)] self.assertEqual(expected_qmenu_calls, mocked_qmenu_cls.call_args_list) self.assertEqual(4, qmenu_call2.addAction.call_count)
def test_construction_registers_handler_for_button_press_event(self): fig_manager = MagicMock() fig_manager.canvas = MagicMock() interactor = FigureInteraction(fig_manager) expected_call = [ call('button_press_event', interactor.on_mouse_button_press), call('button_release_event', interactor.on_mouse_button_release), call('draw_event', interactor.draw_callback), call('motion_notify_event', interactor.motion_event), call('resize_event', interactor.mpl_redraw_annotations), call('figure_leave_event', interactor.on_leave), call('axis_leave_event', interactor.on_leave), call('scroll_event', interactor.on_scroll) ] fig_manager.canvas.mpl_connect.assert_has_calls(expected_call) self.assertEqual(len(expected_call), fig_manager.canvas.mpl_connect.call_count)
def test_log_maintained_when_normalisation_toggled(self): ws = CreateWorkspace(DataX=[1, 2, 3, 4, 2, 4, 6, 8], DataY=[2] * 8, NSpec=2, OutputWorkspace="ragged_ws") fig = pcolormesh_from_names([ws]) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) fig_interactor._change_colorbar_axes(LogNorm) fig_interactor._toggle_normalization(fig.axes[0]) self.assertTrue(isinstance(fig.axes[0].images[-1].norm, LogNorm))
def test_scale_on_ragged_workspaces_maintained_when_toggling_normalisation(self): ws = CreateWorkspace(DataX=[1, 2, 3, 4, 2, 4, 6, 8], DataY=[2] * 8, NSpec=2, OutputWorkspace="ragged_ws") fig = pcolormesh_from_names([ws]) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) fig_interactor._toggle_normalization(fig.axes[0]) clim = fig.axes[0].images[0].get_clim() fig_interactor._toggle_normalization(fig.axes[0]) self.assertEqual(clim, fig.axes[0].images[0].get_clim()) self.assertNotEqual((-0.1, 0.1), fig.axes[0].images[0].get_clim())
class FigureManagerWorkbench(FigureManagerBase, QObject): """ Attributes ---------- canvas : `FigureCanvas` The FigureCanvas instance num : int or str The Figure number toolbar : qt.QToolBar The qt.QToolBar window : qt.QMainWindow The qt.QMainWindow """ def __init__(self, canvas, num): assert QAppThreadCall.is_qapp_thread( ), "FigureManagerWorkbench cannot be created outside of the QApplication thread" QObject.__init__(self) parent, flags = get_window_config() self.window = FigureWindow(canvas, parent=parent, window_flags=flags) self.window.activated.connect(self._window_activated) self.window.closing.connect(canvas.close_event) self.window.closing.connect(self.destroy) self.window.visibility_changed.connect(self.fig_visibility_changed) self.window.setWindowTitle("Figure %d" % num) canvas.figure.set_label("Figure %d" % num) FigureManagerBase.__init__(self, canvas, num) # Give the keyboard focus to the figure instead of the # manager; StrongFocus accepts both tab and click to focus and # will enable the canvas to process event w/o clicking. # ClickFocus only takes the focus is the window has been # clicked # on. http://qt-project.org/doc/qt-4.8/qt.html#FocusPolicy-enum or # http://doc.qt.digia.com/qt/qt.html#FocusPolicy-enum canvas.setFocusPolicy(Qt.StrongFocus) canvas.setFocus() self.window._destroying = False # add text label to status bar self.statusbar_label = QLabel() self.window.statusBar().addWidget(self.statusbar_label) self.plot_options_dialog = None self.toolbar = self._get_toolbar(canvas, self.window) if self.toolbar is not None: self.window.addToolBar(self.toolbar) self.toolbar.message.connect(self.statusbar_label.setText) self.toolbar.sig_grid_toggle_triggered.connect(self.grid_toggle) self.toolbar.sig_toggle_fit_triggered.connect(self.fit_toggle) self.toolbar.sig_toggle_superplot_triggered.connect( self.superplot_toggle) self.toolbar.sig_copy_to_clipboard_triggered.connect( self.copy_to_clipboard) self.toolbar.sig_plot_options_triggered.connect( self.launch_plot_options) self.toolbar.sig_plot_help_triggered.connect(self.launch_plot_help) self.toolbar.sig_generate_plot_script_clipboard_triggered.connect( self.generate_plot_script_clipboard) self.toolbar.sig_generate_plot_script_file_triggered.connect( self.generate_plot_script_file) self.toolbar.sig_home_clicked.connect( self.set_figure_zoom_to_display_all) self.toolbar.sig_waterfall_reverse_order_triggered.connect( self.waterfall_reverse_line_order) self.toolbar.sig_waterfall_offset_amount_triggered.connect( self.launch_waterfall_offset_options) self.toolbar.sig_waterfall_fill_area_triggered.connect( self.launch_waterfall_fill_area_options) 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.setFloatable(False) tbs_height = self.toolbar.sizeHint().height() else: tbs_height = 0 # resize the main window so it will display the canvas with the # requested size: cs = canvas.sizeHint() sbs = self.window.statusBar().sizeHint() self._status_and_tool_height = tbs_height + sbs.height() height = cs.height() + self._status_and_tool_height self.window.resize(cs.width(), height) self.fit_browser = FitPropertyBrowser( canvas, ToolbarStateManager(self.toolbar)) self.fit_browser.closing.connect(self.handle_fit_browser_close) self.window.setCentralWidget(canvas) self.window.addDockWidget(Qt.LeftDockWidgetArea, self.fit_browser) self.superplot = None # Need this line to stop the bug where the dock window snaps back to its original size after resizing. # 0 argument is arbitrary and has no effect on fit widget size # This is a qt bug reported at (https://bugreports.qt.io/browse/QTBUG-65592) if QT_VERSION >= LooseVersion("5.6"): self.window.resizeDocks([self.fit_browser], [1], Qt.Horizontal) self.fit_browser.hide() if matplotlib.is_interactive(): self.window.show() canvas.draw_idle() def notify_axes_change(fig): # This will be called whenever the current axes is changed if self.toolbar is not None: self.toolbar.update() canvas.figure.add_axobserver(notify_axes_change) # Register canvas observers self._fig_interaction = FigureInteraction(self) self._ads_observer = FigureManagerADSObserver(self) self.window.raise_() def full_screen_toggle(self): if self.window.isFullScreen(): self.window.showNormal() else: self.window.showFullScreen() def _window_activated(self): GlobalFigureManager.set_active(self) def _get_toolbar(self, canvas, parent): return WorkbenchNavigationToolbar(canvas, parent, False) def resize(self, width, height): """Set the canvas size in pixels""" self.window.resize(width, height + self._status_and_tool_height) def show(self): self.window.show() self.window.activateWindow() self.window.raise_() if self.window.windowState() & Qt.WindowMinimized: # windowState() stores a combination of window state enums # and multiple window states can be valid. On Windows # a window can be both minimized and maximized at the # same time, so we make a check here. For more info see: # http://doc.qt.io/qt-5/qt.html#WindowState-enum if self.window.windowState() & Qt.WindowMaximized: self.window.setWindowState(Qt.WindowMaximized) else: self.window.setWindowState(Qt.WindowNoState) # Hack to ensure the canvas is up to date self.canvas.draw_idle() if self.toolbar: self.toolbar.set_buttons_visibility(self.canvas.figure) def destroy(self, *args): # check for qApp first, as PySide deletes it in its atexit handler if QApplication.instance() is None: return if self.window._destroying: return self.window._destroying = True if self.toolbar: self.toolbar.destroy() self._ads_observer.observeAll(False) self._ads_observer = None # disconnect window events before calling GlobalFigureManager.destroy. window.close is not guaranteed to # delete the object and do this for us. On macOS it was observed that closing the figure window # would produce an extraneous activated event that would add a new figure to the plots list # right after deleted the old one. self.window.disconnect() self._fig_interaction.disconnect() self.window.close() if self.superplot: self.superplot.close() try: GlobalFigureManager.destroy(self.num) except AttributeError: pass # It seems that when the python session is killed, # GlobalFigureManager can get destroyed before the GlobalFigureManager.destroy # line is run, leading to a useless AttributeError. def launch_plot_options(self): self.plot_options_dialog = PlotConfigDialogPresenter( self.canvas.figure, parent=self.window) def launch_plot_options_on_curves_tab(self, axes, curve): self.plot_options_dialog = PlotConfigDialogPresenter( self.canvas.figure, parent=self.window) self.plot_options_dialog.configure_curves_tab(axes, curve) def launch_plot_help(self): PlotHelpPages.show_help_page_for_figure(self.canvas.figure) def copy_to_clipboard(self): """Copy the current figure image to clipboard""" # store the image in a buffer using savefig(), this has the # advantage of applying all the default savefig parameters # such as background color; those would be ignored if you simply # grab the canvas using Qt buf = io.BytesIO() self.canvas.figure.savefig(buf) QApplication.clipboard().setImage(QImage.fromData(buf.getvalue())) buf.close() def grid_toggle(self, on): """Toggle grid lines on/off""" canvas = self.canvas axes = canvas.figure.get_axes() for ax in axes: if type(ax) == Axes: # Colorbar continue elif isinstance(ax, Axes3D): # The grid toggle function for 3D plots doesn't let you choose between major and minor lines. ax.grid(on) else: which = 'both' if hasattr( ax, 'show_minor_gridlines' ) and ax.show_minor_gridlines else 'major' ax.grid(on, which=which) canvas.draw_idle() def fit_toggle(self): """Toggle fit browser and tool on/off""" if self.fit_browser.isVisible(): self.fit_browser.hide() self.toolbar._actions["toggle_fit"].setChecked(False) else: if self.toolbar._actions["toggle_superplot"].isChecked(): self._superplot_hide() self.fit_browser.show() def _superplot_show(self): """Show the superplot""" self.superplot = Superplot(self.canvas, self.window) if not self.superplot.is_valid(): logger.warning("Superplot cannot be opened on data not linked " "to a workspace.") self.superplot = None self.toolbar._actions["toggle_superplot"].setChecked(False) else: self.superplot.show() self.toolbar._actions["toggle_superplot"].setChecked(True) def _superplot_hide(self): """Hide the superplot""" if self.superplot is None: return self.superplot.close() self.superplot = None self.toolbar._actions["toggle_superplot"].setChecked(False) def superplot_toggle(self): """Toggle superplot dockwidgets on/off""" if self.superplot: self._superplot_hide() else: if self.toolbar._actions["toggle_fit"].isChecked(): self.fit_toggle() self._superplot_show() def handle_fit_browser_close(self): """ Respond to a signal that user closed self.fit_browser by clicking the [x] button. """ self.toolbar.trigger_fit_toggle_action() def hold(self): """ Mark this figure as held""" self.toolbar.hold() def get_window_title(self): return self.window.windowTitle() def set_window_title(self, title): self.window.setWindowTitle(title) # We need to add a call to the figure manager here to call # notify methods when a figure is renamed, to update our # plot list. GlobalFigureManager.figure_title_changed(self.num) # For the workbench we also keep the label in sync, this is # to allow getting a handle as plt.figure('Figure Name') self.canvas.figure.set_label(title) def fig_visibility_changed(self): """ Make a notification in the global figure manager that plot visibility was changed. This method is added to this class so that it can be wrapped in a QAppThreadCall. """ GlobalFigureManager.figure_visibility_changed(self.num) def generate_plot_script_clipboard(self): script = generate_script(self.canvas.figure, exclude_headers=True) QApplication.clipboard().setText(script) logger.notice("Plotting script copied to clipboard.") def generate_plot_script_file(self): script = generate_script(self.canvas.figure) filepath = open_a_file_dialog(parent=self.canvas, default_suffix=".py", file_filter="Python Files (*.py)", accept_mode=QFileDialog.AcceptSave, file_mode=QFileDialog.AnyFile) if filepath: try: with open(filepath, 'w') as f: f.write(script) except IOError as io_error: logger.error("Could not write file: {}\n{}" "".format(filepath, io_error)) def set_figure_zoom_to_display_all(self): axes = self.canvas.figure.get_axes() if axes: for ax in axes: # We check for axes type below as a pseudo check for an axes being # a colorbar. this is based on the same check in # FigureManagerADSObserver.deleteHandle. if type(ax) is not Axes: if ax.lines: # Relim causes issues with colour plots, which have no lines. ax.relim() elif isinstance(ax, Axes3D): if hasattr(ax, 'original_data_surface'): ax.collections[0]._vec = copy.deepcopy( ax.original_data_surface) elif hasattr(ax, 'original_data_wireframe'): ax.collections[0].set_segments( copy.deepcopy(ax.original_data_wireframe)) else: ax.view_init() elif ax.images: axesfunctions.update_colorplot_datalimits( ax, ax.images) continue ax.autoscale() self.canvas.draw() def waterfall_reverse_line_order(self): ax = self.canvas.figure.get_axes()[0] x, y = ax.waterfall_x_offset, ax.waterfall_y_offset fills = datafunctions.get_waterfall_fills(ax) ax.update_waterfall(0, 0) errorbar_cap_lines = datafunctions.remove_and_return_errorbar_cap_lines( ax) ax.lines.reverse() for cap in errorbar_cap_lines: ax.add_line(cap) if LooseVersion("3.7") > LooseVersion( matplotlib.__version__) >= LooseVersion("3.2"): for line_fill in fills: if line_fill not in ax.collections: ax.add_collection(line_fill) elif LooseVersion(matplotlib.__version__) < LooseVersion("3.2"): ax.collections += fills else: raise NotImplementedError( "ArtistList will become an immutable tuple in matplotlib 3.7 and thus, " "this code doesn't work anymore.") ax.collections.reverse() ax.update_waterfall(x, y) if ax.get_legend(): ax.make_legend() self.canvas.draw() def launch_waterfall_offset_options(self): WaterfallPlotOffsetDialogPresenter(self.canvas.figure, parent=self.window) def launch_waterfall_fill_area_options(self): WaterfallPlotFillAreaDialogPresenter(self.canvas.figure, parent=self.window) def update_toolbar_waterfall_plot(self, is_waterfall): self.toolbar.set_waterfall_options_enabled(is_waterfall) self.toolbar.set_fit_enabled(not is_waterfall) self.toolbar.set_generate_plot_script_enabled(not is_waterfall) def change_line_collection_colour(self, colour): for col in self.canvas.figure.get_axes()[0].collections: if isinstance(col, LineCollection): col.set_color(colour.name()) self.canvas.draw()
def setUp(self): fig_manager = self._create_mock_fig_manager_to_accept_right_click() fig_manager.fit_browser.tool = None self.interactor = FigureInteraction(fig_manager) self.fig, self.ax = plt.subplots() # type: matplotlib.figure.Figure, MantidAxes
def setUp(self): fig_manager = self._create_mock_fig_manager_to_accept_right_click() fig_manager.fit_browser.tool = None self.interactor = FigureInteraction(fig_manager)
class FigureManagerWorkbench(FigureManagerBase, QObject): """ Attributes ---------- canvas : `FigureCanvas` The FigureCanvas instance num : int or str The Figure number toolbar : qt.QToolBar The qt.QToolBar window : qt.QMainWindow The qt.QMainWindow """ def __init__(self, canvas, num): QObject.__init__(self) FigureManagerBase.__init__(self, canvas, num) # Patch show/destroy to be thread aware self._destroy_orig = self.destroy self.destroy = QAppThreadCall(self._destroy_orig) self._show_orig = self.show self.show = QAppThreadCall(self._show_orig) self._window_activated_orig = self._window_activated self._window_activated = QAppThreadCall(self._window_activated_orig) self.set_window_title_orig = self.set_window_title self.set_window_title = QAppThreadCall(self.set_window_title_orig) self.fig_visibility_changed_orig = self.fig_visibility_changed self.fig_visibility_changed = QAppThreadCall( self.fig_visibility_changed_orig) self.window = FigureWindow(canvas) self.window.activated.connect(self._window_activated) self.window.closing.connect(canvas.close_event) self.window.closing.connect(self.destroy) self.window.visibility_changed.connect(self.fig_visibility_changed) self.window.setWindowTitle("Figure %d" % num) canvas.figure.set_label("Figure %d" % num) # Give the keyboard focus to the figure instead of the # manager; StrongFocus accepts both tab and click to focus and # will enable the canvas to process event w/o clicking. # ClickFocus only takes the focus is the window has been # clicked # on. http://qt-project.org/doc/qt-4.8/qt.html#FocusPolicy-enum or # http://doc.qt.digia.com/qt/qt.html#FocusPolicy-enum canvas.setFocusPolicy(Qt.StrongFocus) canvas.setFocus() self.window._destroying = False # add text label to status bar self.statusbar_label = QLabel() self.window.statusBar().addWidget(self.statusbar_label) self.plot_options_dialog = None self.toolbar = self._get_toolbar(canvas, self.window) if self.toolbar is not None: self.window.addToolBar(self.toolbar) self.toolbar.message.connect(self.statusbar_label.setText) self.toolbar.sig_grid_toggle_triggered.connect(self.grid_toggle) self.toolbar.sig_toggle_fit_triggered.connect(self.fit_toggle) self.toolbar.sig_plot_options_triggered.connect( self.launch_plot_options) self.toolbar.sig_generate_plot_script_clipboard_triggered.connect( self.generate_plot_script_clipboard) self.toolbar.sig_generate_plot_script_file_triggered.connect( self.generate_plot_script_file) self.toolbar.sig_home_clicked.connect( self.set_figure_zoom_to_display_all) self.toolbar.sig_waterfall_reverse_order_triggered.connect( self.waterfall_reverse_line_order) self.toolbar.sig_waterfall_offset_amount_triggered.connect( self.launch_waterfall_offset_options) self.toolbar.sig_waterfall_fill_area_triggered.connect( self.launch_waterfall_fill_area_options) self.toolbar.sig_waterfall_conversion.connect( self.update_toolbar_waterfall_plot) self.toolbar.setFloatable(False) tbs_height = self.toolbar.sizeHint().height() else: tbs_height = 0 # resize the main window so it will display the canvas with the # requested size: cs = canvas.sizeHint() sbs = self.window.statusBar().sizeHint() self._status_and_tool_height = tbs_height + sbs.height() height = cs.height() + self._status_and_tool_height self.window.resize(cs.width(), height) self.fit_browser = FitPropertyBrowser( canvas, ToolbarStateManager(self.toolbar)) self.fit_browser.closing.connect(self.handle_fit_browser_close) self.window.setCentralWidget(canvas) self.window.addDockWidget(Qt.LeftDockWidgetArea, self.fit_browser) self.fit_browser.hide() if matplotlib.is_interactive(): self.window.show() canvas.draw_idle() def notify_axes_change(fig): # This will be called whenever the current axes is changed if self.toolbar is not None: self.toolbar.update() canvas.figure.add_axobserver(notify_axes_change) # Register canvas observers self._fig_interaction = FigureInteraction(self) self._ads_observer = FigureManagerADSObserver(self) self.window.raise_() def full_screen_toggle(self): if self.window.isFullScreen(): self.window.showNormal() else: self.window.showFullScreen() def _window_activated(self): Gcf.set_active(self) def _get_toolbar(self, canvas, parent): return WorkbenchNavigationToolbar(canvas, parent, False) def resize(self, width, height): """Set the canvas size in pixels""" self.window.resize(width, height + self._status_and_tool_height) def show(self): self.window.show() self.window.activateWindow() self.window.raise_() if self.window.windowState() & Qt.WindowMinimized: # windowState() stores a combination of window state enums # and multiple window states can be valid. On Windows # a window can be both minimized and maximized at the # same time, so we make a check here. For more info see: # http://doc.qt.io/qt-5/qt.html#WindowState-enum if self.window.windowState() & Qt.WindowMaximized: self.window.setWindowState(Qt.WindowMaximized) else: self.window.setWindowState(Qt.WindowNoState) # Hack to ensure the canvas is up to date self.canvas.draw_idle() if figure_type(self.canvas.figure) not in [FigureType.Line, FigureType.Errorbar] \ or self.toolbar is not None and len(self.canvas.figure.get_axes()) > 1: self._set_fit_enabled(False) # For plot-to-script button to show, we must have a MantidAxes with lines in it # Plot-to-script currently doesn't work with waterfall plots so the button is hidden for that plot type. if not any((isinstance(ax, MantidAxes) and curve_in_ax(ax)) for ax in self.canvas.figure.get_axes() ) or self.canvas.figure.get_axes()[0].is_waterfall(): self.toolbar.set_generate_plot_script_enabled(False) # Only show options specific to waterfall plots if the axes is a MantidAxes and is a waterfall plot. if not isinstance(self.canvas.figure.get_axes()[0], MantidAxes) or not self.canvas.figure.get_axes( )[0].is_waterfall(): self.toolbar.set_waterfall_options_enabled(False) def destroy(self, *args): # check for qApp first, as PySide deletes it in its atexit handler if QApplication.instance() is None: return if self.window._destroying: return self.window._destroying = True if self.toolbar: self.toolbar.destroy() self._ads_observer.observeAll(False) del self._ads_observer self._fig_interaction.disconnect() self.window.close() try: Gcf.destroy(self.num) except AttributeError: pass # It seems that when the python session is killed, # Gcf can get destroyed before the Gcf.destroy # line is run, leading to a useless AttributeError. def launch_plot_options(self): self.plot_options_dialog = PlotConfigDialogPresenter( self.canvas.figure, parent=self.window) def grid_toggle(self): """Toggle grid lines on/off""" canvas = self.canvas axes = canvas.figure.get_axes() for ax in axes: ax.grid() canvas.draw_idle() def fit_toggle(self): """Toggle fit browser and tool on/off""" if self.fit_browser.isVisible(): self.fit_browser.hide() else: self.fit_browser.show() def handle_fit_browser_close(self): """ Respond to a signal that user closed self.fit_browser by clicking the [x] button. """ self.toolbar.trigger_fit_toggle_action() def hold(self): """ Mark this figure as held""" self.toolbar.hold() def get_window_title(self): return text_type(self.window.windowTitle()) def set_window_title(self, title): self.window.setWindowTitle(title) # We need to add a call to the figure manager here to call # notify methods when a figure is renamed, to update our # plot list. Gcf.figure_title_changed(self.num) # For the workbench we also keep the label in sync, this is # to allow getting a handle as plt.figure('Figure Name') self.canvas.figure.set_label(title) def fig_visibility_changed(self): """ Make a notification in the global figure manager that plot visibility was changed. This method is added to this class so that it can be wrapped in a QAppThreadCall. """ Gcf.figure_visibility_changed(self.num) def _set_fit_enabled(self, on): action = self.toolbar._actions['toggle_fit'] action.setEnabled(on) action.setVisible(on) def generate_plot_script_clipboard(self): script = generate_script(self.canvas.figure, exclude_headers=True) QApplication.clipboard().setText(script) logger.notice("Plotting script copied to clipboard.") def generate_plot_script_file(self): script = generate_script(self.canvas.figure) filepath = open_a_file_dialog(parent=self.canvas, default_suffix=".py", file_filter="Python Files (*.py)", accept_mode=QFileDialog.AcceptSave, file_mode=QFileDialog.AnyFile) if filepath: try: with open(filepath, 'w') as f: f.write(script) except IOError as io_error: logger.error("Could not write file: {}\n{}" "".format(filepath, io_error)) def set_figure_zoom_to_display_all(self): axes = self.canvas.figure.get_axes() if axes: for ax in axes: # We check for axes type below as a pseudo check for an axes being # a colorbar. this is based on the same check in # FigureManagerADSObserver.deleteHandle. if type(ax) is not Axes: if ax.lines: # Relim causes issues with colour plots, which have no lines. ax.relim() ax.autoscale() self.canvas.draw() def waterfall_reverse_line_order(self): ax = self.canvas.figure.get_axes()[0] x, y = ax.waterfall_x_offset, ax.waterfall_y_offset fills = datafunctions.get_waterfall_fills(ax) ax.update_waterfall(0, 0) errorbar_cap_lines = datafunctions.remove_and_return_errorbar_cap_lines( ax) ax.lines.reverse() ax.lines += errorbar_cap_lines ax.collections += fills ax.collections.reverse() ax.update_waterfall(x, y) if ax.get_legend(): ax.make_legend() def launch_waterfall_offset_options(self): WaterfallPlotOffsetDialogPresenter(self.canvas.figure, parent=self.window) def launch_waterfall_fill_area_options(self): WaterfallPlotFillAreaDialogPresenter(self.canvas.figure, parent=self.window) def update_toolbar_waterfall_plot(self, is_waterfall): self.toolbar.set_waterfall_options_enabled(is_waterfall) self.toolbar.set_generate_plot_script_enabled(not is_waterfall)
class FigureInteractionTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.ws = CreateWorkspace( DataX=np.array([10, 20, 30], dtype=np.float64), DataY=np.array([2, 3], dtype=np.float64), DataE=np.array([0.02, 0.02], dtype=np.float64), Distribution=False, UnitX='Wavelength', YUnitLabel='Counts', OutputWorkspace='ws') cls.ws1 = CreateWorkspace( DataX=np.array([11, 21, 31], dtype=np.float64), DataY=np.array([3, 4], dtype=np.float64), DataE=np.array([0.03, 0.03], dtype=np.float64), Distribution=False, UnitX='Wavelength', YUnitLabel='Counts', OutputWorkspace='ws1') # initialises the QApplication super(cls, FigureInteractionTest).setUpClass() @classmethod def tearDownClass(cls): cls.ws.delete() cls.ws1.delete() def setUp(self): fig_manager = self._create_mock_fig_manager_to_accept_right_click() fig_manager.fit_browser.tool = None self.interactor = FigureInteraction(fig_manager) self.fig, self.ax = plt.subplots() # type: matplotlib.figure.Figure, MantidAxes def tearDown(self): plt.close('all') del self.fig del self.ax del self.interactor # Success tests def test_construction_registers_handler_for_button_press_event(self): fig_manager = MagicMock() fig_manager.canvas = MagicMock() interactor = FigureInteraction(fig_manager) expected_call = [ call('button_press_event', interactor.on_mouse_button_press), call('button_release_event', interactor.on_mouse_button_release), call('draw_event', interactor.draw_callback), call('motion_notify_event', interactor.motion_event), call('resize_event', interactor.mpl_redraw_annotations), call('figure_leave_event', interactor.on_leave), call('axis_leave_event', interactor.on_leave), call('scroll_event', interactor.on_scroll) ] fig_manager.canvas.mpl_connect.assert_has_calls(expected_call) self.assertEqual(len(expected_call), fig_manager.canvas.mpl_connect.call_count) def test_disconnect_called_for_each_registered_handler(self): fig_manager = MagicMock() canvas = MagicMock() fig_manager.canvas = canvas interactor = FigureInteraction(fig_manager) interactor.disconnect() self.assertEqual(interactor.nevents, canvas.mpl_disconnect.call_count) @patch('workbench.plotting.figureinteraction.QMenu', autospec=True) @patch('workbench.plotting.figureinteraction.figure_type', autospec=True) def test_right_click_gives_no_context_menu_for_empty_figure(self, mocked_figure_type, mocked_qmenu): fig_manager = self._create_mock_fig_manager_to_accept_right_click() interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mocked_figure_type.return_value = FigureType.Empty with patch.object(interactor.toolbar_manager, 'is_tool_active', lambda: False): interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, mocked_qmenu.call_count) @patch('workbench.plotting.figureinteraction.QMenu', autospec=True) @patch('workbench.plotting.figureinteraction.figure_type', autospec=True) def test_right_click_gives_context_menu_for_color_plot(self, mocked_figure_type, mocked_qmenu): fig_manager = self._create_mock_fig_manager_to_accept_right_click() interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mocked_figure_type.return_value = FigureType.Image # Expect a call to QMenu() for the outer menu followed by three more calls # for the Axes, Normalization and Colorbar menus qmenu_call1 = MagicMock() qmenu_call2 = MagicMock() qmenu_call3 = MagicMock() qmenu_call4 = MagicMock() mocked_qmenu.side_effect = [qmenu_call1, qmenu_call2, qmenu_call3, qmenu_call4] with patch('workbench.plotting.figureinteraction.QActionGroup', autospec=True): with patch.object(interactor.toolbar_manager, 'is_tool_active', lambda: False): interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, qmenu_call1.addAction.call_count) expected_qmenu_calls = [call(), call("Axes", qmenu_call1), call("Normalization", qmenu_call1), call("Color bar", qmenu_call1)] self.assertEqual(expected_qmenu_calls, mocked_qmenu.call_args_list) # 4 actions in Axes submenu self.assertEqual(4, qmenu_call2.addAction.call_count) # 2 actions in Normalization submenu self.assertEqual(2, qmenu_call3.addAction.call_count) # 2 actions in Colorbar submenu self.assertEqual(2, qmenu_call4.addAction.call_count) @patch('workbench.plotting.figureinteraction.QMenu', autospec=True) @patch('workbench.plotting.figureinteraction.figure_type', autospec=True) def test_right_click_gives_context_menu_for_plot_without_fit_enabled(self, mocked_figure_type, mocked_qmenu_cls): fig_manager = self._create_mock_fig_manager_to_accept_right_click() fig_manager.fit_browser.tool = None interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mouse_event.inaxes.get_xlim.return_value = (1, 2) mouse_event.inaxes.get_ylim.return_value = (1, 2) mouse_event.inaxes.lines = [] mocked_figure_type.return_value = FigureType.Line # Expect a call to QMenu() for the outer menu followed by two more calls # for the Axes and Normalization menus qmenu_call1 = MagicMock() qmenu_call2 = MagicMock() qmenu_call3 = MagicMock() qmenu_call4 = MagicMock() mocked_qmenu_cls.side_effect = [qmenu_call1, qmenu_call2, qmenu_call3, qmenu_call4] with patch('workbench.plotting.figureinteraction.QActionGroup', autospec=True): with patch.object(interactor.toolbar_manager, 'is_tool_active', lambda: False): with patch.object(interactor, 'add_error_bars_menu', MagicMock()): interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, qmenu_call1.addSeparator.call_count) self.assertEqual(0, qmenu_call1.addAction.call_count) expected_qmenu_calls = [call(), call("Axes", qmenu_call1), call("Normalization", qmenu_call1), call("Markers", qmenu_call1)] self.assertEqual(expected_qmenu_calls, mocked_qmenu_cls.call_args_list) # 4 actions in Axes submenu self.assertEqual(4, qmenu_call2.addAction.call_count) # 2 actions in Normalization submenu self.assertEqual(2, qmenu_call3.addAction.call_count) # 3 actions in Markers submenu self.assertEqual(3, qmenu_call4.addAction.call_count) def test_toggle_normalization_no_errorbars(self): self._test_toggle_normalization(errorbars_on=False, plot_kwargs={'distribution': True}) def test_toggle_normalization_with_errorbars(self): self._test_toggle_normalization(errorbars_on=True, plot_kwargs={'distribution': True}) def test_correct_yunit_label_when_overplotting_after_normalization_toggle(self): # The earlier version of Matplotlib on RHEL throws an error when performing the second # plot in this test, if the lines have errorbars. The error occurred when it attempted # to draw an interactive legend. Plotting without errors still fulfills the purpose of this # test, so turn them off for old Matplotlib versions. errors = True if int(matplotlib.__version__[0]) < 2: errors = False fig = plot([self.ws], spectrum_nums=[1], errors=errors, plot_kwargs={'distribution': True}) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) ax = fig.axes[0] fig_interactor._toggle_normalization(ax) self.assertEqual(r"Counts ($\AA$)$^{-1}$", ax.get_ylabel()) plot([self.ws1], spectrum_nums=[1], errors=errors, overplot=True, fig=fig) self.assertEqual(r"Counts ($\AA$)$^{-1}$", ax.get_ylabel()) def test_normalization_toggle_with_no_autoscale_on_update_no_errors(self): self._test_toggle_normalization(errorbars_on=False, plot_kwargs={'distribution': True, 'autoscale_on_update': False}) def test_normalization_toggle_with_no_autoscale_on_update_with_errors(self): self._test_toggle_normalization(errorbars_on=True, plot_kwargs={'distribution': True, 'autoscale_on_update': False}) def test_add_error_bars_menu(self): self.ax.errorbar([0, 15000], [0, 14000], yerr=[10, 10000], label='MyLabel 2') main_menu = QMenu() self.interactor.add_error_bars_menu(main_menu, self.ax) # Check the expected sub-menu with buttons is added added_menu = main_menu.children()[1] self.assertTrue( any(FigureInteraction.SHOW_ERROR_BARS_BUTTON_TEXT == child.text() for child in added_menu.children())) self.assertTrue( any(FigureInteraction.HIDE_ERROR_BARS_BUTTON_TEXT == child.text() for child in added_menu.children())) def test_context_menu_not_added_for_scripted_plot_without_errors(self): self.ax.plot([0, 15000], [0, 15000], label='MyLabel') self.ax.plot([0, 15000], [0, 14000], label='MyLabel 2') main_menu = QMenu() # QMenu always seems to have 1 child when empty, # but just making sure the count as expected at this point in the test self.assertEqual(1, len(main_menu.children())) # plot above doesn't have errors, nor is a MantidAxes # so no context menu will be added self.interactor.add_error_bars_menu(main_menu, self.ax) # number of children should remain unchanged self.assertEqual(1, len(main_menu.children())) def test_scripted_plot_line_without_label_handled_properly(self): # having the special nolabel is usually present on lines with errors, # but sometimes can be present on lines without errors, this test covers that case self.ax.plot([0, 15000], [0, 15000], label='_nolegend_') self.ax.plot([0, 15000], [0, 15000], label='_nolegend_') main_menu = QMenu() # QMenu always seems to have 1 child when empty, # but just making sure the count as expected at this point in the test self.assertEqual(1, len(main_menu.children())) # plot above doesn't have errors, nor is a MantidAxes # so no context menu will be added for error bars self.interactor.add_error_bars_menu(main_menu, self.ax) # number of children should remain unchanged self.assertEqual(1, len(main_menu.children())) def test_context_menu_added_for_scripted_plot_with_errors(self): self.ax.plot([0, 15000], [0, 15000], label='MyLabel') self.ax.errorbar([0, 15000], [0, 14000], yerr=[10, 10000], label='MyLabel 2') main_menu = QMenu() # QMenu always seems to have 1 child when empty, # but just making sure the count as expected at this point in the test self.assertEqual(1, len(main_menu.children())) # plot above doesn't have errors, nor is a MantidAxes # so no context menu will be added self.interactor.add_error_bars_menu(main_menu, self.ax) added_menu = main_menu.children()[1] # actions should have been added now, which for this case are only `Show all` and `Hide all` self.assertTrue( any(FigureInteraction.SHOW_ERROR_BARS_BUTTON_TEXT == child.text() for child in added_menu.children())) self.assertTrue( any(FigureInteraction.HIDE_ERROR_BARS_BUTTON_TEXT == child.text() for child in added_menu.children())) def test_context_menu_includes_plot_type_if_plot_has_multiple_lines(self): fig, self.ax = plt.subplots(subplot_kw={'projection': 'mantid'}) self.ax.plot([0, 1], [0, 1]) self.ax.plot([0, 1], [0, 1]) main_menu = QMenu() # QMenu always seems to have 1 child when empty, # but just making sure the count as expected at this point in the test self.assertEqual(1, len(main_menu.children())) self.interactor._add_plot_type_option_menu(main_menu, self.ax) added_menu = main_menu.children()[1] self.assertEqual(added_menu.children()[0].text(), "Plot Type") def test_context_menu_does_not_include_plot_type_if_plot_has_one_line(self): fig, self.ax = plt.subplots(subplot_kw={'projection': 'mantid'}) self.ax.errorbar([0, 1], [0, 1], capsize=1) main_menu = QMenu() # QMenu always seems to have 1 child when empty, # but just making sure the count as expected at this point in the test self.assertEqual(1, len(main_menu.children())) self.interactor._add_plot_type_option_menu(main_menu, self.ax) # Number of children should remain unchanged self.assertEqual(1, len(main_menu.children())) def test_scripted_plot_show_and_hide_all(self): self.ax.plot([0, 15000], [0, 15000], label='MyLabel') self.ax.errorbar([0, 15000], [0, 14000], yerr=[10, 10000], label='MyLabel 2') anonymous_menu = QMenu() # this initialises some of the class internals self.interactor.add_error_bars_menu(anonymous_menu, self.ax) self.assertTrue(self.ax.containers[0][2][0].get_visible()) self.interactor.errors_manager.toggle_all_errors(self.ax, make_visible=False) self.assertFalse(self.ax.containers[0][2][0].get_visible()) # make the menu again, this updates the internal state of the errors manager # and is what actually happens when the user opens the menu again self.interactor.add_error_bars_menu(anonymous_menu, self.ax) self.interactor.errors_manager.toggle_all_errors(self.ax, make_visible=True) self.assertTrue(self.ax.containers[0][2][0].get_visible()) def test_no_normalisation_options_on_non_workspace_plot(self): fig, self.ax = plt.subplots(subplot_kw={'projection': 'mantid'}) self.ax.plot([1, 2], [1, 2], label="myLabel") anonymous_menu = QMenu() self.assertEqual(None, self.interactor._add_normalization_option_menu(anonymous_menu, self.ax)) # Failure tests def test_construction_with_non_qt_canvas_raises_exception(self): class NotQtCanvas(object): pass class FigureManager(object): def __init__(self): self.canvas = NotQtCanvas() self.assertRaises(RuntimeError, FigureInteraction, FigureManager()) def test_context_menu_change_axis_scale_is_axis_aware(self): fig = plot([self.ws, self.ws1], spectrum_nums=[1, 1], tiled=True) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) scale_types = ("log", "log") ax = fig.axes[0] ax1 = fig.axes[1] current_scale_types = (ax.get_xscale(), ax.get_yscale()) current_scale_types1 = (ax1.get_xscale(), ax1.get_yscale()) self.assertEqual(current_scale_types, current_scale_types1) fig_interactor._quick_change_axes(scale_types, ax) current_scale_types2 = (ax.get_xscale(), ax.get_yscale()) self.assertNotEqual(current_scale_types2, current_scale_types1) def test_scale_on_ragged_workspaces_maintained_when_toggling_normalisation(self): ws = CreateWorkspace(DataX=[1, 2, 3, 4, 2, 4, 6, 8], DataY=[2] * 8, NSpec=2, OutputWorkspace="ragged_ws") fig = pcolormesh_from_names([ws]) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) fig_interactor._toggle_normalization(fig.axes[0]) clim = fig.axes[0].images[0].get_clim() fig_interactor._toggle_normalization(fig.axes[0]) self.assertEqual(clim, fig.axes[0].images[0].get_clim()) self.assertNotEqual((-0.1, 0.1), fig.axes[0].images[0].get_clim()) def test_log_maintained_when_normalisation_toggled(self): ws = CreateWorkspace(DataX=[1, 2, 3, 4, 2, 4, 6, 8], DataY=[2] * 8, NSpec=2, OutputWorkspace="ragged_ws") fig = pcolormesh_from_names([ws]) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) fig_interactor._change_colorbar_axes(LogNorm) fig_interactor._toggle_normalization(fig.axes[0]) self.assertTrue(isinstance(fig.axes[0].images[-1].norm, LogNorm)) @patch('workbench.plotting.figureinteraction.QMenu', autospec=True) @patch('workbench.plotting.figureinteraction.figure_type', autospec=True) def test_right_click_gives_marker_menu_when_hovering_over_one(self, mocked_figure_type, mocked_qmenu_cls): mouse_event = self._create_mock_right_click() mouse_event.inaxes.get_xlim.return_value = (1, 2) mouse_event.inaxes.get_ylim.return_value = (1, 2) mocked_figure_type.return_value = FigureType.Line marker1 = MagicMock() marker2 = MagicMock() marker3 = MagicMock() self.interactor.markers = [marker1, marker2, marker3] for marker in self.interactor.markers: marker.is_above.return_value = True # Expect a call to QMenu() for the outer menu followed by two more calls # for the Axes and Normalization menus qmenu_call1 = MagicMock() qmenu_call2 = MagicMock() qmenu_call3 = MagicMock() qmenu_call4 = MagicMock() mocked_qmenu_cls.side_effect = [qmenu_call1, qmenu_call2, qmenu_call3, qmenu_call4] with patch('workbench.plotting.figureinteraction.QActionGroup', autospec=True): with patch.object(self.interactor.toolbar_manager, 'is_tool_active', lambda: False): with patch.object(self.interactor, 'add_error_bars_menu', MagicMock()): self.interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, qmenu_call1.addSeparator.call_count) self.assertEqual(0, qmenu_call1.addAction.call_count) expected_qmenu_calls = [call(), call(marker1.name, qmenu_call1), call(marker2.name, qmenu_call1), call(marker3.name, qmenu_call1)] self.assertEqual(expected_qmenu_calls, mocked_qmenu_cls.call_args_list) # 2 Actions in marker menu self.assertEqual(2, qmenu_call2.addAction.call_count) self.assertEqual(2, qmenu_call3.addAction.call_count) self.assertEqual(2, qmenu_call4.addAction.call_count) @patch('workbench.plotting.figureinteraction.SingleMarker') def test_adding_horizontal_marker_adds_correct_marker(self, mock_marker): y0, y1 = 0, 1 data = MagicMock() axis = MagicMock() self.interactor._add_horizontal_marker(data, y0, y1, axis) expected_call = call(self.interactor.canvas, '#2ca02c', data, y0, y1, name='marker 0', marker_type='YSingle', line_style='dashed', axis=axis) self.assertEqual(1, mock_marker.call_count) mock_marker.assert_has_calls([expected_call]) @patch('workbench.plotting.figureinteraction.SingleMarker') def test_adding_vertical_marker_adds_correct_marker(self, mock_marker): x0, x1 = 0, 1 data = MagicMock() axis = MagicMock() self.interactor._add_vertical_marker(data, x0, x1, axis) expected_call = call(self.interactor.canvas, '#2ca02c', data, x0, x1, name='marker 0', marker_type='XSingle', line_style='dashed', axis=axis) self.assertEqual(1, mock_marker.call_count) mock_marker.assert_has_calls([expected_call]) def test_delete_marker_does_not_delete_markers_if_not_present(self): marker = MagicMock() self.interactor.markers = [] self.interactor._delete_marker(marker) self.assertEqual(0, self.interactor.canvas.draw.call_count) self.assertEqual(0, marker.marker.remove.call_count) self.assertEqual(0, marker.remove_all_annotations.call_count) def test_delete_marker_preforms_correct_cleanup(self): marker = MagicMock() self.interactor.markers = [marker] self.interactor._delete_marker(marker) self.assertEqual(1, marker.marker.remove.call_count) self.assertEqual(1, marker.remove_all_annotations.call_count) self.assertEqual(1, self.interactor.canvas.draw.call_count) self.assertNotIn(marker, self.interactor.markers) @patch('workbench.plotting.figureinteraction.SingleMarkerEditor') @patch('workbench.plotting.figureinteraction.QApplication') def test_edit_marker_opens_correct_editor(self, mock_qapp, mock_editor): marker = MagicMock() expected_call = [call(self.interactor.canvas, marker, self.interactor.valid_lines, self.interactor.valid_colors, [])] self.interactor._edit_marker(marker) self.assertEqual(1, mock_qapp.restoreOverrideCursor.call_count) mock_editor.assert_has_calls(expected_call) @patch('workbench.plotting.figureinteraction.GlobalMarkerEditor') def test_global_edit_marker_opens_correct_editor(self, mock_editor): marker = MagicMock() self.interactor.markers = [marker] expected_call = [call(self.interactor.canvas, [marker], self.interactor.valid_lines, self.interactor.valid_colors)] self.interactor._global_edit_markers() mock_editor.assert_has_calls(expected_call) def test_motion_event_returns_if_toolbar_has_active_tools(self): self.interactor.toolbar_manager.is_tool_active = MagicMock(return_value=True) self.interactor._set_hover_cursor = MagicMock() self.interactor.motion_event(MagicMock()) self.assertEqual(0, self.interactor._set_hover_cursor.call_count) def test_motion_event_returns_if_fit_active(self): self.interactor.toolbar_manager.is_fit_active = MagicMock(return_value=True) self.interactor._set_hover_cursor = MagicMock() self.interactor.motion_event(MagicMock()) self.assertEqual(0, self.interactor._set_hover_cursor.call_count) def test_motion_event_changes_cursor_and_draws_canvas_if_any_marker_is_moving(self): markers = [MagicMock(), MagicMock(), MagicMock()] for marker in markers: marker.mouse_move.return_value = True event = MagicMock() event.xdata = 1 event.ydata = 2 self.interactor.markers = markers self.interactor.toolbar_manager.is_tool_active = MagicMock(return_value=False) self.interactor.toolbar_manager.is_fit_active = MagicMock(return_value=False) self.interactor._set_hover_cursor = MagicMock() self.interactor.motion_event(event) self.interactor._set_hover_cursor.assert_has_calls([call(1, 2)]) self.assertEqual(1, self.interactor.canvas.draw.call_count) def test_motion_event_changes_cursor_and_does_not_draw_canvas_if_no_marker_is_moving(self): markers = [MagicMock(), MagicMock(), MagicMock()] for marker in markers: marker.mouse_move.return_value = False event = MagicMock() event.xdata = 1 event.ydata = 2 self.interactor.markers = markers self.interactor.toolbar_manager.is_tool_active = MagicMock(return_value=False) self.interactor.toolbar_manager.is_fit_active = MagicMock(return_value=False) self.interactor._set_hover_cursor = MagicMock() self.interactor.motion_event(event) self.interactor._set_hover_cursor.assert_has_calls([call(1, 2)]) self.assertEqual(0, self.interactor.canvas.draw.call_count) def test_redraw_annotations_removes_and_adds_all_annotations_for_all_markers(self): markers = [MagicMock(), MagicMock(), MagicMock()] call_list = [call.remove_all_annotations(), call.add_all_annotations()] self.interactor.markers = markers self.interactor.redraw_annotations() for marker in markers: marker.assert_has_calls(call_list) def test_mpl_redraw_annotations_does_not_redraw_if_event_does_not_have_a_button_attribute(self): self.interactor.redraw_annotations = MagicMock() event = MagicMock(spec='no_button') event.no_button = MagicMock(spec='no_button') self.interactor.mpl_redraw_annotations(event.no_button) self.assertEqual(0, self.interactor.redraw_annotations.call_count) def test_mpl_redraw_annotations_does_not_redraw_if_event_button_not_pressed(self): self.interactor.redraw_annotations = MagicMock() event = MagicMock() event.button = None self.interactor.mpl_redraw_annotations(event) self.assertEqual(0, self.interactor.redraw_annotations.call_count) def test_mpl_redraw_annotations_redraws_if_button_pressed(self): self.interactor.redraw_annotations = MagicMock() event = MagicMock() self.interactor.mpl_redraw_annotations(event) self.assertEqual(1, self.interactor.redraw_annotations.call_count) def test_toggle_normalisation_on_contour_plot_maintains_contour_line_colour(self): from mantid.plots.legend import convert_color_to_hex ws = CreateWorkspace(DataX=[1, 2, 3, 4, 2, 4, 6, 8], DataY=[2] * 8, NSpec=2, OutputWorkspace="test_ws") fig = plot_contour([ws]) for col in fig.get_axes()[0].collections: col.set_color("#ff9900") mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) fig_interactor._toggle_normalization(fig.axes[0]) self.assertTrue(all(convert_color_to_hex(col.get_color()[0]) == "#ff9900" for col in fig.get_axes()[0].collections)) def test_toggle_normalisation_applies_to_all_images_if_one_colorbar(self): fig = pcolormesh([self.ws, self.ws]) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) # there should be 3 axes, 2 colorplots and 1 colorbar self.assertEqual(3, len(fig.axes)) fig.axes[0].tracked_workspaces.values() self.assertTrue(fig.axes[0].tracked_workspaces['ws'][0].is_normalized) self.assertTrue(fig.axes[1].tracked_workspaces['ws'][0].is_normalized) fig_interactor._toggle_normalization(fig.axes[0]) self.assertFalse(fig.axes[0].tracked_workspaces['ws'][0].is_normalized) self.assertFalse(fig.axes[1].tracked_workspaces['ws'][0].is_normalized) # Private methods def _create_mock_fig_manager_to_accept_right_click(self): fig_manager = MagicMock() canvas = MagicMock() type(canvas).buttond = PropertyMock(return_value={Qt.RightButton: 3}) fig_manager.canvas = canvas return fig_manager def _create_mock_right_click(self): mouse_event = MagicMock(inaxes=MagicMock(spec=MantidAxes, collections = [], creation_args = [{}])) type(mouse_event).button = PropertyMock(return_value=3) return mouse_event def _test_toggle_normalization(self, errorbars_on, plot_kwargs): fig = plot([self.ws], spectrum_nums=[1], errors=errorbars_on, plot_kwargs=plot_kwargs) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) # Earlier versions of matplotlib do not store the data assciated with a # line with high precision and hence we need to set a lower tolerance # when making comparisons of this data if matplotlib.__version__ < "2": decimal_tol = 1 else: decimal_tol = 7 ax = fig.axes[0] fig_interactor._toggle_normalization(ax) assert_almost_equal(ax.lines[0].get_xdata(), [15, 25]) assert_almost_equal(ax.lines[0].get_ydata(), [0.2, 0.3], decimal=decimal_tol) self.assertEqual("Counts ($\\AA$)$^{-1}$", ax.get_ylabel()) fig_interactor._toggle_normalization(ax) assert_almost_equal(ax.lines[0].get_xdata(), [15, 25]) assert_almost_equal(ax.lines[0].get_ydata(), [2, 3], decimal=decimal_tol) self.assertEqual("Counts", ax.get_ylabel())
def __init__(self, canvas, num): QObject.__init__(self) FigureManagerBase.__init__(self, canvas, num) # Patch show/destroy to be thread aware self._destroy_orig = self.destroy self.destroy = QAppThreadCall(self._destroy_orig) self._show_orig = self.show self.show = QAppThreadCall(self._show_orig) self._window_activated_orig = self._window_activated self._window_activated = QAppThreadCall(self._window_activated_orig) self.set_window_title_orig = self.set_window_title self.set_window_title = QAppThreadCall(self.set_window_title_orig) self.fig_visibility_changed_orig = self.fig_visibility_changed self.fig_visibility_changed = QAppThreadCall( self.fig_visibility_changed_orig) self.window = FigureWindow(canvas) self.window.activated.connect(self._window_activated) self.window.closing.connect(canvas.close_event) self.window.closing.connect(self.destroy) self.window.visibility_changed.connect(self.fig_visibility_changed) self.window.setWindowTitle("Figure %d" % num) canvas.figure.set_label("Figure %d" % num) # Give the keyboard focus to the figure instead of the # manager; StrongFocus accepts both tab and click to focus and # will enable the canvas to process event w/o clicking. # ClickFocus only takes the focus is the window has been # clicked # on. http://qt-project.org/doc/qt-4.8/qt.html#FocusPolicy-enum or # http://doc.qt.digia.com/qt/qt.html#FocusPolicy-enum canvas.setFocusPolicy(Qt.StrongFocus) canvas.setFocus() self.window._destroying = False # add text label to status bar self.statusbar_label = QLabel() self.window.statusBar().addWidget(self.statusbar_label) self.plot_options_dialog = None self.toolbar = self._get_toolbar(canvas, self.window) if self.toolbar is not None: self.window.addToolBar(self.toolbar) self.toolbar.message.connect(self.statusbar_label.setText) self.toolbar.sig_grid_toggle_triggered.connect(self.grid_toggle) self.toolbar.sig_toggle_fit_triggered.connect(self.fit_toggle) self.toolbar.sig_plot_options_triggered.connect( self.launch_plot_options) self.toolbar.sig_generate_plot_script_clipboard_triggered.connect( self.generate_plot_script_clipboard) self.toolbar.sig_generate_plot_script_file_triggered.connect( self.generate_plot_script_file) self.toolbar.sig_home_clicked.connect( self.set_figure_zoom_to_display_all) self.toolbar.sig_waterfall_reverse_order_triggered.connect( self.waterfall_reverse_line_order) self.toolbar.sig_waterfall_offset_amount_triggered.connect( self.launch_waterfall_offset_options) self.toolbar.sig_waterfall_fill_area_triggered.connect( self.launch_waterfall_fill_area_options) self.toolbar.sig_waterfall_conversion.connect( self.update_toolbar_waterfall_plot) self.toolbar.setFloatable(False) tbs_height = self.toolbar.sizeHint().height() else: tbs_height = 0 # resize the main window so it will display the canvas with the # requested size: cs = canvas.sizeHint() sbs = self.window.statusBar().sizeHint() self._status_and_tool_height = tbs_height + sbs.height() height = cs.height() + self._status_and_tool_height self.window.resize(cs.width(), height) self.fit_browser = FitPropertyBrowser( canvas, ToolbarStateManager(self.toolbar)) self.fit_browser.closing.connect(self.handle_fit_browser_close) self.window.setCentralWidget(canvas) self.window.addDockWidget(Qt.LeftDockWidgetArea, self.fit_browser) self.fit_browser.hide() if matplotlib.is_interactive(): self.window.show() canvas.draw_idle() def notify_axes_change(fig): # This will be called whenever the current axes is changed if self.toolbar is not None: self.toolbar.update() canvas.figure.add_axobserver(notify_axes_change) # Register canvas observers self._fig_interaction = FigureInteraction(self) self._ads_observer = FigureManagerADSObserver(self) self.window.raise_()
def test_construction_registers_handler_for_button_press_event(self): fig_manager = MagicMock() fig_manager.canvas = MagicMock() interactor = FigureInteraction(fig_manager) fig_manager.canvas.mpl_connect.assert_called_once_with( 'button_press_event', interactor.on_mouse_button_press)
def __init__(self, canvas, num): assert QAppThreadCall.is_qapp_thread( ), "FigureManagerWorkbench cannot be created outside of the QApplication thread" QObject.__init__(self) parent, flags = get_window_config() self.window = FigureWindow(canvas, parent=parent, window_flags=flags) self.window.activated.connect(self._window_activated) self.window.closing.connect(canvas.close_event) self.window.closing.connect(self.destroy) self.window.visibility_changed.connect(self.fig_visibility_changed) self.window.setWindowTitle("Figure %d" % num) canvas.figure.set_label("Figure %d" % num) FigureManagerBase.__init__(self, canvas, num) # Give the keyboard focus to the figure instead of the # manager; StrongFocus accepts both tab and click to focus and # will enable the canvas to process event w/o clicking. # ClickFocus only takes the focus is the window has been # clicked # on. http://qt-project.org/doc/qt-4.8/qt.html#FocusPolicy-enum or # http://doc.qt.digia.com/qt/qt.html#FocusPolicy-enum canvas.setFocusPolicy(Qt.StrongFocus) canvas.setFocus() self.window._destroying = False # add text label to status bar self.statusbar_label = QLabel() self.window.statusBar().addWidget(self.statusbar_label) self.plot_options_dialog = None self.toolbar = self._get_toolbar(canvas, self.window) if self.toolbar is not None: self.window.addToolBar(self.toolbar) self.toolbar.message.connect(self.statusbar_label.setText) self.toolbar.sig_grid_toggle_triggered.connect(self.grid_toggle) self.toolbar.sig_toggle_fit_triggered.connect(self.fit_toggle) self.toolbar.sig_toggle_superplot_triggered.connect( self.superplot_toggle) self.toolbar.sig_copy_to_clipboard_triggered.connect( self.copy_to_clipboard) self.toolbar.sig_plot_options_triggered.connect( self.launch_plot_options) self.toolbar.sig_plot_help_triggered.connect(self.launch_plot_help) self.toolbar.sig_generate_plot_script_clipboard_triggered.connect( self.generate_plot_script_clipboard) self.toolbar.sig_generate_plot_script_file_triggered.connect( self.generate_plot_script_file) self.toolbar.sig_home_clicked.connect( self.set_figure_zoom_to_display_all) self.toolbar.sig_waterfall_reverse_order_triggered.connect( self.waterfall_reverse_line_order) self.toolbar.sig_waterfall_offset_amount_triggered.connect( self.launch_waterfall_offset_options) self.toolbar.sig_waterfall_fill_area_triggered.connect( self.launch_waterfall_fill_area_options) 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.setFloatable(False) tbs_height = self.toolbar.sizeHint().height() else: tbs_height = 0 # resize the main window so it will display the canvas with the # requested size: cs = canvas.sizeHint() sbs = self.window.statusBar().sizeHint() self._status_and_tool_height = tbs_height + sbs.height() height = cs.height() + self._status_and_tool_height self.window.resize(cs.width(), height) self.fit_browser = FitPropertyBrowser( canvas, ToolbarStateManager(self.toolbar)) self.fit_browser.closing.connect(self.handle_fit_browser_close) self.window.setCentralWidget(canvas) self.window.addDockWidget(Qt.LeftDockWidgetArea, self.fit_browser) self.superplot = None # Need this line to stop the bug where the dock window snaps back to its original size after resizing. # 0 argument is arbitrary and has no effect on fit widget size # This is a qt bug reported at (https://bugreports.qt.io/browse/QTBUG-65592) if QT_VERSION >= LooseVersion("5.6"): self.window.resizeDocks([self.fit_browser], [1], Qt.Horizontal) self.fit_browser.hide() if matplotlib.is_interactive(): self.window.show() canvas.draw_idle() def notify_axes_change(fig): # This will be called whenever the current axes is changed if self.toolbar is not None: self.toolbar.update() canvas.figure.add_axobserver(notify_axes_change) # Register canvas observers self._fig_interaction = FigureInteraction(self) self._ads_observer = FigureManagerADSObserver(self) self.window.raise_()
class FigureInteractionTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.ws = CreateWorkspace(DataX=np.array([10, 20, 30], dtype=np.float64), DataY=np.array([2, 3], dtype=np.float64), DataE=np.array([0.02, 0.02], dtype=np.float64), Distribution=False, UnitX='Wavelength', YUnitLabel='Counts', OutputWorkspace='ws') cls.ws1 = CreateWorkspace(DataX=np.array([11, 21, 31], dtype=np.float64), DataY=np.array([3, 4], dtype=np.float64), DataE=np.array([0.03, 0.03], dtype=np.float64), Distribution=False, UnitX='Wavelength', YUnitLabel='Counts', OutputWorkspace='ws1') @classmethod def tearDownClass(cls): cls.ws.delete() cls.ws1.delete() def setUp(self): fig_manager = self._create_mock_fig_manager_to_accept_right_click() fig_manager.fit_browser.tool = None self.interactor = FigureInteraction(fig_manager) # Success tests def test_construction_registers_handler_for_button_press_event(self): fig_manager = MagicMock() fig_manager.canvas = MagicMock() interactor = FigureInteraction(fig_manager) expected_call = [ call('button_press_event', interactor.on_mouse_button_press), call('button_release_event', interactor.on_mouse_button_release), call('draw_event', interactor.draw_callback), call('motion_notify_event', interactor.motion_event), call('resize_event', interactor.mpl_redraw_annotations), call('figure_leave_event', interactor.on_leave), call('axis_leave_event', interactor.on_leave), ] fig_manager.canvas.mpl_connect.assert_has_calls(expected_call) self.assertEqual(len(expected_call), fig_manager.canvas.mpl_connect.call_count) def test_disconnect_called_for_each_registered_handler(self): fig_manager = MagicMock() canvas = MagicMock() fig_manager.canvas = canvas interactor = FigureInteraction(fig_manager) interactor.disconnect() self.assertEqual(interactor.nevents, canvas.mpl_disconnect.call_count) @patch('workbench.plotting.figureinteraction.QMenu', autospec=True) @patch('workbench.plotting.figureinteraction.figure_type', autospec=True) def test_right_click_gives_no_context_menu_for_empty_figure( self, mocked_figure_type, mocked_qmenu): fig_manager = self._create_mock_fig_manager_to_accept_right_click() interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mocked_figure_type.return_value = FigureType.Empty with patch.object(interactor.toolbar_manager, 'is_tool_active', lambda: False): interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, mocked_qmenu.call_count) @patch('workbench.plotting.figureinteraction.QMenu', autospec=True) @patch('workbench.plotting.figureinteraction.figure_type', autospec=True) def test_right_click_gives_no_context_menu_for_color_plot( self, mocked_figure_type, mocked_qmenu): fig_manager = self._create_mock_fig_manager_to_accept_right_click() interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mocked_figure_type.return_value = FigureType.Image with patch.object(interactor.toolbar_manager, 'is_tool_active', lambda: False): interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, mocked_qmenu.call_count) @patch('workbench.plotting.figureinteraction.QMenu', autospec=True) @patch('workbench.plotting.figureinteraction.figure_type', autospec=True) def test_right_click_gives_context_menu_for_plot_without_fit_enabled( self, mocked_figure_type, mocked_qmenu_cls): fig_manager = self._create_mock_fig_manager_to_accept_right_click() fig_manager.fit_browser.tool = None interactor = FigureInteraction(fig_manager) mouse_event = self._create_mock_right_click() mouse_event.inaxes.get_xlim.return_value = (1, 2) mouse_event.inaxes.get_ylim.return_value = (1, 2) mocked_figure_type.return_value = FigureType.Line # Expect a call to QMenu() for the outer menu followed by two more calls # for the Axes and Normalization menus qmenu_call1 = MagicMock() qmenu_call2 = MagicMock() qmenu_call3 = MagicMock() qmenu_call4 = MagicMock() mocked_qmenu_cls.side_effect = [ qmenu_call1, qmenu_call2, qmenu_call3, qmenu_call4 ] with patch('workbench.plotting.figureinteraction.QActionGroup', autospec=True): with patch.object(interactor.toolbar_manager, 'is_tool_active', lambda: False): with patch.object(interactor.errors_manager, 'add_error_bars_menu', MagicMock()): interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, qmenu_call1.addSeparator.call_count) self.assertEqual(0, qmenu_call1.addAction.call_count) expected_qmenu_calls = [ call(), call("Axes", qmenu_call1), call("Normalization", qmenu_call1), call("Markers", qmenu_call1) ] self.assertEqual(expected_qmenu_calls, mocked_qmenu_cls.call_args_list) # 4 actions in Axes submenu self.assertEqual(4, qmenu_call2.addAction.call_count) # 2 actions in Normalization submenu self.assertEqual(2, qmenu_call3.addAction.call_count) # 3 actions in Markers submenu self.assertEqual(3, qmenu_call4.addAction.call_count) def test_toggle_normalization_no_errorbars(self): self._test_toggle_normalization(errorbars_on=False, plot_kwargs={'distribution': True}) def test_toggle_normalization_with_errorbars(self): self._test_toggle_normalization(errorbars_on=True, plot_kwargs={'distribution': True}) def test_correct_yunit_label_when_overplotting_after_normaliztion_toggle( self): fig = plot([self.ws], spectrum_nums=[1], errors=True, plot_kwargs={'distribution': True}) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) ax = fig.axes[0] fig_interactor._toggle_normalization(ax) self.assertEqual("Counts ($\AA$)$^{-1}$", ax.get_ylabel()) plot([self.ws1], spectrum_nums=[1], errors=True, overplot=True, fig=fig) self.assertEqual("Counts ($\AA$)$^{-1}$", ax.get_ylabel()) def test_normalization_toggle_with_no_autoscale_on_update_no_errors(self): self._test_toggle_normalization(errorbars_on=False, plot_kwargs={ 'distribution': True, 'autoscale_on_update': False }) def test_normalization_toggle_with_no_autoscale_on_update_with_errors( self): self._test_toggle_normalization(errorbars_on=True, plot_kwargs={ 'distribution': True, 'autoscale_on_update': False }) # Failure tests def test_construction_with_non_qt_canvas_raises_exception(self): class NotQtCanvas(object): pass class FigureManager(object): def __init__(self): self.canvas = NotQtCanvas() self.assertRaises(RuntimeError, FigureInteraction, FigureManager()) def test_context_menu_change_axis_scale_is_axis_aware(self): fig = plot([self.ws, self.ws1], spectrum_nums=[1, 1], tiled=True) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) scale_types = ("log", "log") ax = fig.axes[0] ax1 = fig.axes[1] current_scale_types = (ax.get_xscale(), ax.get_yscale()) current_scale_types1 = (ax1.get_xscale(), ax1.get_yscale()) self.assertEqual(current_scale_types, current_scale_types1) fig_interactor._quick_change_axes(scale_types, ax) current_scale_types2 = (ax.get_xscale(), ax.get_yscale()) self.assertNotEqual(current_scale_types2, current_scale_types1) # Private methods def _create_mock_fig_manager_to_accept_right_click(self): fig_manager = MagicMock() canvas = MagicMock() type(canvas).buttond = PropertyMock(return_value={Qt.RightButton: 3}) fig_manager.canvas = canvas return fig_manager def _create_mock_right_click(self): mouse_event = MagicMock(inaxes=MagicMock(spec=MantidAxes)) type(mouse_event).button = PropertyMock(return_value=3) return mouse_event def _test_toggle_normalization(self, errorbars_on, plot_kwargs): fig = plot([self.ws], spectrum_nums=[1], errors=errorbars_on, plot_kwargs=plot_kwargs) mock_canvas = MagicMock(figure=fig) fig_manager_mock = MagicMock(canvas=mock_canvas) fig_interactor = FigureInteraction(fig_manager_mock) # Earlier versions of matplotlib do not store the data assciated with a # line with high precision and hence we need to set a lower tolerance # when making comparisons of this data if matplotlib.__version__ < "2": decimal_tol = 1 else: decimal_tol = 7 ax = fig.axes[0] fig_interactor._toggle_normalization(ax) assert_almost_equal(ax.lines[0].get_xdata(), [15, 25]) assert_almost_equal(ax.lines[0].get_ydata(), [0.2, 0.3], decimal=decimal_tol) self.assertEqual("Counts ($\\AA$)$^{-1}$", ax.get_ylabel()) fig_interactor._toggle_normalization(ax) assert_almost_equal(ax.lines[0].get_xdata(), [15, 25]) assert_almost_equal(ax.lines[0].get_ydata(), [2, 3], decimal=decimal_tol) self.assertEqual("Counts", ax.get_ylabel()) @patch('workbench.plotting.figureinteraction.QMenu', autospec=True) @patch('workbench.plotting.figureinteraction.figure_type', autospec=True) def test_right_click_gives_marker_menu_when_hovering_over_one( self, mocked_figure_type, mocked_qmenu_cls): mouse_event = self._create_mock_right_click() mouse_event.inaxes.get_xlim.return_value = (1, 2) mouse_event.inaxes.get_ylim.return_value = (1, 2) mocked_figure_type.return_value = FigureType.Line marker1 = MagicMock() marker2 = MagicMock() marker3 = MagicMock() self.interactor.markers = [marker1, marker2, marker3] for marker in self.interactor.markers: marker.is_above.return_value = True # Expect a call to QMenu() for the outer menu followed by two more calls # for the Axes and Normalization menus qmenu_call1 = MagicMock() qmenu_call2 = MagicMock() qmenu_call3 = MagicMock() qmenu_call4 = MagicMock() mocked_qmenu_cls.side_effect = [ qmenu_call1, qmenu_call2, qmenu_call3, qmenu_call4 ] with patch('workbench.plotting.figureinteraction.QActionGroup', autospec=True): with patch.object(self.interactor.toolbar_manager, 'is_tool_active', lambda: False): with patch.object(self.interactor.errors_manager, 'add_error_bars_menu', MagicMock()): self.interactor.on_mouse_button_press(mouse_event) self.assertEqual(0, qmenu_call1.addSeparator.call_count) self.assertEqual(0, qmenu_call1.addAction.call_count) expected_qmenu_calls = [ call(), call(marker1.name, qmenu_call1), call(marker2.name, qmenu_call1), call(marker3.name, qmenu_call1) ] self.assertEqual(expected_qmenu_calls, mocked_qmenu_cls.call_args_list) # 2 Actions in marker menu self.assertEqual(2, qmenu_call2.addAction.call_count) self.assertEqual(2, qmenu_call3.addAction.call_count) self.assertEqual(2, qmenu_call4.addAction.call_count) @patch('workbench.plotting.figureinteraction.SingleMarker') def test_adding_horizontal_marker_adds_correct_marker(self, mock_marker): y0, y1 = 0, 1 data = MagicMock() axis = MagicMock() self.interactor._add_horizontal_marker(data, y0, y1, axis) expected_call = call(self.interactor.canvas, '#2ca02c', data, y0, y1, name='marker 0', marker_type='YSingle', line_style='dashed', axis=axis) self.assertEqual(1, mock_marker.call_count) mock_marker.assert_has_calls([expected_call]) @patch('workbench.plotting.figureinteraction.SingleMarker') def test_adding_vertical_marker_adds_correct_marker(self, mock_marker): x0, x1 = 0, 1 data = MagicMock() axis = MagicMock() self.interactor._add_vertical_marker(data, x0, x1, axis) expected_call = call(self.interactor.canvas, '#2ca02c', data, x0, x1, name='marker 0', marker_type='XSingle', line_style='dashed', axis=axis) self.assertEqual(1, mock_marker.call_count) mock_marker.assert_has_calls([expected_call]) def test_delete_marker_does_not_delete_markers_if_not_present(self): marker = MagicMock() self.interactor.markers = [] self.interactor._delete_marker(marker) self.assertEqual(0, self.interactor.canvas.draw.call_count) self.assertEqual(0, marker.marker.remove.call_count) self.assertEqual(0, marker.remove_all_annotations.call_count) def test_delete_marker_preforms_correct_cleanup(self): marker = MagicMock() self.interactor.markers = [marker] self.interactor._delete_marker(marker) self.assertEqual(1, marker.marker.remove.call_count) self.assertEqual(1, marker.remove_all_annotations.call_count) self.assertEqual(1, self.interactor.canvas.draw.call_count) self.assertNotIn(marker, self.interactor.markers) @patch('workbench.plotting.figureinteraction.SingleMarkerEditor') @patch('workbench.plotting.figureinteraction.QApplication') def test_edit_marker_opens_correct_editor(self, mock_qapp, mock_editor): marker = MagicMock() expected_call = [ call(self.interactor.canvas, marker, self.interactor.valid_lines, self.interactor.valid_colors, []) ] self.interactor._edit_marker(marker) self.assertEqual(1, mock_qapp.restoreOverrideCursor.call_count) mock_editor.assert_has_calls(expected_call) @patch('workbench.plotting.figureinteraction.GlobalMarkerEditor') def test_global_edit_marker_opens_correct_editor(self, mock_editor): marker = MagicMock() self.interactor.markers = [marker] expected_call = [ call(self.interactor.canvas, [marker], self.interactor.valid_lines, self.interactor.valid_colors) ] self.interactor._global_edit_markers() mock_editor.assert_has_calls(expected_call) def test_motion_event_returns_if_toolbar_has_active_tools(self): self.interactor.toolbar_manager.is_tool_active = MagicMock( return_value=True) self.interactor._set_hover_cursor = MagicMock() self.interactor.motion_event(MagicMock()) self.assertEqual(0, self.interactor._set_hover_cursor.call_count) def test_motion_event_changes_cursor_and_draws_canvas_if_any_marker_is_moving( self): markers = [MagicMock(), MagicMock(), MagicMock()] for marker in markers: marker.mouse_move.return_value = True event = MagicMock() event.xdata = 1 event.ydata = 2 self.interactor.markers = markers self.interactor.toolbar_manager.is_tool_active = MagicMock( return_value=False) self.interactor._set_hover_cursor = MagicMock() self.interactor.motion_event(event) self.interactor._set_hover_cursor.assert_has_calls([call(1, 2)]) self.assertEqual(1, self.interactor.canvas.draw.call_count) def test_motion_event_changes_cursor_and_does_not_draw_canvas_if_no_marker_is_moving( self): markers = [MagicMock(), MagicMock(), MagicMock()] for marker in markers: marker.mouse_move.return_value = False event = MagicMock() event.xdata = 1 event.ydata = 2 self.interactor.markers = markers self.interactor.toolbar_manager.is_tool_active = MagicMock( return_value=False) self.interactor._set_hover_cursor = MagicMock() self.interactor.motion_event(event) self.interactor._set_hover_cursor.assert_has_calls([call(1, 2)]) self.assertEqual(0, self.interactor.canvas.draw.call_count) def test_redraw_annotations_removes_and_adds_all_annotations_for_all_markers( self): markers = [MagicMock(), MagicMock(), MagicMock()] call_list = [call.remove_all_annotations(), call.add_all_annotations()] self.interactor.markers = markers self.interactor.redraw_annotations() for marker in markers: marker.assert_has_calls(call_list) def test_mpl_redraw_annotations_does_not_redraw_if_event_does_not_have_a_button_attribute( self): self.interactor.redraw_annotations = MagicMock() event = MagicMock(spec='no_button') event.no_button = MagicMock(spec='no_button') self.interactor.mpl_redraw_annotations(event.no_button) self.assertEqual(0, self.interactor.redraw_annotations.call_count) def test_mpl_redraw_annotations_does_not_redraw_if_event_button_not_pressed( self): self.interactor.redraw_annotations = MagicMock() event = MagicMock() event.button = None self.interactor.mpl_redraw_annotations(event) self.assertEqual(0, self.interactor.redraw_annotations.call_count) def test_mpl_redraw_annotations_redraws_if_button_pressed(self): self.interactor.redraw_annotations = MagicMock() event = MagicMock() self.interactor.mpl_redraw_annotations(event) self.assertEqual(1, self.interactor.redraw_annotations.call_count)