source: sasview/src/sas/qtgui/Plotting/PlotterBase.py @ 033b1f2

Last change on this file since 033b1f2 was 8fad50b, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

Replaced pylab.cm with mpl.cm (PK's code review)

  • Property mode set to 100644
File size: 13.0 KB
Line 
1import numpy
2
3from PyQt5 import QtCore
4from PyQt5 import QtGui
5from PyQt5 import QtWidgets, QtPrintSupport
6
7from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
8from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
9
10import matplotlib.pyplot as plt
11import matplotlib as mpl
12from matplotlib import rcParams
13
14DEFAULT_CMAP = mpl.cm.jet
15from sas.qtgui.Plotting.Binder import BindArtist
16from sas.qtgui.Plotting.PlotterData import Data1D
17from sas.qtgui.Plotting.PlotterData import Data2D
18
19from sas.qtgui.Plotting.ScaleProperties import ScaleProperties
20from sas.qtgui.Plotting.WindowTitle import WindowTitle
21import sas.qtgui.Utilities.GuiUtils as GuiUtils
22import sas.qtgui.Plotting.PlotHelper as PlotHelper
23import sas.qtgui.Plotting.PlotUtilities as PlotUtilities
24
25class PlotterBase(QtWidgets.QWidget):
26    def __init__(self, parent=None, manager=None, quickplot=False):
27        super(PlotterBase, self).__init__(parent)
28
29        # Required for the communicator
30        self.manager = manager
31        self.quickplot = quickplot
32
33        # Set auto layout so x/y axis captions don't get cut off
34        rcParams.update({'figure.autolayout': True})
35
36        #plt.style.use('ggplot')
37        #plt.style.use('seaborn-darkgrid')
38
39        # a figure instance to plot on
40        self.figure = plt.figure()
41
42        # Define canvas for the figure to be placed on
43        self.canvas = FigureCanvas(self.figure)
44
45        # Simple window for data display
46        self.txt_widget = QtWidgets.QTextEdit(None)
47
48        # Set the layout and place the canvas widget in it.
49        layout = QtWidgets.QVBoxLayout()
50        # FIXME setMargin -> setContentsMargins in qt5 with 4 args
51        #layout.setContentsMargins(0)
52        layout.addWidget(self.canvas)
53
54        # 1D plotter defaults
55        self.current_plot = 111
56        self._data = [] # Original 1D/2D object
57        self._xscale = 'log'
58        self._yscale = 'log'
59        self.qx_data = []
60        self.qy_data = []
61        self.color = 0
62        self.symbol = 0
63        self.grid_on = False
64        self.scale = 'linear'
65        self.x_label = "log10(x)"
66        self.y_label = "log10(y)"
67
68        # Mouse click related
69        self._scale_xlo = None
70        self._scale_xhi = None
71        self._scale_ylo = None
72        self._scale_yhi = None
73        self.x_click = None
74        self.y_click = None
75        self.event_pos = None
76        self.leftdown = False
77        self.gotLegend = 0
78
79        self.show_legend = True
80
81        # Annotations
82        self.selectedText = None
83        self.textList = []
84
85        # Pre-define the Scale properties dialog
86        self.properties = ScaleProperties(self,
87                                init_scale_x=self.x_label,
88                                init_scale_y=self.y_label)
89
90        # default color map
91        self.cmap = DEFAULT_CMAP
92
93        # Add the axes object -> subplot
94        # TODO: self.ax will have to be tracked and exposed
95        # to enable subplot specific operations
96        self.ax = self.figure.add_subplot(self.current_plot)
97
98        # Remove this, DAMMIT
99        self.axes = [self.ax]
100
101        # Set the background color to white
102        self.canvas.figure.set_facecolor('#FFFFFF')
103
104        # Canvas event handlers
105        self.canvas.mpl_connect('button_release_event', self.onMplMouseUp)
106        self.canvas.mpl_connect('button_press_event', self.onMplMouseDown)
107        self.canvas.mpl_connect('motion_notify_event', self.onMplMouseMotion)
108        self.canvas.mpl_connect('pick_event', self.onMplPick)
109        self.canvas.mpl_connect('scroll_event', self.onMplWheel)
110
111        self.contextMenu = QtWidgets.QMenu(self)
112        self.toolbar = NavigationToolbar(self.canvas, self)
113        layout.addWidget(self.toolbar)
114        if not quickplot:
115            # Add the toolbar
116            # self.toolbar.show()
117            self.toolbar.hide() # hide for the time being
118            # Notify PlotHelper about the new plot
119            self.upatePlotHelper()
120        else:
121            self.toolbar.hide()
122
123        self.setLayout(layout)
124
125    @property
126    def data(self):
127        """ data getter """
128        return self._data
129
130    @data.setter
131    def data(self, data=None):
132        """ Pure virtual data setter """
133        raise NotImplementedError("Data setter must be implemented in derived class.")
134
135    def title(self, title=""):
136        """ title setter """
137        self._title = title
138        # Set the object name to satisfy the Squish object picker
139        self.canvas.setObjectName(title)
140
141    @property
142    def item(self):
143        ''' getter for this plot's QStandardItem '''
144        return self._item
145
146    @item.setter
147    def item(self, item=None):
148        ''' setter for this plot's QStandardItem '''
149        self._item = item
150
151    @property
152    def xLabel(self, xlabel=""):
153        """ x-label setter """
154        return self.x_label
155
156    @xLabel.setter
157    def xLabel(self, xlabel=""):
158        """ x-label setter """
159        self.x_label = r'$%s$'% xlabel if xlabel else ""
160
161    @property
162    def yLabel(self, ylabel=""):
163        """ y-label setter """
164        return self.y_label
165
166    @yLabel.setter
167    def yLabel(self, ylabel=""):
168        """ y-label setter """
169        self.y_label = r'$%s$'% ylabel if ylabel else ""
170
171    @property
172    def yscale(self):
173        """ Y-axis scale getter """
174        return self._yscale
175
176    @yscale.setter
177    def yscale(self, scale='linear'):
178        """ Y-axis scale setter """
179        self.ax.set_yscale(scale, nonposy='clip')
180        self._yscale = scale
181
182    @property
183    def xscale(self):
184        """ X-axis scale getter """
185        return self._xscale
186
187    @xscale.setter
188    def xscale(self, scale='linear'):
189        """ X-axis scale setter """
190        self.ax.cla()
191        self.ax.set_xscale(scale)
192        self._xscale = scale
193
194    @property
195    def showLegend(self):
196        """ Legend visibility getter """
197        return self.show_legend
198
199    @showLegend.setter
200    def showLegend(self, show=True):
201        """ Legend visibility setter """
202        self.show_legend = show
203
204    def upatePlotHelper(self):
205        """
206        Notify the plot helper about the new plot
207        """
208        # Notify the helper
209        PlotHelper.addPlot(self)
210        # Notify the listeners about a new graph
211        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
212
213    def defaultContextMenu(self):
214        """
215        Content of the dialog-universal context menu:
216        Save, Print and Copy
217        """
218        # Actions
219        self.contextMenu.clear()
220        self.actionSaveImage = self.contextMenu.addAction("Save Image")
221        self.actionPrintImage = self.contextMenu.addAction("Print Image")
222        self.actionCopyToClipboard = self.contextMenu.addAction("Copy to Clipboard")
223        #self.contextMenu.addSeparator()
224        #self.actionToggleMenu = self.contextMenu.addAction("Toggle Navigation Menu")
225        self.contextMenu.addSeparator()
226
227
228        # Define the callbacks
229        self.actionSaveImage.triggered.connect(self.onImageSave)
230        self.actionPrintImage.triggered.connect(self.onImagePrint)
231        self.actionCopyToClipboard.triggered.connect(self.onClipboardCopy)
232        #self.actionToggleMenu.triggered.connect(self.onToggleMenu)
233
234    def createContextMenu(self):
235        """
236        Define common context menu and associated actions for the MPL widget
237        """
238        raise NotImplementedError("Context menu method must be implemented in derived class.")
239
240    def createContextMenuQuick(self):
241        """
242        Define context menu and associated actions for the quickplot MPL widget
243        """
244        raise NotImplementedError("Context menu method must be implemented in derived class.")
245
246    def contextMenuEvent(self, event):
247        """
248        Display the context menu
249        """
250        if not self.quickplot:
251            self.createContextMenu()
252        else:
253            self.createContextMenuQuick()
254
255        event_pos = event.pos()
256        self.contextMenu.exec_(self.canvas.mapToGlobal(event_pos))
257
258    def onMplMouseUp(self, event):
259        """
260        Mouse button up callback
261        """
262        pass
263
264    def onMplMouseDown(self, event):
265        """
266        Mouse button down callback
267        """
268        pass
269
270    def onMplMouseMotion(self, event):
271        """
272        Mouse motion callback
273        """
274        pass
275
276    def onMplPick(self, event):
277        """
278        Mouse pick callback
279        """
280        pass
281
282    def onMplWheel(self, event):
283        """
284        Mouse wheel scroll callback
285        """
286        pass
287
288    def clean(self):
289        """
290        Redraw the graph
291        """
292        self.figure.delaxes(self.ax)
293        self.ax = self.figure.add_subplot(self.current_plot)
294
295    def plot(self, marker=None, linestyle=None):
296        """
297        PURE VIRTUAL
298        Plot the content of self._data
299        """
300        raise NotImplementedError("Plot method must be implemented in derived class.")
301
302    def closeEvent(self, event):
303        """
304        Overwrite the close event adding helper notification
305        """
306        # Please remove me from your database.
307        PlotHelper.deletePlot(PlotHelper.idOfPlot(self))
308
309        # Notify the listeners
310        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
311
312        event.accept()
313
314    def onImageSave(self):
315        """
316        Use the internal MPL method for saving to file
317        """
318        if not hasattr(self, "toolbar"):
319            self.toolbar = NavigationToolbar(self.canvas, self)
320        self.toolbar.save_figure()
321
322    def onImagePrint(self):
323        """
324        Display printer dialog and print the MPL widget area
325        """
326        # Define the printer
327        printer = QtPrintSupport.QPrinter()
328
329        # Display the print dialog
330        dialog = QtPrintSupport.QPrintDialog(printer)
331        dialog.setModal(True)
332        dialog.setWindowTitle("Print")
333        if dialog.exec_() != QtWidgets.QDialog.Accepted:
334            return
335
336        painter = QtGui.QPainter(printer)
337        # Grab the widget screenshot
338        pmap = QtGui.QPixmap(self.size())
339        self.render(pmap)
340        # Create a label with pixmap drawn
341        printLabel = QtWidgets.QLabel()
342        printLabel.setPixmap(pmap)
343
344        # Print the label
345        printLabel.render(painter)
346        painter.end()
347
348    def onClipboardCopy(self):
349        """
350        Copy MPL widget area to buffer
351        """
352        bmp = QtWidgets.QApplication.clipboard()
353        pixmap = QtGui.QPixmap(self.canvas.size())
354        self.canvas.render(pixmap)
355        bmp.setPixmap(pixmap)
356
357    def onGridToggle(self):
358        """
359        Add/remove grid lines from MPL plot
360        """
361        self.grid_on = (not self.grid_on)
362        self.ax.grid(self.grid_on)
363        self.canvas.draw_idle()
364
365    def onWindowsTitle(self):
366        """
367        Show a dialog allowing chart title customisation
368        """
369        current_title = self.windowTitle()
370        titleWidget = WindowTitle(self, new_title=current_title)
371        result = titleWidget.exec_()
372        if result != QtWidgets.QDialog.Accepted:
373            return
374
375        title = titleWidget.title()
376        self.setWindowTitle(title)
377        # Notify the listeners about a new graph title
378        self.manager.communicator.activeGraphName.emit((current_title, title))
379
380    def onToggleMenu(self):
381        """
382        Toggle navigation menu visibility in the chart
383        """
384        self.toolbar.hide()
385        # Current toolbar menu is too buggy.
386        # Comment out until we support 3.x, then recheck.
387        #if self.toolbar.isVisible():
388        #    self.toolbar.hide()
389        #else:
390        #    self.toolbar.show()
391
392    def offset_graph(self):
393        """
394        Zoom and offset the graph to the last known settings
395        """
396        for ax in self.axes:
397            if self._scale_xhi is not None and self._scale_xlo is not None:
398                ax.set_xlim(self._scale_xlo, self._scale_xhi)
399            if self._scale_yhi is not None and self._scale_ylo is not None:
400                ax.set_ylim(self._scale_ylo, self._scale_yhi)
401
402    def onDataInfo(self, plot_data):
403        """
404        Displays data info text window for the selected plot
405        """
406        if isinstance(plot_data, Data1D):
407            text_to_show = GuiUtils.retrieveData1d(plot_data)
408        else:
409            text_to_show = GuiUtils.retrieveData2d(plot_data)
410        # Hardcoded sizes to enable full width rendering with default font
411        self.txt_widget.resize(420,600)
412
413        self.txt_widget.setReadOnly(True)
414        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
415        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
416        self.txt_widget.setWindowTitle("Data Info: %s" % plot_data.filename)
417        self.txt_widget.insertPlainText(text_to_show)
418
419        self.txt_widget.show()
420        # Move the slider all the way up, if present
421        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
422        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
423
424    def onSavePoints(self, plot_data):
425        """
426        Saves plot data to a file
427        """
428        if isinstance(plot_data, Data1D):
429            GuiUtils.saveData1D(plot_data)
430        else:
431            GuiUtils.saveData2D(plot_data)
Note: See TracBrowser for help on using the repository browser.