source: sasview/src/sas/qtgui/Plotting/PlotterBase.py @ 343d7fd

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 343d7fd was 343d7fd, checked in by piotr, 3 years ago

set autolayout for MPL so x/y axis titles don't get squashed. SASVIEW-1000

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