Esempio n. 1
0
 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())
Esempio n. 4
0
    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)
Esempio n. 5
0
    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()
Esempio n. 8
0
    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)
Esempio n. 9
0
    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))
Esempio n. 10
0
    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)
Esempio n. 11
0
    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)
Esempio n. 12
0
    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)
Esempio n. 13
0
    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)
Esempio n. 17
0
 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)
Esempio n. 18
0
    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))
Esempio n. 19
0
    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())
Esempio n. 20
0
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()
Esempio n. 21
0
 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)
Esempio n. 23
0
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)
Esempio n. 24
0
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())
Esempio n. 25
0
    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_()
Esempio n. 26
0
 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)
Esempio n. 27
0
    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)