source: sasview/src/sas/qtgui/Plotting/PlotterBase.py @ 863ebca

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 863ebca was 863ebca, checked in by Piotr Rozyczko <piotrrozyczko@…>, 3 years ago

Introduced navigation bar toggle in context menu for all types of
charts. SASVIEW-890

  • Property mode set to 100644
File size: 12.8 KB
RevLine 
[ef01be4]1import pylab
[9290b1a]2import numpy
[ef01be4]3
[4992ff2]4from PyQt5 import QtCore
5from PyQt5 import QtGui
[53c771e]6from PyQt5 import QtWidgets, QtPrintSupport
[ef01be4]7
[4992ff2]8from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
9from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
[ef01be4]10
11import matplotlib.pyplot as plt
[343d7fd]12from matplotlib import rcParams
[ef01be4]13
14DEFAULT_CMAP = pylab.cm.jet
[dc5ef15]15from sas.qtgui.Plotting.Binder import BindArtist
16from sas.qtgui.Plotting.PlotterData import Data1D
17from sas.qtgui.Plotting.PlotterData import Data2D
[9290b1a]18
[83eb5208]19from sas.qtgui.Plotting.ScaleProperties import ScaleProperties
20from sas.qtgui.Plotting.WindowTitle import WindowTitle
[dc5ef15]21import sas.qtgui.Utilities.GuiUtils as GuiUtils
[83eb5208]22import sas.qtgui.Plotting.PlotHelper as PlotHelper
23import sas.qtgui.Plotting.PlotUtilities as PlotUtilities
[ef01be4]24
[4992ff2]25class PlotterBase(QtWidgets.QWidget):
[416fa8f]26    def __init__(self, parent=None, manager=None, quickplot=False):
[ef01be4]27        super(PlotterBase, self).__init__(parent)
28
29        # Required for the communicator
[416fa8f]30        self.manager = manager
[ef01be4]31        self.quickplot = quickplot
32
[343d7fd]33        # Set auto layout so x/y axis captions don't get cut off
34        rcParams.update({'figure.autolayout': True})
35
[fbfc488]36        #plt.style.use('ggplot')
[eb1a386]37        #plt.style.use('seaborn-darkgrid')
[fbfc488]38
[ef01be4]39        # a figure instance to plot on
40        self.figure = plt.figure()
41
[3b7b218]42        # Define canvas for the figure to be placed on
[ef01be4]43        self.canvas = FigureCanvas(self.figure)
44
[092a3d9]45        # Simple window for data display
[4992ff2]46        self.txt_widget = QtWidgets.QTextEdit(None)
[092a3d9]47
[3b7b218]48        # Set the layout and place the canvas widget in it.
[4992ff2]49        layout = QtWidgets.QVBoxLayout()
50        # FIXME setMargin -> setContentsMargins in qt5 with 4 args
51        #layout.setContentsMargins(0)
[ef01be4]52        layout.addWidget(self.canvas)
53
[3b7b218]54        # 1D plotter defaults
[ef01be4]55        self.current_plot = 111
[6d05e1d]56        self._data = [] # Original 1D/2D object
57        self._xscale = 'log'
58        self._yscale = 'log'
[ef01be4]59        self.qx_data = []
60        self.qy_data = []
[b4b8589]61        self.color = 0
62        self.symbol = 0
[ef01be4]63        self.grid_on = False
64        self.scale = 'linear'
[6d05e1d]65        self.x_label = "log10(x)"
66        self.y_label = "log10(y)"
[ef01be4]67
[9290b1a]68        # Mouse click related
[b46f285]69        self._scale_xlo = None
70        self._scale_xhi = None
71        self._scale_ylo = None
[570a58f9]72        self._scale_yhi = None
[9290b1a]73        self.x_click = None
74        self.y_click = None
75        self.event_pos = None
76        self.leftdown = False
77        self.gotLegend = 0
78
[42787fb]79        self.show_legend = True
80
[9290b1a]81        # Annotations
82        self.selectedText = None
83        self.textList = []
84
[3b7b218]85        # Pre-define the Scale properties dialog
86        self.properties = ScaleProperties(self,
[570a58f9]87                                init_scale_x=self.x_label,
88                                init_scale_y=self.y_label)
[3b7b218]89
[ef01be4]90        # default color map
91        self.cmap = DEFAULT_CMAP
92
[3b7b218]93        # Add the axes object -> subplot
94        # TODO: self.ax will have to be tracked and exposed
95        # to enable subplot specific operations
[ef01be4]96        self.ax = self.figure.add_subplot(self.current_plot)
[3b7b218]97
[9290b1a]98        # Remove this, DAMMIT
99        self.axes = [self.ax]
100
[3b7b218]101        # Set the background color to white
[ef01be4]102        self.canvas.figure.set_facecolor('#FFFFFF')
103
[9290b1a]104        # Canvas event handlers
105        self.canvas.mpl_connect('button_release_event', self.onMplMouseUp)
106        self.canvas.mpl_connect('button_press_event', self.onMplMouseDown)
107        self.canvas.mpl_connect('motion_notify_event', self.onMplMouseMotion)
108        self.canvas.mpl_connect('pick_event', self.onMplPick)
[d3ca363]109        self.canvas.mpl_connect('scroll_event', self.onMplWheel)
[9290b1a]110
[4992ff2]111        self.contextMenu = QtWidgets.QMenu(self)
[863ebca]112        self.toolbar = NavigationToolbar(self.canvas, self)
113        layout.addWidget(self.toolbar)
[ef01be4]114        if not quickplot:
[aadf0af1]115            # Add the toolbar
[863ebca]116            self.toolbar.show()
[3b7b218]117            # Notify PlotHelper about the new plot
118            self.upatePlotHelper()
[863ebca]119        else:
120            self.toolbar.hide()
[ef01be4]121
122        self.setLayout(layout)
123
124    @property
125    def data(self):
[b4b8589]126        """ data getter """
[ef01be4]127        return self._data
128
129    @data.setter
130    def data(self, data=None):
[3b7b218]131        """ Pure virtual data setter """
132        raise NotImplementedError("Data setter must be implemented in derived class.")
[ef01be4]133
134    def title(self, title=""):
135        """ title setter """
[6d05e1d]136        self._title = title
[7d8bebf]137        # Set the object name to satisfy the Squish object picker
138        self.canvas.setObjectName(title)
[ef01be4]139
[6d05e1d]140    @property
[d9150d8]141    def item(self):
142        ''' getter for this plot's QStandardItem '''
143        return self._item
144
145    @item.setter
146    def item(self, item=None):
147        ''' setter for this plot's QStandardItem '''
148        self._item = item
149
150    @property
[6d05e1d]151    def xLabel(self, xlabel=""):
152        """ x-label setter """
153        return self.x_label
154
155    @xLabel.setter
[ef01be4]156    def xLabel(self, xlabel=""):
157        """ x-label setter """
[8f83719f]158        self.x_label = r'$%s$'% xlabel if xlabel else ""
[ef01be4]159
[6d05e1d]160    @property
161    def yLabel(self, ylabel=""):
162        """ y-label setter """
163        return self.y_label
164
165    @yLabel.setter
[ef01be4]166    def yLabel(self, ylabel=""):
167        """ y-label setter """
[8f83719f]168        self.y_label = r'$%s$'% ylabel if ylabel else ""
[ef01be4]169
[6d05e1d]170    @property
171    def yscale(self):
172        """ Y-axis scale getter """
173        return self._yscale
174
[ef01be4]175    @yscale.setter
[6d05e1d]176    def yscale(self, scale='linear'):
177        """ Y-axis scale setter """
178        self.ax.set_yscale(scale, nonposy='clip')
179        self._yscale = scale
180
181    @property
182    def xscale(self):
183        """ X-axis scale getter """
184        return self._xscale
185
[ef01be4]186    @xscale.setter
[6d05e1d]187    def xscale(self, scale='linear'):
188        """ X-axis scale setter """
[eb1a386]189        self.ax.cla()
[6d05e1d]190        self.ax.set_xscale(scale)
191        self._xscale = scale
[ef01be4]192
[42787fb]193    @property
194    def showLegend(self):
195        """ Legend visibility getter """
196        return self.show_legend
197
198    @showLegend.setter
199    def showLegend(self, show=True):
200        """ Legend visibility setter """
201        self.show_legend = show
202
[3b7b218]203    def upatePlotHelper(self):
204        """
205        Notify the plot helper about the new plot
206        """
207        # Notify the helper
208        PlotHelper.addPlot(self)
209        # Notify the listeners about a new graph
210        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
211
[c4e5400]212    def defaultContextMenu(self):
[ef01be4]213        """
[c4e5400]214        Content of the dialog-universal context menu:
215        Save, Print and Copy
[ef01be4]216        """
217        # Actions
[aadf0af1]218        self.contextMenu.clear()
[6d05e1d]219        self.actionSaveImage = self.contextMenu.addAction("Save Image")
220        self.actionPrintImage = self.contextMenu.addAction("Print Image")
221        self.actionCopyToClipboard = self.contextMenu.addAction("Copy to Clipboard")
222        self.contextMenu.addSeparator()
[863ebca]223        self.actionToggleMenu = self.contextMenu.addAction("Toggle Navigation Menu")
224        self.contextMenu.addSeparator()
225
[6d05e1d]226
227        # Define the callbacks
228        self.actionSaveImage.triggered.connect(self.onImageSave)
[ef01be4]229        self.actionPrintImage.triggered.connect(self.onImagePrint)
230        self.actionCopyToClipboard.triggered.connect(self.onClipboardCopy)
[863ebca]231        self.actionToggleMenu.triggered.connect(self.onToggleMenu)
[6d05e1d]232
[aadf0af1]233    def createContextMenu(self):
[c4e5400]234        """
235        Define common context menu and associated actions for the MPL widget
236        """
237        raise NotImplementedError("Context menu method must be implemented in derived class.")
238
[aadf0af1]239    def createContextMenuQuick(self):
[6d05e1d]240        """
241        Define context menu and associated actions for the quickplot MPL widget
242        """
[3b7b218]243        raise NotImplementedError("Context menu method must be implemented in derived class.")
[6d05e1d]244
245    def contextMenuEvent(self, event):
246        """
247        Display the context menu
248        """
[aadf0af1]249        if not self.quickplot:
250            self.createContextMenu()
251        else:
252            self.createContextMenuQuick()
253
[9290b1a]254        event_pos = event.pos()
255        self.contextMenu.exec_(self.canvas.mapToGlobal(event_pos))
256
[3bdbfcc]257    def onMplMouseUp(self, event):
[9290b1a]258        """
[3bdbfcc]259        Mouse button up callback
[9290b1a]260        """
[3bdbfcc]261        pass
[9290b1a]262
[3bdbfcc]263    def onMplMouseDown(self, event):
[9290b1a]264        """
[3bdbfcc]265        Mouse button down callback
[9290b1a]266        """
[3bdbfcc]267        pass
[9290b1a]268
269    def onMplMouseMotion(self, event):
270        """
[3bdbfcc]271        Mouse motion callback
[9290b1a]272        """
[3bdbfcc]273        pass
[9290b1a]274
275    def onMplPick(self, event):
276        """
[3bdbfcc]277        Mouse pick callback
[9290b1a]278        """
[3bdbfcc]279        pass
[d3ca363]280
281    def onMplWheel(self, event):
282        """
[3bdbfcc]283        Mouse wheel scroll callback
[d3ca363]284        """
[3bdbfcc]285        pass
[6d05e1d]286
[ef01be4]287    def clean(self):
288        """
289        Redraw the graph
290        """
291        self.figure.delaxes(self.ax)
292        self.ax = self.figure.add_subplot(self.current_plot)
293
294    def plot(self, marker=None, linestyle=None):
295        """
[3b7b218]296        PURE VIRTUAL
[ef01be4]297        Plot the content of self._data
298        """
[3b7b218]299        raise NotImplementedError("Plot method must be implemented in derived class.")
[ef01be4]300
301    def closeEvent(self, event):
302        """
303        Overwrite the close event adding helper notification
304        """
305        # Please remove me from your database.
306        PlotHelper.deletePlot(PlotHelper.idOfPlot(self))
[7d8bebf]307
[ef01be4]308        # Notify the listeners
[416fa8f]309        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
[7d8bebf]310
[ef01be4]311        event.accept()
312
313    def onImageSave(self):
314        """
315        Use the internal MPL method for saving to file
316        """
[e30646ab]317        if not hasattr(self, "toolbar"):
318            self.toolbar = NavigationToolbar(self.canvas, self)
[ef01be4]319        self.toolbar.save_figure()
320
321    def onImagePrint(self):
322        """
323        Display printer dialog and print the MPL widget area
324        """
325        # Define the printer
[53c771e]326        printer = QtPrintSupport.QPrinter()
[6d05e1d]327
328        # Display the print dialog
[53c771e]329        dialog = QtPrintSupport.QPrintDialog(printer)
[6d05e1d]330        dialog.setModal(True)
331        dialog.setWindowTitle("Print")
[4992ff2]332        if dialog.exec_() != QtWidgets.QDialog.Accepted:
[ef01be4]333            return
334
335        painter = QtGui.QPainter(printer)
[b4b8589]336        # Grab the widget screenshot
[dd150ef]337        pmap = QtGui.QPixmap(self.size())
338        self.render(pmap)
[b4b8589]339        # Create a label with pixmap drawn
[4992ff2]340        printLabel = QtWidgets.QLabel()
[ef01be4]341        printLabel.setPixmap(pmap)
342
343        # Print the label
344        printLabel.render(painter)
345        painter.end()
346
347    def onClipboardCopy(self):
348        """
349        Copy MPL widget area to buffer
350        """
[7969b9c]351        bmp = QtWidgets.QApplication.clipboard()
[dd150ef]352        pixmap = QtGui.QPixmap(self.canvas.size())
353        self.canvas.render(pixmap)
[6d05e1d]354        bmp.setPixmap(pixmap)
[ef01be4]355
356    def onGridToggle(self):
357        """
358        Add/remove grid lines from MPL plot
359        """
360        self.grid_on = (not self.grid_on)
361        self.ax.grid(self.grid_on)
362        self.canvas.draw_idle()
[27313b7]363
364    def onWindowsTitle(self):
365        """
366        Show a dialog allowing chart title customisation
367        """
368        current_title = self.windowTitle()
369        titleWidget = WindowTitle(self, new_title=current_title)
370        result = titleWidget.exec_()
[4992ff2]371        if result != QtWidgets.QDialog.Accepted:
[27313b7]372            return
373
374        title = titleWidget.title()
375        self.setWindowTitle(title)
376        # Notify the listeners about a new graph title
377        self.manager.communicator.activeGraphName.emit((current_title, title))
[570a58f9]378
[863ebca]379    def onToggleMenu(self):
380        """
381        Toggle navigation menu visibility in the chart
382        """
383        if self.toolbar.isVisible():
384            self.toolbar.hide()
385        else:
386            self.toolbar.show()
387
[b46f285]388    def offset_graph(self):
389        """
390        Zoom and offset the graph to the last known settings
391        """
392        for ax in self.axes:
393            if self._scale_xhi is not None and self._scale_xlo is not None:
394                ax.set_xlim(self._scale_xlo, self._scale_xhi)
395            if self._scale_yhi is not None and self._scale_ylo is not None:
396                ax.set_ylim(self._scale_ylo, self._scale_yhi)
[092a3d9]397
398    def onDataInfo(self, plot_data):
399        """
400        Displays data info text window for the selected plot
401        """
402        if isinstance(plot_data, Data1D):
403            text_to_show = GuiUtils.retrieveData1d(plot_data)
404        else:
405            text_to_show = GuiUtils.retrieveData2d(plot_data)
406        # Hardcoded sizes to enable full width rendering with default font
407        self.txt_widget.resize(420,600)
408
409        self.txt_widget.setReadOnly(True)
410        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
411        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
412        self.txt_widget.setWindowTitle("Data Info: %s" % plot_data.filename)
413        self.txt_widget.insertPlainText(text_to_show)
414
415        self.txt_widget.show()
416        # Move the slider all the way up, if present
417        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
[7969b9c]418        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
[092a3d9]419
420    def onSavePoints(self, plot_data):
421        """
422        Saves plot data to a file
423        """
424        if isinstance(plot_data, Data1D):
425            GuiUtils.saveData1D(plot_data)
426        else:
427            GuiUtils.saveData2D(plot_data)
Note: See TracBrowser for help on using the repository browser.