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

Last change on this file since e0da307 was d9150d8, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Delete open plots on data removal SASVIEW-958

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