source: sasview/src/sas/qtgui/PlotterBase.py @ aadf0af1

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 aadf0af1 was aadf0af1, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Plot specific part of the context menu SASVIEW-427

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