source: sasview/src/sas/qtgui/PlotterBase.py @ 9290b1a

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

Added AddText? to plot, enabled legend drag - SASVIEW-378

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