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
Line 
1import pylab
2import numpy
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
16from sas.sasgui.plottools.binder import BindArtist
17
18from sas.qtgui.ScaleProperties import ScaleProperties
19from sas.qtgui.WindowTitle import WindowTitle
20import sas.qtgui.PlotHelper as PlotHelper
21import sas.qtgui.PlotUtilities as PlotUtilities
22
23class PlotterBase(QtGui.QWidget):
24    def __init__(self, parent=None, manager=None, quickplot=False):
25        super(PlotterBase, self).__init__(parent)
26
27        # Required for the communicator
28        self.manager = manager
29        self.quickplot = quickplot
30
31        # a figure instance to plot on
32        self.figure = plt.figure()
33
34        # Define canvas for the figure to be placed on
35        self.canvas = FigureCanvas(self.figure)
36
37        # ... and the toolbar with all the default MPL buttons
38        self.toolbar = NavigationToolbar(self.canvas, self)
39
40        # Set the layout and place the canvas widget in it.
41        layout = QtGui.QVBoxLayout()
42        layout.setMargin(0)
43        layout.addWidget(self.canvas)
44
45        # 1D plotter defaults
46        self.current_plot = 111
47        self._data = [] # Original 1D/2D object
48        self._xscale = 'log'
49        self._yscale = 'log'
50        self.qx_data = []
51        self.qy_data = []
52        self.color = 0
53        self.symbol = 0
54        self.grid_on = False
55        self.scale = 'linear'
56        self.x_label = "log10(x)"
57        self.y_label = "log10(y)"
58
59        # Mouse click related
60        self._scale_xlo = None
61        self._scale_xhi = None
62        self._scale_ylo = None
63        self._scale_yhi = None
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
74        # Pre-define the Scale properties dialog
75        self.properties = ScaleProperties(self,
76                                init_scale_x=self.x_label,
77                                init_scale_y=self.y_label)
78
79        # default color map
80        self.cmap = DEFAULT_CMAP
81
82        # Add the axes object -> subplot
83        # TODO: self.ax will have to be tracked and exposed
84        # to enable subplot specific operations
85        self.ax = self.figure.add_subplot(self.current_plot)
86
87        # Remove this, DAMMIT
88        self.axes = [self.ax]
89
90        # Set the background color to white
91        self.canvas.figure.set_facecolor('#FFFFFF')
92
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)
98        self.canvas.mpl_connect('scroll_event', self.onMplWheel)
99
100        self.contextMenu = QtGui.QMenu(self)
101
102        if not quickplot:
103            # Add the toolbar
104            layout.addWidget(self.toolbar)
105            # Notify PlotHelper about the new plot
106            self.upatePlotHelper()
107
108        self.setLayout(layout)
109
110    @property
111    def data(self):
112        """ data getter """
113        return self._data
114
115    @data.setter
116    def data(self, data=None):
117        """ Pure virtual data setter """
118        raise NotImplementedError("Data setter must be implemented in derived class.")
119
120    def title(self, title=""):
121        """ title setter """
122        self._title = title
123
124    @property
125    def xLabel(self, xlabel=""):
126        """ x-label setter """
127        return self.x_label
128
129    @xLabel.setter
130    def xLabel(self, xlabel=""):
131        """ x-label setter """
132        self.x_label = r'$%s$'% xlabel
133
134    @property
135    def yLabel(self, ylabel=""):
136        """ y-label setter """
137        return self.y_label
138
139    @yLabel.setter
140    def yLabel(self, ylabel=""):
141        """ y-label setter """
142        self.y_label = r'$%s$'% ylabel
143
144    @property
145    def yscale(self):
146        """ Y-axis scale getter """
147        return self._yscale
148
149    @yscale.setter
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
160    @xscale.setter
161    def xscale(self, scale='linear'):
162        """ X-axis scale setter """
163        self.ax.set_xscale(scale)
164        self._xscale = scale
165
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
175    def defaultContextMenu(self):
176        """
177        Content of the dialog-universal context menu:
178        Save, Print and Copy
179        """
180        # Actions
181        self.contextMenu.clear()
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)
189        self.actionPrintImage.triggered.connect(self.onImagePrint)
190        self.actionCopyToClipboard.triggered.connect(self.onClipboardCopy)
191
192    def createContextMenu(self):
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
198    def createContextMenuQuick(self):
199        """
200        Define context menu and associated actions for the quickplot MPL widget
201        """
202        raise NotImplementedError("Context menu method must be implemented in derived class.")
203
204    def contextMenuEvent(self, event):
205        """
206        Display the context menu
207        """
208        if not self.quickplot:
209            self.createContextMenu()
210        else:
211            self.createContextMenuQuick()
212
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()
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()
380
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        """
390        PURE VIRTUAL
391        Plot the content of self._data
392        """
393        raise NotImplementedError("Plot method must be implemented in derived class.")
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
402        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
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
416        printer = QtGui.QPrinter()
417
418        # Display the print dialog
419        dialog = QtGui.QPrintDialog(printer)
420        dialog.setModal(True)
421        dialog.setWindowTitle("Print")
422        if dialog.exec_() != QtGui.QDialog.Accepted:
423            return
424
425        painter = QtGui.QPainter(printer)
426        # Grab the widget screenshot
427        pmap = QtGui.QPixmap.grabWidget(self)
428        # Create a label with pixmap drawn
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        """
440        bmp = QtGui.QApplication.clipboard()
441        pixmap = QtGui.QPixmap.grabWidget(self.canvas)
442        bmp.setPixmap(pixmap)
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()
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))
466
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.