source: sasview/src/sas/qtgui/PlotterBase.py @ 5d89f43

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 5d89f43 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
Line 
1import pylab
2import numpy
3
4from PyQt4 import QtGui
5from PyQt4 import QtCore
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
17from sas.sasgui.plottools.binder import BindArtist
18
19import sas.qtgui.GuiUtils as GuiUtils
20from sas.sasgui.guiframe.dataFitting import Data1D, Data2D
21from sas.qtgui.ScaleProperties import ScaleProperties
22from sas.qtgui.WindowTitle import WindowTitle
23import sas.qtgui.PlotHelper as PlotHelper
24import sas.qtgui.PlotUtilities as PlotUtilities
25
26class PlotterBase(QtGui.QWidget):
27    def __init__(self, parent=None, manager=None, quickplot=False):
28        super(PlotterBase, self).__init__(parent)
29
30        # Required for the communicator
31        self.manager = manager
32        self.quickplot = quickplot
33
34        # a figure instance to plot on
35        self.figure = plt.figure()
36
37        # Define canvas for the figure to be placed on
38        self.canvas = FigureCanvas(self.figure)
39
40        # ... and the toolbar with all the default MPL buttons
41        self.toolbar = NavigationToolbar(self.canvas, self)
42
43        # Simple window for data display
44        self.txt_widget = QtGui.QTextEdit(None)
45
46        # Set the layout and place the canvas widget in it.
47        layout = QtGui.QVBoxLayout()
48        layout.setMargin(0)
49        layout.addWidget(self.canvas)
50
51        # 1D plotter defaults
52        self.current_plot = 111
53        self._data = [] # Original 1D/2D object
54        self._xscale = 'log'
55        self._yscale = 'log'
56        self.qx_data = []
57        self.qy_data = []
58        self.color = 0
59        self.symbol = 0
60        self.grid_on = False
61        self.scale = 'linear'
62        self.x_label = "log10(x)"
63        self.y_label = "log10(y)"
64
65        # Mouse click related
66        self._scale_xlo = None
67        self._scale_xhi = None
68        self._scale_ylo = None
69        self._scale_yhi = None
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
80        # Pre-define the Scale properties dialog
81        self.properties = ScaleProperties(self,
82                                init_scale_x=self.x_label,
83                                init_scale_y=self.y_label)
84
85        # default color map
86        self.cmap = DEFAULT_CMAP
87
88        # Add the axes object -> subplot
89        # TODO: self.ax will have to be tracked and exposed
90        # to enable subplot specific operations
91        self.ax = self.figure.add_subplot(self.current_plot)
92
93        # Remove this, DAMMIT
94        self.axes = [self.ax]
95
96        # Set the background color to white
97        self.canvas.figure.set_facecolor('#FFFFFF')
98
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)
104        self.canvas.mpl_connect('scroll_event', self.onMplWheel)
105
106        self.contextMenu = QtGui.QMenu(self)
107
108        if not quickplot:
109            # Add the toolbar
110            layout.addWidget(self.toolbar)
111            # Notify PlotHelper about the new plot
112            self.upatePlotHelper()
113
114        self.setLayout(layout)
115
116    @property
117    def data(self):
118        """ data getter """
119        return self._data
120
121    @data.setter
122    def data(self, data=None):
123        """ Pure virtual data setter """
124        raise NotImplementedError("Data setter must be implemented in derived class.")
125
126    def title(self, title=""):
127        """ title setter """
128        self._title = title
129
130    @property
131    def xLabel(self, xlabel=""):
132        """ x-label setter """
133        return self.x_label
134
135    @xLabel.setter
136    def xLabel(self, xlabel=""):
137        """ x-label setter """
138        self.x_label = r'$%s$'% xlabel
139
140    @property
141    def yLabel(self, ylabel=""):
142        """ y-label setter """
143        return self.y_label
144
145    @yLabel.setter
146    def yLabel(self, ylabel=""):
147        """ y-label setter """
148        self.y_label = r'$%s$'% ylabel
149
150    @property
151    def yscale(self):
152        """ Y-axis scale getter """
153        return self._yscale
154
155    @yscale.setter
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
166    @xscale.setter
167    def xscale(self, scale='linear'):
168        """ X-axis scale setter """
169        self.ax.set_xscale(scale)
170        self._xscale = scale
171
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
181    def defaultContextMenu(self):
182        """
183        Content of the dialog-universal context menu:
184        Save, Print and Copy
185        """
186        # Actions
187        self.contextMenu.clear()
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)
195        self.actionPrintImage.triggered.connect(self.onImagePrint)
196        self.actionCopyToClipboard.triggered.connect(self.onClipboardCopy)
197
198    def createContextMenu(self):
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
204    def createContextMenuQuick(self):
205        """
206        Define context menu and associated actions for the quickplot MPL widget
207        """
208        raise NotImplementedError("Context menu method must be implemented in derived class.")
209
210    def contextMenuEvent(self, event):
211        """
212        Display the context menu
213        """
214        if not self.quickplot:
215            self.createContextMenu()
216        else:
217            self.createContextMenuQuick()
218
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()
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()
386
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        """
396        PURE VIRTUAL
397        Plot the content of self._data
398        """
399        raise NotImplementedError("Plot method must be implemented in derived class.")
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
408        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
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
422        printer = QtGui.QPrinter()
423
424        # Display the print dialog
425        dialog = QtGui.QPrintDialog(printer)
426        dialog.setModal(True)
427        dialog.setWindowTitle("Print")
428        if dialog.exec_() != QtGui.QDialog.Accepted:
429            return
430
431        painter = QtGui.QPainter(printer)
432        # Grab the widget screenshot
433        pmap = QtGui.QPixmap.grabWidget(self)
434        # Create a label with pixmap drawn
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        """
446        bmp = QtGui.QApplication.clipboard()
447        pixmap = QtGui.QPixmap.grabWidget(self.canvas)
448        bmp.setPixmap(pixmap)
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()
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))
472
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)
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.