source: sasview/src/sas/qtgui/Plotting/PlotterBase.py

ESS_GUI
Last change on this file was b016f17, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Resize matplotlib legend with canvas size. SASVIEW-1000

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