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

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

Allow for fully toolbar free charts. Part of SASVIEW-789

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