source: sasview/src/sas/qtgui/Plotter.py @ 0f3c22d

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

Code review for SASVIEW-383

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