source: sasview/src/sas/qtgui/Plotter.py @ 570a58f9

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

Linear fits for 1D charts

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