source: sasview/src/sas/qtgui/PlotterBase.py @ 0f3c22d

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 0f3c22d was b46f285, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Unit tests for linear fit

  • Property mode set to 100644
File size: 15.2 KB
RevLine 
[ef01be4]1import pylab
[9290b1a]2import numpy
[ef01be4]3
4from PyQt4 import QtGui
5
6# TODO: Replace the qt4agg calls below with qt5 equivalent.
7# Requires some code modifications.
8# https://www.boxcontrol.net/embedding-matplotlib-plot-on-pyqt5-gui.html
9#
10from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
11from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
12
13import matplotlib.pyplot as plt
14
15DEFAULT_CMAP = pylab.cm.jet
[9290b1a]16from sas.sasgui.plottools.binder import BindArtist
17
[ef01be4]18from sas.qtgui.ScaleProperties import ScaleProperties
[27313b7]19from sas.qtgui.WindowTitle import WindowTitle
[ef01be4]20import sas.qtgui.PlotHelper as PlotHelper
[9290b1a]21import sas.qtgui.PlotUtilities as PlotUtilities
[ef01be4]22
[416fa8f]23class PlotterBase(QtGui.QWidget):
24    def __init__(self, parent=None, manager=None, quickplot=False):
[ef01be4]25        super(PlotterBase, self).__init__(parent)
26
27        # Required for the communicator
[416fa8f]28        self.manager = manager
[ef01be4]29        self.quickplot = quickplot
30
31        # a figure instance to plot on
32        self.figure = plt.figure()
33
[3b7b218]34        # Define canvas for the figure to be placed on
[ef01be4]35        self.canvas = FigureCanvas(self.figure)
36
[3b7b218]37        # ... and the toolbar with all the default MPL buttons
[ef01be4]38        self.toolbar = NavigationToolbar(self.canvas, self)
39
[3b7b218]40        # Set the layout and place the canvas widget in it.
[ef01be4]41        layout = QtGui.QVBoxLayout()
[6d05e1d]42        layout.setMargin(0)
[ef01be4]43        layout.addWidget(self.canvas)
44
[3b7b218]45        # 1D plotter defaults
[ef01be4]46        self.current_plot = 111
[6d05e1d]47        self._data = [] # Original 1D/2D object
48        self._xscale = 'log'
49        self._yscale = 'log'
[ef01be4]50        self.qx_data = []
51        self.qy_data = []
[b4b8589]52        self.color = 0
53        self.symbol = 0
[ef01be4]54        self.grid_on = False
55        self.scale = 'linear'
[6d05e1d]56        self.x_label = "log10(x)"
57        self.y_label = "log10(y)"
[ef01be4]58
[9290b1a]59        # Mouse click related
[b46f285]60        self._scale_xlo = None
61        self._scale_xhi = None
62        self._scale_ylo = None
[570a58f9]63        self._scale_yhi = None
[9290b1a]64        self.x_click = None
65        self.y_click = None
66        self.event_pos = None
67        self.leftdown = False
68        self.gotLegend = 0
69
70        # Annotations
71        self.selectedText = None
72        self.textList = []
73
[3b7b218]74        # Pre-define the Scale properties dialog
75        self.properties = ScaleProperties(self,
[570a58f9]76                                init_scale_x=self.x_label,
77                                init_scale_y=self.y_label)
[3b7b218]78
[ef01be4]79        # default color map
80        self.cmap = DEFAULT_CMAP
81
[3b7b218]82        # Add the axes object -> subplot
83        # TODO: self.ax will have to be tracked and exposed
84        # to enable subplot specific operations
[ef01be4]85        self.ax = self.figure.add_subplot(self.current_plot)
[3b7b218]86
[9290b1a]87        # Remove this, DAMMIT
88        self.axes = [self.ax]
89
[3b7b218]90        # Set the background color to white
[ef01be4]91        self.canvas.figure.set_facecolor('#FFFFFF')
92
[9290b1a]93        # Canvas event handlers
94        self.canvas.mpl_connect('button_release_event', self.onMplMouseUp)
95        self.canvas.mpl_connect('button_press_event', self.onMplMouseDown)
96        self.canvas.mpl_connect('motion_notify_event', self.onMplMouseMotion)
97        self.canvas.mpl_connect('pick_event', self.onMplPick)
[d3ca363]98        self.canvas.mpl_connect('scroll_event', self.onMplWheel)
[9290b1a]99
[aadf0af1]100        self.contextMenu = QtGui.QMenu(self)
101
[ef01be4]102        if not quickplot:
[aadf0af1]103            # Add the toolbar
[ef01be4]104            layout.addWidget(self.toolbar)
[3b7b218]105            # Notify PlotHelper about the new plot
106            self.upatePlotHelper()
[ef01be4]107
108        self.setLayout(layout)
109
110    @property
111    def data(self):
[b4b8589]112        """ data getter """
[ef01be4]113        return self._data
114
115    @data.setter
116    def data(self, data=None):
[3b7b218]117        """ Pure virtual data setter """
118        raise NotImplementedError("Data setter must be implemented in derived class.")
[ef01be4]119
120    def title(self, title=""):
121        """ title setter """
[6d05e1d]122        self._title = title
[ef01be4]123
[6d05e1d]124    @property
125    def xLabel(self, xlabel=""):
126        """ x-label setter """
127        return self.x_label
128
129    @xLabel.setter
[ef01be4]130    def xLabel(self, xlabel=""):
131        """ x-label setter """
132        self.x_label = r'$%s$'% xlabel
133
[6d05e1d]134    @property
135    def yLabel(self, ylabel=""):
136        """ y-label setter """
137        return self.y_label
138
139    @yLabel.setter
[ef01be4]140    def yLabel(self, ylabel=""):
141        """ y-label setter """
142        self.y_label = r'$%s$'% ylabel
143
[6d05e1d]144    @property
145    def yscale(self):
146        """ Y-axis scale getter """
147        return self._yscale
148
[ef01be4]149    @yscale.setter
[6d05e1d]150    def yscale(self, scale='linear'):
151        """ Y-axis scale setter """
152        self.ax.set_yscale(scale, nonposy='clip')
153        self._yscale = scale
154
155    @property
156    def xscale(self):
157        """ X-axis scale getter """
158        return self._xscale
159
[ef01be4]160    @xscale.setter
[6d05e1d]161    def xscale(self, scale='linear'):
162        """ X-axis scale setter """
163        self.ax.set_xscale(scale)
164        self._xscale = scale
[ef01be4]165
[3b7b218]166    def upatePlotHelper(self):
167        """
168        Notify the plot helper about the new plot
169        """
170        # Notify the helper
171        PlotHelper.addPlot(self)
172        # Notify the listeners about a new graph
173        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
174
[c4e5400]175    def defaultContextMenu(self):
[ef01be4]176        """
[c4e5400]177        Content of the dialog-universal context menu:
178        Save, Print and Copy
[ef01be4]179        """
180        # Actions
[aadf0af1]181        self.contextMenu.clear()
[6d05e1d]182        self.actionSaveImage = self.contextMenu.addAction("Save Image")
183        self.actionPrintImage = self.contextMenu.addAction("Print Image")
184        self.actionCopyToClipboard = self.contextMenu.addAction("Copy to Clipboard")
185        self.contextMenu.addSeparator()
186
187        # Define the callbacks
188        self.actionSaveImage.triggered.connect(self.onImageSave)
[ef01be4]189        self.actionPrintImage.triggered.connect(self.onImagePrint)
190        self.actionCopyToClipboard.triggered.connect(self.onClipboardCopy)
[6d05e1d]191
[aadf0af1]192    def createContextMenu(self):
[c4e5400]193        """
194        Define common context menu and associated actions for the MPL widget
195        """
196        raise NotImplementedError("Context menu method must be implemented in derived class.")
197
[aadf0af1]198    def createContextMenuQuick(self):
[6d05e1d]199        """
200        Define context menu and associated actions for the quickplot MPL widget
201        """
[3b7b218]202        raise NotImplementedError("Context menu method must be implemented in derived class.")
[6d05e1d]203
204    def contextMenuEvent(self, event):
205        """
206        Display the context menu
207        """
[aadf0af1]208        if not self.quickplot:
209            self.createContextMenu()
210        else:
211            self.createContextMenuQuick()
212
[9290b1a]213        event_pos = event.pos()
214        self.contextMenu.exec_(self.canvas.mapToGlobal(event_pos))
215
216    def onMplMouseDown(self, event):
217        """
218        Left button down and ready to drag
219        """
220        # Check that the LEFT button was pressed
221        if event.button == 1:
222            self.leftdown = True
223            ax = event.inaxes
224            for text in self.textList:
225                if text.contains(event)[0]: # If user has clicked on text
226                    self.selectedText = text
227                    return
228
229            if ax != None:
230                self.xInit, self.yInit = event.xdata, event.ydata
231                try:
232                    self.x_click = float(event.xdata)  # / size_x
233                    self.y_click = float(event.ydata)  # / size_y
234                except:
235                    self.position = None
236
237    def onMplMouseUp(self, event):
238        """
239        Set the data coordinates of the click
240        """
241        self.x_click = event.xdata
242        self.y_click = event.ydata
243
244        # Check that the LEFT button was released
245        if event.button == 1:
246            self.leftdown = False
247            #self.leftup = True
248            self.selectedText = None
249
250        #release the legend
251        if self.gotLegend == 1:
252            self.gotLegend = 0
253
254    def onMplMouseMotion(self, event):
255        """
256        Check if the left button is press and the mouse in moving.
257        Compute delta for x and y coordinates and then perform the drag
258        """
259        if self.gotLegend == 1 and self.leftdown:
260            self.onLegendMotion(event)
261            return
262
263        if self.leftdown and self.selectedText is not None:
264            # User has clicked on text and is dragging
265            ax = event.inaxes
266            if ax != None:
267                # Only move text if mouse is within axes
268                self.selectedText.set_position((event.xdata, event.ydata))
269                self.canvas.draw_idle()
270            else:
271                # User has dragged outside of axes
272                self.selectedText = None
273            return
274
275    def onMplPick(self, event):
276        """
277        On pick legend
278        """
279        legend = self.legend
280        if event.artist == legend:
281            # Get the box of the legend.
282            bbox = self.legend.get_window_extent()
283            # Get mouse coordinates at time of pick.
284            self.mouse_x = event.mouseevent.x
285            self.mouse_y = event.mouseevent.y
286            # Get legend coordinates at time of pick.
287            self.legend_x = bbox.xmin
288            self.legend_y = bbox.ymin
289            # Indicate we picked up the legend.
290            self.gotLegend = 1
291
292            #self.legend.legendPatch.set_alpha(0.5)
293
294    def onLegendMotion(self, event):
295        """
296        On legend in motion
297        """
298        ax = event.inaxes
299        if ax == None:
300            return
301        # Event occurred inside a plotting area
302        lo_x, hi_x = ax.get_xlim()
303        lo_y, hi_y = ax.get_ylim()
304        # How much the mouse moved.
305        x = mouse_diff_x = self.mouse_x - event.x
306        y = mouse_diff_y = self.mouse_y - event.y
307        # Put back inside
308        if x < lo_x:
309            x = lo_x
310        if x > hi_x:
311            x = hi_x
312        if y < lo_y:
313            y = lo_y
314        if y > hi_y:
315            y = hi_y
316        # Move the legend from its previous location by that same amount
317        loc_in_canvas = self.legend_x - mouse_diff_x, \
318                        self.legend_y - mouse_diff_y
319        # Transform into legend coordinate system
320        trans_axes = self.legend.parent.transAxes.inverted()
321        loc_in_norm_axes = trans_axes.transform_point(loc_in_canvas)
322        self.legend_pos_loc = tuple(loc_in_norm_axes)
323        self.legend._loc = self.legend_pos_loc
324        # self.canvas.draw()
[d3ca363]325        self.canvas.draw_idle()
326
327    def onMplWheel(self, event):
328        """
329        Process mouse wheel as zoom events
330        """
331        ax = event.inaxes
332        step = event.step
333
334        if ax != None:
335            # Event occurred inside a plotting area
336            lo, hi = ax.get_xlim()
337            lo, hi = PlotUtilities.rescale(lo, hi, step,
338                              pt=event.xdata, scale=ax.get_xscale())
339            if not self.xscale == 'log' or lo > 0:
340                self._scale_xlo = lo
341                self._scale_xhi = hi
342                ax.set_xlim((lo, hi))
343
344            lo, hi = ax.get_ylim()
345            lo, hi = PlotUtilities.rescale(lo, hi, step, pt=event.ydata,
346                              scale=ax.get_yscale())
347            if not self.yscale == 'log' or lo > 0:
348                self._scale_ylo = lo
349                self._scale_yhi = hi
350                ax.set_ylim((lo, hi))
351        else:
352            # Check if zoom happens in the axes
353            xdata, ydata = None, None
354            x, y = event.x, event.y
355
356            for ax in self.axes:
357                insidex, _ = ax.xaxis.contains(event)
358                if insidex:
359                    xdata, _ = ax.transAxes.inverted().transform_point((x, y))
360                insidey, _ = ax.yaxis.contains(event)
361                if insidey:
362                    _, ydata = ax.transAxes.inverted().transform_point((x, y))
363            if xdata is not None:
364                lo, hi = ax.get_xlim()
365                lo, hi = PlotUtilities.rescale(lo, hi, step,
366                                  bal=xdata, scale=ax.get_xscale())
367                if not self.xscale == 'log' or lo > 0:
368                    self._scale_xlo = lo
369                    self._scale_xhi = hi
370                    ax.set_xlim((lo, hi))
371            if ydata is not None:
372                lo, hi = ax.get_ylim()
373                lo, hi = PlotUtilities.rescale(lo, hi, step, bal=ydata,
374                                  scale=ax.get_yscale())
375                if not self.yscale == 'log' or lo > 0:
376                    self._scale_ylo = lo
377                    self._scale_yhi = hi
378                    ax.set_ylim((lo, hi))
379        self.canvas.draw_idle()
[6d05e1d]380
[ef01be4]381    def clean(self):
382        """
383        Redraw the graph
384        """
385        self.figure.delaxes(self.ax)
386        self.ax = self.figure.add_subplot(self.current_plot)
387
388    def plot(self, marker=None, linestyle=None):
389        """
[3b7b218]390        PURE VIRTUAL
[ef01be4]391        Plot the content of self._data
392        """
[3b7b218]393        raise NotImplementedError("Plot method must be implemented in derived class.")
[ef01be4]394
395    def closeEvent(self, event):
396        """
397        Overwrite the close event adding helper notification
398        """
399        # Please remove me from your database.
400        PlotHelper.deletePlot(PlotHelper.idOfPlot(self))
401        # Notify the listeners
[416fa8f]402        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
[ef01be4]403        event.accept()
404
405    def onImageSave(self):
406        """
407        Use the internal MPL method for saving to file
408        """
409        self.toolbar.save_figure()
410
411    def onImagePrint(self):
412        """
413        Display printer dialog and print the MPL widget area
414        """
415        # Define the printer
[6d05e1d]416        printer = QtGui.QPrinter()
417
418        # Display the print dialog
419        dialog = QtGui.QPrintDialog(printer)
420        dialog.setModal(True)
421        dialog.setWindowTitle("Print")
[b4b8589]422        if dialog.exec_() != QtGui.QDialog.Accepted:
[ef01be4]423            return
424
425        painter = QtGui.QPainter(printer)
[b4b8589]426        # Grab the widget screenshot
[ef01be4]427        pmap = QtGui.QPixmap.grabWidget(self)
[b4b8589]428        # Create a label with pixmap drawn
[ef01be4]429        printLabel = QtGui.QLabel()
430        printLabel.setPixmap(pmap)
431
432        # Print the label
433        printLabel.render(painter)
434        painter.end()
435
436    def onClipboardCopy(self):
437        """
438        Copy MPL widget area to buffer
439        """
[6d05e1d]440        bmp = QtGui.QApplication.clipboard()
441        pixmap = QtGui.QPixmap.grabWidget(self.canvas)
442        bmp.setPixmap(pixmap)
[ef01be4]443
444    def onGridToggle(self):
445        """
446        Add/remove grid lines from MPL plot
447        """
448        self.grid_on = (not self.grid_on)
449        self.ax.grid(self.grid_on)
450        self.canvas.draw_idle()
[27313b7]451
452    def onWindowsTitle(self):
453        """
454        Show a dialog allowing chart title customisation
455        """
456        current_title = self.windowTitle()
457        titleWidget = WindowTitle(self, new_title=current_title)
458        result = titleWidget.exec_()
459        if result != QtGui.QDialog.Accepted:
460            return
461
462        title = titleWidget.title()
463        self.setWindowTitle(title)
464        # Notify the listeners about a new graph title
465        self.manager.communicator.activeGraphName.emit((current_title, title))
[570a58f9]466
[b46f285]467    def offset_graph(self):
468        """
469        Zoom and offset the graph to the last known settings
470        """
471        for ax in self.axes:
472            if self._scale_xhi is not None and self._scale_xlo is not None:
473                ax.set_xlim(self._scale_xlo, self._scale_xhi)
474            if self._scale_yhi is not None and self._scale_ylo is not None:
475                ax.set_ylim(self._scale_ylo, self._scale_yhi)
Note: See TracBrowser for help on using the repository browser.