source: sasview/src/sas/qtgui/Plotting/PlotterBase.py @ 343d7fd

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 343d7fd was 343d7fd, checked in by piotr, 6 years ago

set autolayout for MPL so x/y axis titles don't get squashed. SASVIEW-1000

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