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
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
15from sas.qtgui.PlotProperties import PlotProperties
16import sas.qtgui.PlotUtilities as PlotUtilities
17
18class PlotterWidget(PlotterBase):
19    """
20    1D Plot widget for use with a QDialog
21    """
22    def __init__(self, parent=None, manager=None, quickplot=False):
23        super(PlotterWidget, self).__init__(parent, manager=manager, quickplot=quickplot)
24
25        self.parent = parent
26
27        # Dictionary of {plot_id:Data1d}
28        self.plot_dict = {}
29
30        # Simple window for data display
31        self.txt_widget = QtGui.QTextEdit(None)
32        # Window for text add
33        self.addText = AddText(self)
34
35        # Log-ness of the axes
36        self.xLogLabel = "log10(x)"
37        self.yLogLabel = "log10(y)"
38
39        # Data container for the linear fit
40        self.fit_result = Data1D(x=[], y=[], dy=None)
41        self.fit_result.symbol = 13
42        self.fit_result.name = "Fit"
43
44        # Add a slot for receiving update signal from LinearFit
45        # NEW style signals
46        #self.updatePlot = QtCore.pyqtSignal(tuple)
47        # self.updatePlot.connect(self.onFitDisplay)
48        # OLD style signals
49        QtCore.QObject.connect(self, QtCore.SIGNAL('updatePlot'), self.onFitDisplay)
50
51    @property
52    def data(self):
53        return self._data
54
55    @data.setter
56    def data(self, value):
57        """ data setter """
58        self._data = value
59        self.xLabel = "%s(%s)"%(value._xaxis, value._xunit)
60        self.yLabel = "%s(%s)"%(value._yaxis, value._yunit)
61        self.title(title=value.name)
62
63    def plot(self, data=None, color=None, marker=None, hide_error=False):
64        """
65        Add a new plot of self._data to the chart.
66        """
67        # Data1D
68        if isinstance(data, Data1D):
69            self.data = data
70        assert(self._data)
71
72        is_fit = (self.data.id=="fit")
73
74        # Shortcuts
75        ax = self.ax
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
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]
114
115        else:
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)
132
133        # Update the list of data sets (plots) in chart
134        self.plot_dict[self._data.id] = self.data
135
136        # Now add the legend with some customizations.
137        self.legend = ax.legend(loc='upper right', shadow=True)
138        self.legend.set_picker(True)
139
140        # Current labels for axes
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)
145
146        # Include scaling (log vs. linear)
147        ax.set_xscale(self.xscale)
148        ax.set_yscale(self.yscale)
149
150        # define the ranges
151        self.setRange = SetGraphRange(parent=self,
152            x_range=self.ax.get_xlim(), y_range=self.ax.get_ylim())
153
154        # refresh canvas
155        self.canvas.draw()
156
157    def createContextMenu(self):
158        """
159        Define common context menu and associated actions for the MPL widget
160        """
161        self.defaultContextMenu()
162
163        # Separate plots
164        self.addPlotsToContextMenu()
165
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
177        #if self.parent:
178        self.contextMenu.addSeparator()
179        self.actionWindowTitle = self.contextMenu.addAction("Window Title")
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)
188
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]
195            #name = plot.name
196            name = plot.title
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')
210                self.actionLinearFit.triggered.connect(
211                                functools.partial(self.onLinearFit, id))
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')
236            self.actionModifyPlot.triggered.connect(
237                                functools.partial(self.onModifyPlot, id))
238
239    def createContextMenuQuick(self):
240        """
241        Define context menu and associated actions for the quickplot MPL widget
242        """
243        # Default actions
244        self.defaultContextMenu()
245
246        # Additional actions
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:
260            self.xLogLabel, self.yLogLabel = self.properties.getValues()
261            self.xyTransform(self.xLogLabel, self.yLogLabel)
262
263    def onAddText(self):
264        """
265        Show a dialog allowing adding custom text to the chart
266        """
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()
299
300    def onRemoveText(self):
301        """
302        Remove the most recently added text
303        """
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()
313
314    def onSetGraphRange(self):
315        """
316        Show a dialog allowing setting the chart ranges
317        """
318        # min and max of data
319        if self.setRange.exec_() == QtGui.QDialog.Accepted:
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()
326
327    def onResetGraphRange(self):
328        """
329        Resets the chart X and Y ranges to their original values
330        """
331        x_range = (self.data.x.min(), self.data.x.max())
332        y_range = (self.data.y.min(), self.data.y.max())
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()
337
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
363    def onLinearFit(self, id):
364        """
365        Creates and displays a simple linear fit for the selected plot
366        """
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
380
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
389    def onRemovePlot(self, id):
390        """
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        """
401        Deletes the selected plot from the chart
402        """
403        if id not in self.plot_dict:
404            return
405
406        selected_plot = self.plot_dict[id]
407
408        plot_dict = copy.deepcopy(self.plot_dict)
409
410        # Labels might have been changed
411        xl = self.ax.xaxis.label.get_text()
412        yl = self.ax.yaxis.label.get_text()
413
414        self.plot_dict = {}
415
416        plt.cla()
417        self.ax.cla()
418
419        for ids in plot_dict:
420            if ids != id:
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()
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
435    def onModifyPlot(self, id):
436        """
437        Allows for MPL modifications to the selected plot
438        """
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
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()
461
462            # Redraw the plot
463            self.replacePlot(id, selected_plot)
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
489    def xyTransform(self, xLabel="", yLabel=""):
490        """
491        Transforms x and y in View and set the scale
492        """
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
499
500            new_xlabel, new_ylabel, xscale, yscale =\
501                GuiUtils.xyTransform(current_plot, xLabel, yLabel)
502            self.xscale = xscale
503            self.yscale = yscale
504
505            # Plot the updated chart
506            self.removePlot(id)
507
508            # This assignment will wrap the label in Latex "$"
509            self.xLabel = new_xlabel
510            self.yLabel = new_ylabel
511            # Directly overwrite the data to avoid label reassignment
512            self._data = current_plot
513            self.plot()
514
515        pass # debug hook
516
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
543        self.plot(data=self.fit_result, marker='-', hide_error=True)
544
545
546class Plotter(QtGui.QDialog, PlotterWidget):
547    def __init__(self, parent=None, quickplot=False):
548
549        QtGui.QDialog.__init__(self)
550        PlotterWidget.__init__(self, parent=self, manager=parent, quickplot=quickplot)
551        icon = QtGui.QIcon()
552        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
553        self.setWindowIcon(icon)
554
555
Note: See TracBrowser for help on using the repository browser.