source: sasview/src/sas/qtgui/Plotting/PlotterBase.py

ESS_GUI
Last change on this file was b016f17, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 3 years ago

Resize matplotlib legend with canvas size. SASVIEW-1000

  • Property mode set to 100644
File size: 13.2 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        cid = self.canvas.mpl_connect('resize_event', self.onResize)
114
115        layout.addWidget(self.toolbar)
116        if not quickplot:
117            # Add the toolbar
118            # self.toolbar.show()
119            self.toolbar.hide() # hide for the time being
120            # Notify PlotHelper about the new plot
121            self.upatePlotHelper()
122        else:
123            self.toolbar.hide()
124
125        self.setLayout(layout)
126
127    @property
128    def data(self):
129        """ data getter """
130        return self._data
131
132    @data.setter
133    def data(self, data=None):
134        """ Pure virtual data setter """
135        raise NotImplementedError("Data setter must be implemented in derived class.")
136
137    def title(self, title=""):
138        """ title setter """
139        self._title = title
140        # Set the object name to satisfy the Squish object picker
141        self.canvas.setObjectName(title)
142
143    @property
144    def item(self):
145        ''' getter for this plot's QStandardItem '''
146        return self._item
147
148    @item.setter
149    def item(self, item=None):
150        ''' setter for this plot's QStandardItem '''
151        self._item = item
152
153    @property
154    def xLabel(self, xlabel=""):
155        """ x-label setter """
156        return self.x_label
157
158    @xLabel.setter
159    def xLabel(self, xlabel=""):
160        """ x-label setter """
161        self.x_label = r'$%s$'% xlabel if xlabel else ""
162
163    @property
164    def yLabel(self, ylabel=""):
165        """ y-label setter """
166        return self.y_label
167
168    @yLabel.setter
169    def yLabel(self, ylabel=""):
170        """ y-label setter """
171        self.y_label = r'$%s$'% ylabel if ylabel else ""
172
173    @property
174    def yscale(self):
175        """ Y-axis scale getter """
176        return self._yscale
177
178    @yscale.setter
179    def yscale(self, scale='linear'):
180        """ Y-axis scale setter """
181        self.ax.set_yscale(scale, nonposy='clip')
182        self._yscale = scale
183
184    @property
185    def xscale(self):
186        """ X-axis scale getter """
187        return self._xscale
188
189    @xscale.setter
190    def xscale(self, scale='linear'):
191        """ X-axis scale setter """
192        self.ax.cla()
193        self.ax.set_xscale(scale)
194        self._xscale = scale
195
196    @property
197    def showLegend(self):
198        """ Legend visibility getter """
199        return self.show_legend
200
201    @showLegend.setter
202    def showLegend(self, show=True):
203        """ Legend visibility setter """
204        self.show_legend = show
205
206    def upatePlotHelper(self):
207        """
208        Notify the plot helper about the new plot
209        """
210        # Notify the helper
211        PlotHelper.addPlot(self)
212        # Notify the listeners about a new graph
213        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
214
215    def defaultContextMenu(self):
216        """
217        Content of the dialog-universal context menu:
218        Save, Print and Copy
219        """
220        # Actions
221        self.contextMenu.clear()
222        self.actionSaveImage = self.contextMenu.addAction("Save Image")
223        self.actionPrintImage = self.contextMenu.addAction("Print Image")
224        self.actionCopyToClipboard = self.contextMenu.addAction("Copy to Clipboard")
225        #self.contextMenu.addSeparator()
226        #self.actionToggleMenu = self.contextMenu.addAction("Toggle Navigation Menu")
227        self.contextMenu.addSeparator()
228
229
230        # Define the callbacks
231        self.actionSaveImage.triggered.connect(self.onImageSave)
232        self.actionPrintImage.triggered.connect(self.onImagePrint)
233        self.actionCopyToClipboard.triggered.connect(self.onClipboardCopy)
234        #self.actionToggleMenu.triggered.connect(self.onToggleMenu)
235
236    def createContextMenu(self):
237        """
238        Define common context menu and associated actions for the MPL widget
239        """
240        raise NotImplementedError("Context menu method must be implemented in derived class.")
241
242    def createContextMenuQuick(self):
243        """
244        Define context menu and associated actions for the quickplot MPL widget
245        """
246        raise NotImplementedError("Context menu method must be implemented in derived class.")
247
248    def onResize(self, event):
249        """
250        Redefine default resize event
251        """
252        pass
253
254    def contextMenuEvent(self, event):
255        """
256        Display the context menu
257        """
258        if not self.quickplot:
259            self.createContextMenu()
260        else:
261            self.createContextMenuQuick()
262
263        event_pos = event.pos()
264        self.contextMenu.exec_(self.canvas.mapToGlobal(event_pos))
265
266    def onMplMouseUp(self, event):
267        """
268        Mouse button up callback
269        """
270        pass
271
272    def onMplMouseDown(self, event):
273        """
274        Mouse button down callback
275        """
276        pass
277
278    def onMplMouseMotion(self, event):
279        """
280        Mouse motion callback
281        """
282        pass
283
284    def onMplPick(self, event):
285        """
286        Mouse pick callback
287        """
288        pass
289
290    def onMplWheel(self, event):
291        """
292        Mouse wheel scroll callback
293        """
294        pass
295
296    def clean(self):
297        """
298        Redraw the graph
299        """
300        self.figure.delaxes(self.ax)
301        self.ax = self.figure.add_subplot(self.current_plot)
302
303    def plot(self, marker=None, linestyle=None):
304        """
305        PURE VIRTUAL
306        Plot the content of self._data
307        """
308        raise NotImplementedError("Plot method must be implemented in derived class.")
309
310    def closeEvent(self, event):
311        """
312        Overwrite the close event adding helper notification
313        """
314        # Please remove me from your database.
315        PlotHelper.deletePlot(PlotHelper.idOfPlot(self))
316
317        # Notify the listeners
318        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
319
320        event.accept()
321
322    def onImageSave(self):
323        """
324        Use the internal MPL method for saving to file
325        """
326        if not hasattr(self, "toolbar"):
327            self.toolbar = NavigationToolbar(self.canvas, self)
328        self.toolbar.save_figure()
329
330    def onImagePrint(self):
331        """
332        Display printer dialog and print the MPL widget area
333        """
334        # Define the printer
335        printer = QtPrintSupport.QPrinter()
336
337        # Display the print dialog
338        dialog = QtPrintSupport.QPrintDialog(printer)
339        dialog.setModal(True)
340        dialog.setWindowTitle("Print")
341        if dialog.exec_() != QtWidgets.QDialog.Accepted:
342            return
343
344        painter = QtGui.QPainter(printer)
345        # Grab the widget screenshot
346        pmap = QtGui.QPixmap(self.size())
347        self.render(pmap)
348        # Create a label with pixmap drawn
349        printLabel = QtWidgets.QLabel()
350        printLabel.setPixmap(pmap)
351
352        # Print the label
353        printLabel.render(painter)
354        painter.end()
355
356    def onClipboardCopy(self):
357        """
358        Copy MPL widget area to buffer
359        """
360        bmp = QtWidgets.QApplication.clipboard()
361        pixmap = QtGui.QPixmap(self.canvas.size())
362        self.canvas.render(pixmap)
363        bmp.setPixmap(pixmap)
364
365    def onGridToggle(self):
366        """
367        Add/remove grid lines from MPL plot
368        """
369        self.grid_on = (not self.grid_on)
370        self.ax.grid(self.grid_on)
371        self.canvas.draw_idle()
372
373    def onWindowsTitle(self):
374        """
375        Show a dialog allowing chart title customisation
376        """
377        current_title = self.windowTitle()
378        titleWidget = WindowTitle(self, new_title=current_title)
379        result = titleWidget.exec_()
380        if result != QtWidgets.QDialog.Accepted:
381            return
382
383        title = titleWidget.title()
384        self.setWindowTitle(title)
385        # Notify the listeners about a new graph title
386        self.manager.communicator.activeGraphName.emit((current_title, title))
387
388    def onToggleMenu(self):
389        """
390        Toggle navigation menu visibility in the chart
391        """
392        self.toolbar.hide()
393        # Current toolbar menu is too buggy.
394        # Comment out until we support 3.x, then recheck.
395        #if self.toolbar.isVisible():
396        #    self.toolbar.hide()
397        #else:
398        #    self.toolbar.show()
399
400    def offset_graph(self):
401        """
402        Zoom and offset the graph to the last known settings
403        """
404        for ax in self.axes:
405            if self._scale_xhi is not None and self._scale_xlo is not None:
406                ax.set_xlim(self._scale_xlo, self._scale_xhi)
407            if self._scale_yhi is not None and self._scale_ylo is not None:
408                ax.set_ylim(self._scale_ylo, self._scale_yhi)
409
410    def onDataInfo(self, plot_data):
411        """
412        Displays data info text window for the selected plot
413        """
414        if isinstance(plot_data, Data1D):
415            text_to_show = GuiUtils.retrieveData1d(plot_data)
416        else:
417            text_to_show = GuiUtils.retrieveData2d(plot_data)
418        # Hardcoded sizes to enable full width rendering with default font
419        self.txt_widget.resize(420,600)
420
421        self.txt_widget.setReadOnly(True)
422        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
423        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
424        self.txt_widget.setWindowTitle("Data Info: %s" % plot_data.filename)
425        self.txt_widget.insertPlainText(text_to_show)
426
427        self.txt_widget.show()
428        # Move the slider all the way up, if present
429        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
430        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
431
432    def onSavePoints(self, plot_data):
433        """
434        Saves plot data to a file
435        """
436        if isinstance(plot_data, Data1D):
437            GuiUtils.saveData1D(plot_data)
438        else:
439            GuiUtils.saveData2D(plot_data)
Note: See TracBrowser for help on using the repository browser.