source: sasview/src/sas/qtgui/PlotterBase.py @ 092a3d9

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

Color Map control for 2D charts. Initial commit - SASVIEW-391

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