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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 863ebca was 863ebca, checked in by Piotr Rozyczko <piotrrozyczko@…>, 13 months ago

Introduced navigation bar toggle in context menu for all types of
charts. SASVIEW-890

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