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

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

Setting graph range - SASVIEW-381

  • Property mode set to 100644
File size: 14.6 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
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
[3b7b218]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
[ef01be4]75        # default color map
76        self.cmap = DEFAULT_CMAP
77
[3b7b218]78        # Add the axes object -> subplot
79        # TODO: self.ax will have to be tracked and exposed
80        # to enable subplot specific operations
[ef01be4]81        self.ax = self.figure.add_subplot(self.current_plot)
[3b7b218]82
[9290b1a]83        # Remove this, DAMMIT
84        self.axes = [self.ax]
85
[3b7b218]86        # Set the background color to white
[ef01be4]87        self.canvas.figure.set_facecolor('#FFFFFF')
88
[9290b1a]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)
[d3ca363]94        self.canvas.mpl_connect('scroll_event', self.onMplWheel)
[9290b1a]95
[ef01be4]96        if not quickplot:
[6d05e1d]97            # set the layout
[ef01be4]98            layout.addWidget(self.toolbar)
[6d05e1d]99            # Add the context menu
100            self.contextMenu()
[3b7b218]101            # Notify PlotHelper about the new plot
102            self.upatePlotHelper()
[ef01be4]103        else:
104            self.contextMenuQuickPlot()
105
106        self.setLayout(layout)
107
108    @property
109    def data(self):
[b4b8589]110        """ data getter """
[ef01be4]111        return self._data
112
113    @data.setter
114    def data(self, data=None):
[3b7b218]115        """ Pure virtual data setter """
116        raise NotImplementedError("Data setter must be implemented in derived class.")
[ef01be4]117
118    def title(self, title=""):
119        """ title setter """
[6d05e1d]120        self._title = title
[ef01be4]121
[6d05e1d]122    @property
123    def xLabel(self, xlabel=""):
124        """ x-label setter """
125        return self.x_label
126
127    @xLabel.setter
[ef01be4]128    def xLabel(self, xlabel=""):
129        """ x-label setter """
130        self.x_label = r'$%s$'% xlabel
131
[6d05e1d]132    @property
133    def yLabel(self, ylabel=""):
134        """ y-label setter """
135        return self.y_label
136
137    @yLabel.setter
[ef01be4]138    def yLabel(self, ylabel=""):
139        """ y-label setter """
140        self.y_label = r'$%s$'% ylabel
141
[6d05e1d]142    @property
143    def yscale(self):
144        """ Y-axis scale getter """
145        return self._yscale
146
[ef01be4]147    @yscale.setter
[6d05e1d]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
[ef01be4]158    @xscale.setter
[6d05e1d]159    def xscale(self, scale='linear'):
160        """ X-axis scale setter """
161        self.ax.set_xscale(scale)
162        self._xscale = scale
[ef01be4]163
[3b7b218]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
[c4e5400]173    def defaultContextMenu(self):
[ef01be4]174        """
[c4e5400]175        Content of the dialog-universal context menu:
176        Save, Print and Copy
[ef01be4]177        """
178        # Actions
[6d05e1d]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)
[ef01be4]187        self.actionPrintImage.triggered.connect(self.onImagePrint)
188        self.actionCopyToClipboard.triggered.connect(self.onClipboardCopy)
[6d05e1d]189
[c4e5400]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
[6d05e1d]196    def contextMenuQuickPlot(self):
197        """
198        Define context menu and associated actions for the quickplot MPL widget
199        """
[3b7b218]200        raise NotImplementedError("Context menu method must be implemented in derived class.")
[6d05e1d]201
202    def contextMenuEvent(self, event):
203        """
204        Display the context menu
205        """
[9290b1a]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()
[d3ca363]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()
[6d05e1d]373
[ef01be4]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        """
[3b7b218]383        PURE VIRTUAL
[ef01be4]384        Plot the content of self._data
385        """
[3b7b218]386        raise NotImplementedError("Plot method must be implemented in derived class.")
[ef01be4]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
[416fa8f]395        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
[ef01be4]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
[6d05e1d]409        printer = QtGui.QPrinter()
410
411        # Display the print dialog
412        dialog = QtGui.QPrintDialog(printer)
413        dialog.setModal(True)
414        dialog.setWindowTitle("Print")
[b4b8589]415        if dialog.exec_() != QtGui.QDialog.Accepted:
[ef01be4]416            return
417
418        painter = QtGui.QPainter(printer)
[b4b8589]419        # Grab the widget screenshot
[ef01be4]420        pmap = QtGui.QPixmap.grabWidget(self)
[b4b8589]421        # Create a label with pixmap drawn
[ef01be4]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        """
[6d05e1d]433        bmp = QtGui.QApplication.clipboard()
434        pixmap = QtGui.QPixmap.grabWidget(self.canvas)
435        bmp.setPixmap(pixmap)
[ef01be4]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()
[27313b7]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.