source: sasview/src/sas/qtgui/Plotting/PlotterBase.py @ 9e587bc

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9e587bc was 34f13a83, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

Disabled native matplotlib toolbar due to bugs in its 2.1.0
implementation. Will revisit when upgrading to 3.0.
SASVIEW-1103, SASVIEW-1106

  • Property mode set to 100644
File size: 13.0 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
[34f13a83]116            # self.toolbar.show()
117            self.toolbar.hide() # hide for the time being
[3b7b218]118            # Notify PlotHelper about the new plot
119            self.upatePlotHelper()
[863ebca]120        else:
121            self.toolbar.hide()
[ef01be4]122
123        self.setLayout(layout)
124
125    @property
126    def data(self):
[b4b8589]127        """ data getter """
[ef01be4]128        return self._data
129
130    @data.setter
131    def data(self, data=None):
[3b7b218]132        """ Pure virtual data setter """
133        raise NotImplementedError("Data setter must be implemented in derived class.")
[ef01be4]134
135    def title(self, title=""):
136        """ title setter """
[6d05e1d]137        self._title = title
[7d8bebf]138        # Set the object name to satisfy the Squish object picker
139        self.canvas.setObjectName(title)
[ef01be4]140
[6d05e1d]141    @property
[d9150d8]142    def item(self):
143        ''' getter for this plot's QStandardItem '''
144        return self._item
145
146    @item.setter
147    def item(self, item=None):
148        ''' setter for this plot's QStandardItem '''
149        self._item = item
150
151    @property
[6d05e1d]152    def xLabel(self, xlabel=""):
153        """ x-label setter """
154        return self.x_label
155
156    @xLabel.setter
[ef01be4]157    def xLabel(self, xlabel=""):
158        """ x-label setter """
[8f83719f]159        self.x_label = r'$%s$'% xlabel if xlabel else ""
[ef01be4]160
[6d05e1d]161    @property
162    def yLabel(self, ylabel=""):
163        """ y-label setter """
164        return self.y_label
165
166    @yLabel.setter
[ef01be4]167    def yLabel(self, ylabel=""):
168        """ y-label setter """
[8f83719f]169        self.y_label = r'$%s$'% ylabel if ylabel else ""
[ef01be4]170
[6d05e1d]171    @property
172    def yscale(self):
173        """ Y-axis scale getter """
174        return self._yscale
175
[ef01be4]176    @yscale.setter
[6d05e1d]177    def yscale(self, scale='linear'):
178        """ Y-axis scale setter """
179        self.ax.set_yscale(scale, nonposy='clip')
180        self._yscale = scale
181
182    @property
183    def xscale(self):
184        """ X-axis scale getter """
185        return self._xscale
186
[ef01be4]187    @xscale.setter
[6d05e1d]188    def xscale(self, scale='linear'):
189        """ X-axis scale setter """
[eb1a386]190        self.ax.cla()
[6d05e1d]191        self.ax.set_xscale(scale)
192        self._xscale = scale
[ef01be4]193
[42787fb]194    @property
195    def showLegend(self):
196        """ Legend visibility getter """
197        return self.show_legend
198
199    @showLegend.setter
200    def showLegend(self, show=True):
201        """ Legend visibility setter """
202        self.show_legend = show
203
[3b7b218]204    def upatePlotHelper(self):
205        """
206        Notify the plot helper about the new plot
207        """
208        # Notify the helper
209        PlotHelper.addPlot(self)
210        # Notify the listeners about a new graph
211        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
212
[c4e5400]213    def defaultContextMenu(self):
[ef01be4]214        """
[c4e5400]215        Content of the dialog-universal context menu:
216        Save, Print and Copy
[ef01be4]217        """
218        # Actions
[aadf0af1]219        self.contextMenu.clear()
[6d05e1d]220        self.actionSaveImage = self.contextMenu.addAction("Save Image")
221        self.actionPrintImage = self.contextMenu.addAction("Print Image")
222        self.actionCopyToClipboard = self.contextMenu.addAction("Copy to Clipboard")
[34f13a83]223        #self.contextMenu.addSeparator()
224        #self.actionToggleMenu = self.contextMenu.addAction("Toggle Navigation Menu")
[863ebca]225        self.contextMenu.addSeparator()
226
[6d05e1d]227
228        # Define the callbacks
229        self.actionSaveImage.triggered.connect(self.onImageSave)
[ef01be4]230        self.actionPrintImage.triggered.connect(self.onImagePrint)
231        self.actionCopyToClipboard.triggered.connect(self.onClipboardCopy)
[34f13a83]232        #self.actionToggleMenu.triggered.connect(self.onToggleMenu)
[6d05e1d]233
[aadf0af1]234    def createContextMenu(self):
[c4e5400]235        """
236        Define common context menu and associated actions for the MPL widget
237        """
238        raise NotImplementedError("Context menu method must be implemented in derived class.")
239
[aadf0af1]240    def createContextMenuQuick(self):
[6d05e1d]241        """
242        Define context menu and associated actions for the quickplot MPL widget
243        """
[3b7b218]244        raise NotImplementedError("Context menu method must be implemented in derived class.")
[6d05e1d]245
246    def contextMenuEvent(self, event):
247        """
248        Display the context menu
249        """
[aadf0af1]250        if not self.quickplot:
251            self.createContextMenu()
252        else:
253            self.createContextMenuQuick()
254
[9290b1a]255        event_pos = event.pos()
256        self.contextMenu.exec_(self.canvas.mapToGlobal(event_pos))
257
[3bdbfcc]258    def onMplMouseUp(self, event):
[9290b1a]259        """
[3bdbfcc]260        Mouse button up callback
[9290b1a]261        """
[3bdbfcc]262        pass
[9290b1a]263
[3bdbfcc]264    def onMplMouseDown(self, event):
[9290b1a]265        """
[3bdbfcc]266        Mouse button down callback
[9290b1a]267        """
[3bdbfcc]268        pass
[9290b1a]269
270    def onMplMouseMotion(self, event):
271        """
[3bdbfcc]272        Mouse motion callback
[9290b1a]273        """
[3bdbfcc]274        pass
[9290b1a]275
276    def onMplPick(self, event):
277        """
[3bdbfcc]278        Mouse pick callback
[9290b1a]279        """
[3bdbfcc]280        pass
[d3ca363]281
282    def onMplWheel(self, event):
283        """
[3bdbfcc]284        Mouse wheel scroll callback
[d3ca363]285        """
[3bdbfcc]286        pass
[6d05e1d]287
[ef01be4]288    def clean(self):
289        """
290        Redraw the graph
291        """
292        self.figure.delaxes(self.ax)
293        self.ax = self.figure.add_subplot(self.current_plot)
294
295    def plot(self, marker=None, linestyle=None):
296        """
[3b7b218]297        PURE VIRTUAL
[ef01be4]298        Plot the content of self._data
299        """
[3b7b218]300        raise NotImplementedError("Plot method must be implemented in derived class.")
[ef01be4]301
302    def closeEvent(self, event):
303        """
304        Overwrite the close event adding helper notification
305        """
306        # Please remove me from your database.
307        PlotHelper.deletePlot(PlotHelper.idOfPlot(self))
[7d8bebf]308
[ef01be4]309        # Notify the listeners
[416fa8f]310        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
[7d8bebf]311
[ef01be4]312        event.accept()
313
314    def onImageSave(self):
315        """
316        Use the internal MPL method for saving to file
317        """
[e30646ab]318        if not hasattr(self, "toolbar"):
319            self.toolbar = NavigationToolbar(self.canvas, self)
[ef01be4]320        self.toolbar.save_figure()
321
322    def onImagePrint(self):
323        """
324        Display printer dialog and print the MPL widget area
325        """
326        # Define the printer
[53c771e]327        printer = QtPrintSupport.QPrinter()
[6d05e1d]328
329        # Display the print dialog
[53c771e]330        dialog = QtPrintSupport.QPrintDialog(printer)
[6d05e1d]331        dialog.setModal(True)
332        dialog.setWindowTitle("Print")
[4992ff2]333        if dialog.exec_() != QtWidgets.QDialog.Accepted:
[ef01be4]334            return
335
336        painter = QtGui.QPainter(printer)
[b4b8589]337        # Grab the widget screenshot
[dd150ef]338        pmap = QtGui.QPixmap(self.size())
339        self.render(pmap)
[b4b8589]340        # Create a label with pixmap drawn
[4992ff2]341        printLabel = QtWidgets.QLabel()
[ef01be4]342        printLabel.setPixmap(pmap)
343
344        # Print the label
345        printLabel.render(painter)
346        painter.end()
347
348    def onClipboardCopy(self):
349        """
350        Copy MPL widget area to buffer
351        """
[7969b9c]352        bmp = QtWidgets.QApplication.clipboard()
[dd150ef]353        pixmap = QtGui.QPixmap(self.canvas.size())
354        self.canvas.render(pixmap)
[6d05e1d]355        bmp.setPixmap(pixmap)
[ef01be4]356
357    def onGridToggle(self):
358        """
359        Add/remove grid lines from MPL plot
360        """
361        self.grid_on = (not self.grid_on)
362        self.ax.grid(self.grid_on)
363        self.canvas.draw_idle()
[27313b7]364
365    def onWindowsTitle(self):
366        """
367        Show a dialog allowing chart title customisation
368        """
369        current_title = self.windowTitle()
370        titleWidget = WindowTitle(self, new_title=current_title)
371        result = titleWidget.exec_()
[4992ff2]372        if result != QtWidgets.QDialog.Accepted:
[27313b7]373            return
374
375        title = titleWidget.title()
376        self.setWindowTitle(title)
377        # Notify the listeners about a new graph title
378        self.manager.communicator.activeGraphName.emit((current_title, title))
[570a58f9]379
[863ebca]380    def onToggleMenu(self):
381        """
382        Toggle navigation menu visibility in the chart
383        """
[34f13a83]384        self.toolbar.hide()
385        # Current toolbar menu is too buggy.
386        # Comment out until we support 3.x, then recheck.
387        #if self.toolbar.isVisible():
388        #    self.toolbar.hide()
389        #else:
390        #    self.toolbar.show()
[863ebca]391
[b46f285]392    def offset_graph(self):
393        """
394        Zoom and offset the graph to the last known settings
395        """
396        for ax in self.axes:
397            if self._scale_xhi is not None and self._scale_xlo is not None:
398                ax.set_xlim(self._scale_xlo, self._scale_xhi)
399            if self._scale_yhi is not None and self._scale_ylo is not None:
400                ax.set_ylim(self._scale_ylo, self._scale_yhi)
[092a3d9]401
402    def onDataInfo(self, plot_data):
403        """
404        Displays data info text window for the selected plot
405        """
406        if isinstance(plot_data, Data1D):
407            text_to_show = GuiUtils.retrieveData1d(plot_data)
408        else:
409            text_to_show = GuiUtils.retrieveData2d(plot_data)
410        # Hardcoded sizes to enable full width rendering with default font
411        self.txt_widget.resize(420,600)
412
413        self.txt_widget.setReadOnly(True)
414        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
415        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
416        self.txt_widget.setWindowTitle("Data Info: %s" % plot_data.filename)
417        self.txt_widget.insertPlainText(text_to_show)
418
419        self.txt_widget.show()
420        # Move the slider all the way up, if present
421        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
[7969b9c]422        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
[092a3d9]423
424    def onSavePoints(self, plot_data):
425        """
426        Saves plot data to a file
427        """
428        if isinstance(plot_data, Data1D):
429            GuiUtils.saveData1D(plot_data)
430        else:
431            GuiUtils.saveData2D(plot_data)
Note: See TracBrowser for help on using the repository browser.