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

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

More Qt5 related fixes.

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