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

Last change on this file since e0da307 was d9150d8, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Delete open plots on data removal SASVIEW-958

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