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

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

More inversion work on details in validation, UI design and such

  • Property mode set to 100644
File size: 11.8 KB
Line 
1import pylab
2import numpy
3
4from PyQt5 import QtCore
5from PyQt5 import QtGui
6from PyQt5 import QtWidgets, QtPrintSupport
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 if xlabel else ""
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 if ylabel else ""
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 = QtPrintSupport.QPrinter()
299
300        # Display the print dialog
301        dialog = QtPrintSupport.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(self.size())
310        self.render(pmap)
311        # Create a label with pixmap drawn
312        printLabel = QtWidgets.QLabel()
313        printLabel.setPixmap(pmap)
314
315        # Print the label
316        printLabel.render(painter)
317        painter.end()
318
319    def onClipboardCopy(self):
320        """
321        Copy MPL widget area to buffer
322        """
323        bmp = QtWidgets.QApplication.clipboard()
324        pixmap = QtGui.QPixmap(self.canvas.size())
325        self.canvas.render(pixmap)
326        bmp.setPixmap(pixmap)
327
328    def onGridToggle(self):
329        """
330        Add/remove grid lines from MPL plot
331        """
332        self.grid_on = (not self.grid_on)
333        self.ax.grid(self.grid_on)
334        self.canvas.draw_idle()
335
336    def onWindowsTitle(self):
337        """
338        Show a dialog allowing chart title customisation
339        """
340        current_title = self.windowTitle()
341        titleWidget = WindowTitle(self, new_title=current_title)
342        result = titleWidget.exec_()
343        if result != QtWidgets.QDialog.Accepted:
344            return
345
346        title = titleWidget.title()
347        self.setWindowTitle(title)
348        # Notify the listeners about a new graph title
349        self.manager.communicator.activeGraphName.emit((current_title, title))
350
351    def offset_graph(self):
352        """
353        Zoom and offset the graph to the last known settings
354        """
355        for ax in self.axes:
356            if self._scale_xhi is not None and self._scale_xlo is not None:
357                ax.set_xlim(self._scale_xlo, self._scale_xhi)
358            if self._scale_yhi is not None and self._scale_ylo is not None:
359                ax.set_ylim(self._scale_ylo, self._scale_yhi)
360
361    def onDataInfo(self, plot_data):
362        """
363        Displays data info text window for the selected plot
364        """
365        if isinstance(plot_data, Data1D):
366            text_to_show = GuiUtils.retrieveData1d(plot_data)
367        else:
368            text_to_show = GuiUtils.retrieveData2d(plot_data)
369        # Hardcoded sizes to enable full width rendering with default font
370        self.txt_widget.resize(420,600)
371
372        self.txt_widget.setReadOnly(True)
373        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
374        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
375        self.txt_widget.setWindowTitle("Data Info: %s" % plot_data.filename)
376        self.txt_widget.insertPlainText(text_to_show)
377
378        self.txt_widget.show()
379        # Move the slider all the way up, if present
380        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
381        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
382
383    def onSavePoints(self, plot_data):
384        """
385        Saves plot data to a file
386        """
387        if isinstance(plot_data, Data1D):
388            GuiUtils.saveData1D(plot_data)
389        else:
390            GuiUtils.saveData2D(plot_data)
Note: See TracBrowser for help on using the repository browser.