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

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

Some improvements in plot handling

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