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

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

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

  • Property mode set to 100644
File size: 11.8 KB
RevLine 
[ef01be4]1import pylab
[9290b1a]2import numpy
[ef01be4]3
[4992ff2]4from PyQt5 import QtCore
5from PyQt5 import QtGui
[53c771e]6from PyQt5 import QtWidgets, QtPrintSupport
[ef01be4]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#
[4992ff2]12# matplotlib.use("Qt5Agg")
13from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
14from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
[ef01be4]15
16import matplotlib.pyplot as plt
17
18DEFAULT_CMAP = pylab.cm.jet
[dc5ef15]19from sas.qtgui.Plotting.Binder import BindArtist
20from sas.qtgui.Plotting.PlotterData import Data1D
21from sas.qtgui.Plotting.PlotterData import Data2D
[9290b1a]22
[83eb5208]23from sas.qtgui.Plotting.ScaleProperties import ScaleProperties
24from sas.qtgui.Plotting.WindowTitle import WindowTitle
[dc5ef15]25import sas.qtgui.Utilities.GuiUtils as GuiUtils
[83eb5208]26import sas.qtgui.Plotting.PlotHelper as PlotHelper
27import sas.qtgui.Plotting.PlotUtilities as PlotUtilities
[ef01be4]28
[4992ff2]29class PlotterBase(QtWidgets.QWidget):
[416fa8f]30    def __init__(self, parent=None, manager=None, quickplot=False):
[ef01be4]31        super(PlotterBase, self).__init__(parent)
32
33        # Required for the communicator
[416fa8f]34        self.manager = manager
[ef01be4]35        self.quickplot = quickplot
36
[fbfc488]37        #plt.style.use('ggplot')
38        plt.style.use('seaborn-darkgrid')
39
[ef01be4]40        # a figure instance to plot on
41        self.figure = plt.figure()
42
[3b7b218]43        # Define canvas for the figure to be placed on
[ef01be4]44        self.canvas = FigureCanvas(self.figure)
45
[3b7b218]46        # ... and the toolbar with all the default MPL buttons
[ef01be4]47        self.toolbar = NavigationToolbar(self.canvas, self)
48
[092a3d9]49        # Simple window for data display
[4992ff2]50        self.txt_widget = QtWidgets.QTextEdit(None)
[092a3d9]51
[3b7b218]52        # Set the layout and place the canvas widget in it.
[4992ff2]53        layout = QtWidgets.QVBoxLayout()
54        # FIXME setMargin -> setContentsMargins in qt5 with 4 args
55        #layout.setContentsMargins(0)
[ef01be4]56        layout.addWidget(self.canvas)
57
[3b7b218]58        # 1D plotter defaults
[ef01be4]59        self.current_plot = 111
[6d05e1d]60        self._data = [] # Original 1D/2D object
61        self._xscale = 'log'
62        self._yscale = 'log'
[ef01be4]63        self.qx_data = []
64        self.qy_data = []
[b4b8589]65        self.color = 0
66        self.symbol = 0
[ef01be4]67        self.grid_on = False
68        self.scale = 'linear'
[6d05e1d]69        self.x_label = "log10(x)"
70        self.y_label = "log10(y)"
[ef01be4]71
[9290b1a]72        # Mouse click related
[b46f285]73        self._scale_xlo = None
74        self._scale_xhi = None
75        self._scale_ylo = None
[570a58f9]76        self._scale_yhi = None
[9290b1a]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
[3b7b218]87        # Pre-define the Scale properties dialog
88        self.properties = ScaleProperties(self,
[570a58f9]89                                init_scale_x=self.x_label,
90                                init_scale_y=self.y_label)
[3b7b218]91
[ef01be4]92        # default color map
93        self.cmap = DEFAULT_CMAP
94
[3b7b218]95        # Add the axes object -> subplot
96        # TODO: self.ax will have to be tracked and exposed
97        # to enable subplot specific operations
[ef01be4]98        self.ax = self.figure.add_subplot(self.current_plot)
[3b7b218]99
[9290b1a]100        # Remove this, DAMMIT
101        self.axes = [self.ax]
102
[3b7b218]103        # Set the background color to white
[ef01be4]104        self.canvas.figure.set_facecolor('#FFFFFF')
105
[9290b1a]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)
[d3ca363]111        self.canvas.mpl_connect('scroll_event', self.onMplWheel)
[9290b1a]112
[4992ff2]113        self.contextMenu = QtWidgets.QMenu(self)
[aadf0af1]114
[ef01be4]115        if not quickplot:
[aadf0af1]116            # Add the toolbar
[ef01be4]117            layout.addWidget(self.toolbar)
[3b7b218]118            # Notify PlotHelper about the new plot
119            self.upatePlotHelper()
[ef01be4]120
121        self.setLayout(layout)
122
123    @property
124    def data(self):
[b4b8589]125        """ data getter """
[ef01be4]126        return self._data
127
128    @data.setter
129    def data(self, data=None):
[3b7b218]130        """ Pure virtual data setter """
131        raise NotImplementedError("Data setter must be implemented in derived class.")
[ef01be4]132
133    def title(self, title=""):
134        """ title setter """
[6d05e1d]135        self._title = title
[7d8bebf]136        # Set the object name to satisfy the Squish object picker
137        self.canvas.setObjectName(title)
[ef01be4]138
[6d05e1d]139    @property
140    def xLabel(self, xlabel=""):
141        """ x-label setter """
142        return self.x_label
143
144    @xLabel.setter
[ef01be4]145    def xLabel(self, xlabel=""):
146        """ x-label setter """
[8f83719f]147        self.x_label = r'$%s$'% xlabel if xlabel else ""
[ef01be4]148
[6d05e1d]149    @property
150    def yLabel(self, ylabel=""):
151        """ y-label setter """
152        return self.y_label
153
154    @yLabel.setter
[ef01be4]155    def yLabel(self, ylabel=""):
156        """ y-label setter """
[8f83719f]157        self.y_label = r'$%s$'% ylabel if ylabel else ""
[ef01be4]158
[6d05e1d]159    @property
160    def yscale(self):
161        """ Y-axis scale getter """
162        return self._yscale
163
[ef01be4]164    @yscale.setter
[6d05e1d]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
[ef01be4]175    @xscale.setter
[6d05e1d]176    def xscale(self, scale='linear'):
177        """ X-axis scale setter """
178        self.ax.set_xscale(scale)
179        self._xscale = scale
[ef01be4]180
[3b7b218]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
[c4e5400]190    def defaultContextMenu(self):
[ef01be4]191        """
[c4e5400]192        Content of the dialog-universal context menu:
193        Save, Print and Copy
[ef01be4]194        """
195        # Actions
[aadf0af1]196        self.contextMenu.clear()
[6d05e1d]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)
[ef01be4]204        self.actionPrintImage.triggered.connect(self.onImagePrint)
205        self.actionCopyToClipboard.triggered.connect(self.onClipboardCopy)
[6d05e1d]206
[aadf0af1]207    def createContextMenu(self):
[c4e5400]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
[aadf0af1]213    def createContextMenuQuick(self):
[6d05e1d]214        """
215        Define context menu and associated actions for the quickplot MPL widget
216        """
[3b7b218]217        raise NotImplementedError("Context menu method must be implemented in derived class.")
[6d05e1d]218
219    def contextMenuEvent(self, event):
220        """
221        Display the context menu
222        """
[aadf0af1]223        if not self.quickplot:
224            self.createContextMenu()
225        else:
226            self.createContextMenuQuick()
227
[9290b1a]228        event_pos = event.pos()
229        self.contextMenu.exec_(self.canvas.mapToGlobal(event_pos))
230
[3bdbfcc]231    def onMplMouseUp(self, event):
[9290b1a]232        """
[3bdbfcc]233        Mouse button up callback
[9290b1a]234        """
[3bdbfcc]235        pass
[9290b1a]236
[3bdbfcc]237    def onMplMouseDown(self, event):
[9290b1a]238        """
[3bdbfcc]239        Mouse button down callback
[9290b1a]240        """
[3bdbfcc]241        pass
[9290b1a]242
243    def onMplMouseMotion(self, event):
244        """
[3bdbfcc]245        Mouse motion callback
[9290b1a]246        """
[3bdbfcc]247        pass
[9290b1a]248
249    def onMplPick(self, event):
250        """
[3bdbfcc]251        Mouse pick callback
[9290b1a]252        """
[3bdbfcc]253        pass
[d3ca363]254
255    def onMplWheel(self, event):
256        """
[3bdbfcc]257        Mouse wheel scroll callback
[d3ca363]258        """
[3bdbfcc]259        pass
[6d05e1d]260
[ef01be4]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        """
[3b7b218]270        PURE VIRTUAL
[ef01be4]271        Plot the content of self._data
272        """
[3b7b218]273        raise NotImplementedError("Plot method must be implemented in derived class.")
[ef01be4]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))
[7d8bebf]281
[ef01be4]282        # Notify the listeners
[416fa8f]283        self.manager.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots())
[7d8bebf]284
[ef01be4]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
[53c771e]298        printer = QtPrintSupport.QPrinter()
[6d05e1d]299
300        # Display the print dialog
[53c771e]301        dialog = QtPrintSupport.QPrintDialog(printer)
[6d05e1d]302        dialog.setModal(True)
303        dialog.setWindowTitle("Print")
[4992ff2]304        if dialog.exec_() != QtWidgets.QDialog.Accepted:
[ef01be4]305            return
306
307        painter = QtGui.QPainter(printer)
[b4b8589]308        # Grab the widget screenshot
[dd150ef]309        pmap = QtGui.QPixmap(self.size())
310        self.render(pmap)
[b4b8589]311        # Create a label with pixmap drawn
[4992ff2]312        printLabel = QtWidgets.QLabel()
[ef01be4]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        """
[7969b9c]323        bmp = QtWidgets.QApplication.clipboard()
[dd150ef]324        pixmap = QtGui.QPixmap(self.canvas.size())
325        self.canvas.render(pixmap)
[6d05e1d]326        bmp.setPixmap(pixmap)
[ef01be4]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()
[27313b7]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_()
[4992ff2]343        if result != QtWidgets.QDialog.Accepted:
[27313b7]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))
[570a58f9]350
[b46f285]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)
[092a3d9]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()
[7969b9c]381        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
[092a3d9]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.