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

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

Unit tests for linear fit

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