source: sasview/src/sas/qtgui/Plotter.py @ aadf0af1

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

Plot specific part of the context menu SASVIEW-427

  • Property mode set to 100644
File size: 13.9 KB
RevLine 
[8cb6cd6]1from PyQt4 import QtGui
[aadf0af1]2from PyQt4 import QtCore
3import functools
4import copy
[8cb6cd6]5
6import matplotlib.pyplot as plt
[9290b1a]7from matplotlib.font_manager import FontProperties
[8cb6cd6]8
[9290b1a]9from sas.sasgui.guiframe.dataFitting import Data1D
[ef01be4]10from sas.qtgui.PlotterBase import PlotterBase
[aadf0af1]11import sas.qtgui.GuiUtils as GuiUtils
[9290b1a]12from sas.qtgui.AddText import AddText
[d3ca363]13from sas.qtgui.SetGraphRange import SetGraphRange
[8cb6cd6]14
[416fa8f]15class PlotterWidget(PlotterBase):
[c4e5400]16    """
17    1D Plot widget for use with a QDialog
[fecfe28]18    """
[416fa8f]19    def __init__(self, parent=None, manager=None, quickplot=False):
20        super(PlotterWidget, self).__init__(parent, manager=manager, quickplot=quickplot)
[27313b7]21        self.parent = parent
[257bd57]22        self.addText = AddText(self)
[8cb6cd6]23
[aadf0af1]24        # Dictionary of {plot_id:Data1d}
25        self.plot_dict = {}
26
27        # Simple window for data display
28        self.txt_widget = QtGui.QTextEdit(None)
29
[31c5b58]30    @property
31    def data(self):
32        return self._data
33
34    @data.setter
35    def data(self, value):
[8cb6cd6]36        """ data setter """
[31c5b58]37        self._data = value
[6d05e1d]38        self.xLabel = "%s(%s)"%(value._xaxis, value._xunit)
39        self.yLabel = "%s(%s)"%(value._yaxis, value._yunit)
[aadf0af1]40        self.title(title=value.name)
[8cb6cd6]41
[9290b1a]42    def plot(self, data=None, marker=None, linestyle=None, hide_error=False):
[8cb6cd6]43        """
[aadf0af1]44        Add a new plot of self._data to the chart.
[8cb6cd6]45        """
[9290b1a]46        # Data1D
47        if isinstance(data, Data1D):
48            self.data = data
49        assert(self._data)
50
[6d05e1d]51        # Shortcut for an axis
[ef01be4]52        ax = self.ax
[8cb6cd6]53
[39551a68]54        if marker == None:
[6d05e1d]55            marker = 'o'
[39551a68]56
57        if linestyle == None:
[b4b8589]58            linestyle = ''
59
[aadf0af1]60        if not self._title:
61            self.title(title=self.data.name)
62
[b4b8589]63        # plot data with/without errorbars
[c4e5400]64        if hide_error:
[aadf0af1]65            line = ax.plot(self._data.view.x, self._data.view.y,
[c4e5400]66                    marker=marker,
67                    linestyle=linestyle,
[9290b1a]68                    label=self._title,
69                    picker=True)
[c4e5400]70        else:
[aadf0af1]71            line = ax.errorbar(self._data.view.x, self._data.view.y,
72                        yerr=self._data.view.dy, xerr=None,
[c4e5400]73                        capsize=2, linestyle='',
74                        barsabove=False,
75                        marker=marker,
76                        lolims=False, uplims=False,
77                        xlolims=False, xuplims=False,
[9290b1a]78                        label=self._title,
79                        picker=True)
[8cb6cd6]80
[aadf0af1]81        # Update the list of data sets (plots) in chart
82        self.plot_dict[self._data.id] = self.data
83
[8cb6cd6]84        # Now add the legend with some customizations.
[9290b1a]85        self.legend = ax.legend(loc='upper right', shadow=True)
86        self.legend.set_picker(True)
[8cb6cd6]87
[6d05e1d]88        # Current labels for axes
[ef01be4]89        ax.set_ylabel(self.y_label)
90        ax.set_xlabel(self.x_label)
[6d05e1d]91
[ef01be4]92        # Title only for regular charts
93        if not self.quickplot:
[6d05e1d]94            ax.set_title(label=self._title)
[8cb6cd6]95
[6d05e1d]96        # Include scaling (log vs. linear)
97        ax.set_xscale(self.xscale)
[3b7b218]98        ax.set_yscale(self.yscale)
[8cb6cd6]99
[257bd57]100        # define the ranges
101        self.setRange = SetGraphRange(parent=self,
102            x_range=self.ax.get_xlim(), y_range=self.ax.get_ylim())
103
[8cb6cd6]104        # refresh canvas
105        self.canvas.draw()
106
[aadf0af1]107    def createContextMenu(self):
[c4e5400]108        """
109        Define common context menu and associated actions for the MPL widget
110        """
111        self.defaultContextMenu()
112
[aadf0af1]113        # Separate plots
114        self.addPlotsToContextMenu()
115
[27313b7]116        # Additional menu items
117        self.contextMenu.addSeparator()
118        self.actionModifyGraphAppearance =\
119            self.contextMenu.addAction("Modify Graph Appearance")
120        self.contextMenu.addSeparator()
121        self.actionAddText = self.contextMenu.addAction("Add Text")
122        self.actionRemoveText = self.contextMenu.addAction("Remove Text")
123        self.contextMenu.addSeparator()
124        self.actionChangeScale = self.contextMenu.addAction("Change Scale")
125        self.contextMenu.addSeparator()
126        self.actionSetGraphRange = self.contextMenu.addAction("Set Graph Range")
127        self.actionResetGraphRange =\
128            self.contextMenu.addAction("Reset Graph Range")
129        # Add the title change for dialogs
[aadf0af1]130        #if self.parent:
131        self.contextMenu.addSeparator()
132        self.actionWindowTitle = self.contextMenu.addAction("Window Title")
[27313b7]133
134        # Define the callbacks
135        self.actionModifyGraphAppearance.triggered.connect(self.onModifyGraph)
136        self.actionAddText.triggered.connect(self.onAddText)
137        self.actionRemoveText.triggered.connect(self.onRemoveText)
138        self.actionChangeScale.triggered.connect(self.onScaleChange)
139        self.actionSetGraphRange.triggered.connect(self.onSetGraphRange)
140        self.actionResetGraphRange.triggered.connect(self.onResetGraphRange)
141        self.actionWindowTitle.triggered.connect(self.onWindowsTitle)
[c4e5400]142
[aadf0af1]143    def addPlotsToContextMenu(self):
144        """
145        Adds operations on all plotted sets of data to the context menu
146        """
147        for id in self.plot_dict.keys():
148            plot = self.plot_dict[id]
149            name = plot.name
150            plot_menu = self.contextMenu.addMenu('&%s' % name)
151
152            self.actionDataInfo = plot_menu.addAction("&DataInfo")
153            self.actionDataInfo.triggered.connect(
154                                functools.partial(self.onDataInfo, plot))
155
156            self.actionSavePointsAsFile = plot_menu.addAction("&Save Points as a File")
157            self.actionSavePointsAsFile.triggered.connect(
158                                functools.partial(self.onSavePoints, plot))
159            plot_menu.addSeparator()
160
161            if plot.id != 'fit':
162                self.actionLinearFit = plot_menu.addAction('&Linear Fit')
163                self.actionLinearFit.triggered.connect(self.onLinearFit)
164                plot_menu.addSeparator()
165
166            self.actionRemovePlot = plot_menu.addAction("Remove")
167            self.actionRemovePlot.triggered.connect(
168                                functools.partial(self.onRemovePlot, id))
169
170            if not plot.is_data:
171                self.actionFreeze = plot_menu.addAction('&Freeze')
172                self.actionFreeze.triggered.connect(
173                                functools.partial(self.onFreeze, id))
174            plot_menu.addSeparator()
175
176            if plot.is_data:
177                self.actionHideError = plot_menu.addAction("Hide Error Bar")
178                if plot.dy is not None and plot.dy != []:
179                    if plot.hide_error:
180                        self.actionHideError.setText('Show Error Bar')
181                else:
182                    self.actionHideError.setEnabled(False)
183                self.actionHideError.triggered.connect(
184                                functools.partial(self.onToggleHideError, id))
185                plot_menu.addSeparator()
186
187            self.actionModifyPlot = plot_menu.addAction('&Modify Plot Property')
188            self.actionModifyPlot.triggered.connect(self.onModifyPlot)
189
190    def createContextMenuQuick(self):
[6d05e1d]191        """
192        Define context menu and associated actions for the quickplot MPL widget
193        """
[c4e5400]194        # Default actions
195        self.defaultContextMenu()
196
197        # Additional actions
[6d05e1d]198        self.actionToggleGrid = self.contextMenu.addAction("Toggle Grid On/Off")
199        self.contextMenu.addSeparator()
200        self.actionChangeScale = self.contextMenu.addAction("Change Scale")
201
202        # Define the callbacks
203        self.actionToggleGrid.triggered.connect(self.onGridToggle)
204        self.actionChangeScale.triggered.connect(self.onScaleChange)
205
206    def onScaleChange(self):
207        """
208        Show a dialog allowing axes rescaling
209        """
210        if self.properties.exec_() == QtGui.QDialog.Accepted:
211            xLabel, yLabel = self.properties.getValues()
212            self.xyTransform(xLabel, yLabel)
213
[27313b7]214    def onModifyGraph(self):
215        """
216        Show a dialog allowing chart manipulations
217        """
218        print ("onModifyGraph")
219        pass
220
221    def onAddText(self):
222        """
223        Show a dialog allowing adding custom text to the chart
224        """
[9290b1a]225        if self.addText.exec_() == QtGui.QDialog.Accepted:
226            # Retrieve the new text, its font and color
227            extra_text = self.addText.text()
228            extra_font = self.addText.font()
229            extra_color = self.addText.color()
230
231            # Place the text on the screen at (0,0)
232            pos_x = self.x_click
233            pos_y = self.y_click
234
235            # Map QFont onto MPL font
236            mpl_font = FontProperties()
237            mpl_font.set_size(int(extra_font.pointSize()))
238            mpl_font.set_family(str(extra_font.family()))
239            mpl_font.set_weight(int(extra_font.weight()))
240            # MPL style names
241            styles = ['normal', 'italic', 'oblique']
242            # QFont::Style maps directly onto the above
243            try:
244                mpl_font.set_style(styles[extra_font.style()])
245            except:
246                pass
247
248            if len(extra_text) > 0:
249                new_text = self.ax.text(str(pos_x),
250                                        str(pos_y),
251                                        extra_text,
252                                        color=extra_color,
253                                        fontproperties=mpl_font)
254                # Update the list of annotations
255                self.textList.append(new_text)
256                self.canvas.draw_idle()
[27313b7]257
258    def onRemoveText(self):
259        """
[9290b1a]260        Remove the most recently added text
[27313b7]261        """
[d3ca363]262        num_text = len(self.textList)
263        if num_text < 1:
264            return
265        txt = self.textList[num_text - 1]
266        text_remove = txt.get_text()
267        txt.remove()
268        self.textList.remove(txt)
269
270        self.canvas.draw_idle()
[27313b7]271
272    def onSetGraphRange(self):
273        """
274        Show a dialog allowing setting the chart ranges
275        """
[d3ca363]276        # min and max of data
277        if self.setRange.exec_() == QtGui.QDialog.Accepted:
[257bd57]278            x_range = self.setRange.xrange()
279            y_range = self.setRange.yrange()
280            if x_range is not None and y_range is not None:
281                self.ax.set_xlim(x_range)
282                self.ax.set_ylim(y_range)
283                self.canvas.draw_idle()
[27313b7]284
285    def onResetGraphRange(self):
286        """
[d3ca363]287        Resets the chart X and Y ranges to their original values
[27313b7]288        """
[d3ca363]289        x_range = (self.data.x.min(), self.data.x.max())
290        y_range = (self.data.y.min(), self.data.y.max())
[257bd57]291        if x_range is not None and y_range is not None:
292            self.ax.set_xlim(x_range)
293            self.ax.set_ylim(y_range)
294            self.canvas.draw_idle()
[27313b7]295
[aadf0af1]296    def onDataInfo(self, plot_data):
297        """
298        Displays data info text window for the selected plot
299        """
300        text_to_show = GuiUtils.retrieveData1d(plot_data)
301        # Hardcoded sizes to enable full width rendering with default font
302        self.txt_widget.resize(420,600)
303
304        self.txt_widget.setReadOnly(True)
305        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
306        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
307        self.txt_widget.setWindowTitle("Data Info: %s" % plot_data.filename)
308        self.txt_widget.insertPlainText(text_to_show)
309
310        self.txt_widget.show()
311        # Move the slider all the way up, if present
312        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
313        vertical_scroll_bar.triggerAction(QtGui.QScrollBar.SliderToMinimum)
314
315    def onSavePoints(self, plot_data):
316        """
317        Saves plot data to a file
318        """
319        GuiUtils.saveData1D(plot_data)
320
321    def onLinearFit(self):
322        """
323        Creates and displays a simple linear fit for the selected plot
324        """
325        pass
326
327    def onRemovePlot(self, id):
328        """
329        Deletes the selected plot from the chart
330        """
331        selected_plot = self.plot_dict[id]
332
333        plot_dict = copy.deepcopy(self.plot_dict)
334
335        self.plot_dict = {}
336
337        plt.cla()
338        self.ax.cla()
339
340        for ids in plot_dict:
341            if ids != id:
342                self.plot(data=plot_dict[ids], hide_error=plot_dict[ids].hide_error)               
343
344        if len(self.plot_dict) == 0:
345            # last plot: graph is empty must be the panel must be destroyed
346                self.parent.close()
347
348    def onFreeze(self, id):
349        """
350        Freezes the selected plot to a separate chart
351        """
352        plot = self.plot_dict[id]
353        self.manager.add_data(data_list=[plot])
354
355    def onModifyPlot(self):
356        """
357        Allows for MPL modifications to the selected plot
358        """
359        pass
360
361    def onToggleHideError(self, id):
362        """
363        Toggles hide error/show error menu item
364        """
365        selected_plot = self.plot_dict[id]
366        current = selected_plot.hide_error
367
368        # Flip the flag
369        selected_plot.hide_error = not current
370
371        plot_dict = copy.deepcopy(self.plot_dict)
372        self.plot_dict = {}
373
374        # Clean the canvas
375        plt.cla()
376        self.ax.cla()
377
378        # Recreate the plots but reverse the error flag for the current
379        for ids in plot_dict:
380            if ids == id:
381                self.plot(data=plot_dict[ids], hide_error=(not current))
382            else:
383                self.plot(data=plot_dict[ids], hide_error=plot_dict[ids].hide_error)               
384
[6d05e1d]385    def xyTransform(self, xLabel="", yLabel=""):
386        """
387        Transforms x and y in View and set the scale
388        """
389        # Clear the plot first
390        plt.cla()
[9290b1a]391        self.ax.cla()
[6d05e1d]392
[aadf0af1]393        new_xlabel, new_ylabel, xscale, yscale = GuiUtils.xyTransform(self.data, xLabel, yLabel)
394        self.xscale = xscale
395        self.yscale = yscale
396        self.xLabel = new_xlabel
397        self.yLabel = new_ylabel
[6d05e1d]398
399        # Plot the updated chart
400        self.plot(marker='o', linestyle='')
[c4e5400]401
402
[416fa8f]403class Plotter(QtGui.QDialog, PlotterWidget):
404    def __init__(self, parent=None, quickplot=False):
405
406        QtGui.QDialog.__init__(self)
[aadf0af1]407        PlotterWidget.__init__(self, parent=self, manager=parent, quickplot=quickplot)
[c4e5400]408        icon = QtGui.QIcon()
409        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
410        self.setWindowIcon(icon)
411
[416fa8f]412
Note: See TracBrowser for help on using the repository browser.