source: sasview/src/sas/qtgui/Plotter.py @ 87cc73a

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

Added Modify Plot Properties functionality. SASVIEW-169

  • Property mode set to 100644
File size: 19.6 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.actionModifyGraphAppearance =\
169            self.contextMenu.addAction("Modify Graph Appearance")
170        self.contextMenu.addSeparator()
171        self.actionAddText = self.contextMenu.addAction("Add Text")
172        self.actionRemoveText = self.contextMenu.addAction("Remove Text")
173        self.contextMenu.addSeparator()
174        self.actionChangeScale = self.contextMenu.addAction("Change Scale")
175        self.contextMenu.addSeparator()
176        self.actionSetGraphRange = self.contextMenu.addAction("Set Graph Range")
177        self.actionResetGraphRange =\
178            self.contextMenu.addAction("Reset Graph Range")
179        # Add the title change for dialogs
180        #if self.parent:
181        self.contextMenu.addSeparator()
182        self.actionWindowTitle = self.contextMenu.addAction("Window Title")
183
184        # Define the callbacks
185        self.actionModifyGraphAppearance.triggered.connect(self.onModifyGraph)
186        self.actionAddText.triggered.connect(self.onAddText)
187        self.actionRemoveText.triggered.connect(self.onRemoveText)
188        self.actionChangeScale.triggered.connect(self.onScaleChange)
189        self.actionSetGraphRange.triggered.connect(self.onSetGraphRange)
190        self.actionResetGraphRange.triggered.connect(self.onResetGraphRange)
191        self.actionWindowTitle.triggered.connect(self.onWindowsTitle)
192
193    def addPlotsToContextMenu(self):
194        """
195        Adds operations on all plotted sets of data to the context menu
196        """
197        for id in self.plot_dict.keys():
198            plot = self.plot_dict[id]
199            #name = plot.name
200            name = plot.title
201            plot_menu = self.contextMenu.addMenu('&%s' % name)
202
203            self.actionDataInfo = plot_menu.addAction("&DataInfo")
204            self.actionDataInfo.triggered.connect(
205                                functools.partial(self.onDataInfo, plot))
206
207            self.actionSavePointsAsFile = plot_menu.addAction("&Save Points as a File")
208            self.actionSavePointsAsFile.triggered.connect(
209                                functools.partial(self.onSavePoints, plot))
210            plot_menu.addSeparator()
211
212            if plot.id != 'fit':
213                self.actionLinearFit = plot_menu.addAction('&Linear Fit')
214                self.actionLinearFit.triggered.connect(
215                                functools.partial(self.onLinearFit, id))
216                plot_menu.addSeparator()
217
218            self.actionRemovePlot = plot_menu.addAction("Remove")
219            self.actionRemovePlot.triggered.connect(
220                                functools.partial(self.onRemovePlot, id))
221
222            if not plot.is_data:
223                self.actionFreeze = plot_menu.addAction('&Freeze')
224                self.actionFreeze.triggered.connect(
225                                functools.partial(self.onFreeze, id))
226            plot_menu.addSeparator()
227
228            if plot.is_data:
229                self.actionHideError = plot_menu.addAction("Hide Error Bar")
230                if plot.dy is not None and plot.dy != []:
231                    if plot.hide_error:
232                        self.actionHideError.setText('Show Error Bar')
233                else:
234                    self.actionHideError.setEnabled(False)
235                self.actionHideError.triggered.connect(
236                                functools.partial(self.onToggleHideError, id))
237                plot_menu.addSeparator()
238
239            self.actionModifyPlot = plot_menu.addAction('&Modify Plot Property')
240            self.actionModifyPlot.triggered.connect(
241                                functools.partial(self.onModifyPlot, id))
242
243    def createContextMenuQuick(self):
244        """
245        Define context menu and associated actions for the quickplot MPL widget
246        """
247        # Default actions
248        self.defaultContextMenu()
249
250        # Additional actions
251        self.actionToggleGrid = self.contextMenu.addAction("Toggle Grid On/Off")
252        self.contextMenu.addSeparator()
253        self.actionChangeScale = self.contextMenu.addAction("Change Scale")
254
255        # Define the callbacks
256        self.actionToggleGrid.triggered.connect(self.onGridToggle)
257        self.actionChangeScale.triggered.connect(self.onScaleChange)
258
259    def onScaleChange(self):
260        """
261        Show a dialog allowing axes rescaling
262        """
263        if self.properties.exec_() == QtGui.QDialog.Accepted:
264            self.xLogLabel, self.yLogLabel = self.properties.getValues()
265            self.xyTransform(self.xLogLabel, self.yLogLabel)
266
267    def onModifyGraph(self):
268        """
269        Show a dialog allowing chart manipulations
270        """
271        print ("onModifyGraph")
272        pass
273
274    def onAddText(self):
275        """
276        Show a dialog allowing adding custom text to the chart
277        """
278        if self.addText.exec_() == QtGui.QDialog.Accepted:
279            # Retrieve the new text, its font and color
280            extra_text = self.addText.text()
281            extra_font = self.addText.font()
282            extra_color = self.addText.color()
283
284            # Place the text on the screen at (0,0)
285            pos_x = self.x_click
286            pos_y = self.y_click
287
288            # Map QFont onto MPL font
289            mpl_font = FontProperties()
290            mpl_font.set_size(int(extra_font.pointSize()))
291            mpl_font.set_family(str(extra_font.family()))
292            mpl_font.set_weight(int(extra_font.weight()))
293            # MPL style names
294            styles = ['normal', 'italic', 'oblique']
295            # QFont::Style maps directly onto the above
296            try:
297                mpl_font.set_style(styles[extra_font.style()])
298            except:
299                pass
300
301            if len(extra_text) > 0:
302                new_text = self.ax.text(str(pos_x),
303                                        str(pos_y),
304                                        extra_text,
305                                        color=extra_color,
306                                        fontproperties=mpl_font)
307                # Update the list of annotations
308                self.textList.append(new_text)
309                self.canvas.draw_idle()
310
311    def onRemoveText(self):
312        """
313        Remove the most recently added text
314        """
315        num_text = len(self.textList)
316        if num_text < 1:
317            return
318        txt = self.textList[num_text - 1]
319        text_remove = txt.get_text()
320        txt.remove()
321        self.textList.remove(txt)
322
323        self.canvas.draw_idle()
324
325    def onSetGraphRange(self):
326        """
327        Show a dialog allowing setting the chart ranges
328        """
329        # min and max of data
330        if self.setRange.exec_() == QtGui.QDialog.Accepted:
331            x_range = self.setRange.xrange()
332            y_range = self.setRange.yrange()
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 onResetGraphRange(self):
339        """
340        Resets the chart X and Y ranges to their original values
341        """
342        x_range = (self.data.x.min(), self.data.x.max())
343        y_range = (self.data.y.min(), self.data.y.max())
344        if x_range is not None and y_range is not None:
345            self.ax.set_xlim(x_range)
346            self.ax.set_ylim(y_range)
347            self.canvas.draw_idle()
348
349    def onDataInfo(self, plot_data):
350        """
351        Displays data info text window for the selected plot
352        """
353        text_to_show = GuiUtils.retrieveData1d(plot_data)
354        # Hardcoded sizes to enable full width rendering with default font
355        self.txt_widget.resize(420,600)
356
357        self.txt_widget.setReadOnly(True)
358        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
359        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
360        self.txt_widget.setWindowTitle("Data Info: %s" % plot_data.filename)
361        self.txt_widget.insertPlainText(text_to_show)
362
363        self.txt_widget.show()
364        # Move the slider all the way up, if present
365        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
366        vertical_scroll_bar.triggerAction(QtGui.QScrollBar.SliderToMinimum)
367
368    def onSavePoints(self, plot_data):
369        """
370        Saves plot data to a file
371        """
372        GuiUtils.saveData1D(plot_data)
373
374    def onLinearFit(self, id):
375        """
376        Creates and displays a simple linear fit for the selected plot
377        """
378        selected_plot = self.plot_dict[id]
379
380        maxrange = (min(selected_plot.x), max(selected_plot.x))
381        fitrange = self.ax.get_xlim()
382
383        fit_dialog = LinearFit(parent=self,
384                    data=selected_plot,
385                    max_range=maxrange,
386                    fit_range=fitrange,
387                    xlabel=self.xLogLabel,
388                    ylabel=self.yLogLabel)
389        if fit_dialog.exec_() == QtGui.QDialog.Accepted:
390            return
391
392    def replacePlot(self, id, new_plot):
393        """
394        Remove plot 'id' and add 'new_plot' to the chart.
395        This effectlvely refreshes the chart with changes to one of its plots
396        """
397        self.removePlot(id)
398        self.plot(data=new_plot)
399
400    def onRemovePlot(self, id):
401        """
402        Responds to the plot delete action
403        """
404        self.removePlot(id)
405
406        if len(self.plot_dict) == 0:
407            # last plot: graph is empty must be the panel must be destroyed
408                self.parent.close()
409
410    def removePlot(self, id):
411        """
412        Deletes the selected plot from the chart
413        """
414        if id not in self.plot_dict:
415            return
416
417        selected_plot = self.plot_dict[id]
418
419        plot_dict = copy.deepcopy(self.plot_dict)
420
421        # Labels might have been changed
422        xl = self.ax.xaxis.label.get_text()
423        yl = self.ax.yaxis.label.get_text()
424
425        self.plot_dict = {}
426
427        plt.cla()
428        self.ax.cla()
429
430        for ids in plot_dict:
431            if ids != id:
432                self.plot(data=plot_dict[ids], hide_error=plot_dict[ids].hide_error)
433
434        # Reset the labels
435        self.ax.set_xlabel(xl)
436        self.ax.set_ylabel(yl)
437        self.canvas.draw()
438
439    def onFreeze(self, id):
440        """
441        Freezes the selected plot to a separate chart
442        """
443        plot = self.plot_dict[id]
444        self.manager.add_data(data_list=[plot])
445
446    def onModifyPlot(self, id):
447        """
448        Allows for MPL modifications to the selected plot
449        """
450        selected_plot = self.plot_dict[id]
451        current = selected_plot.hide_error
452
453        # Old style color - single integer for enum color
454        # New style color - #hhhhhh
455        color = selected_plot.custom_color
456        # marker symbol and size
457        marker = selected_plot.symbol
458        marker_size = selected_plot.markersize
459        # plot name
460        legend = selected_plot.title
461
462        plotPropertiesWidget = PlotProperties(self,
463                                color=color,
464                                marker=marker,
465                                marker_size=marker_size,
466                                legend=legend)
467        if plotPropertiesWidget.exec_() == QtGui.QDialog.Accepted:
468            marker = plotPropertiesWidget.marker()
469            marker_size = plotPropertiesWidget.markersize()
470            color = plotPropertiesWidget.color()
471            legend = plotPropertiesWidget.legend()
472
473            # Update Data1d
474            selected_plot.markersize = marker_size
475            selected_plot.custom_color = color
476            selected_plot.symbol = marker
477            selected_plot.title = legend
478
479            # Redraw the plot
480            self.replacePlot(id, selected_plot)
481
482    def onToggleHideError(self, id):
483        """
484        Toggles hide error/show error menu item
485        """
486        selected_plot = self.plot_dict[id]
487        current = selected_plot.hide_error
488
489        # Flip the flag
490        selected_plot.hide_error = not current
491
492        plot_dict = copy.deepcopy(self.plot_dict)
493        self.plot_dict = {}
494
495        # Clean the canvas
496        plt.cla()
497        self.ax.cla()
498
499        # Recreate the plots but reverse the error flag for the current
500        for ids in plot_dict:
501            if ids == id:
502                self.plot(data=plot_dict[ids], hide_error=(not current))
503            else:
504                self.plot(data=plot_dict[ids], hide_error=plot_dict[ids].hide_error)               
505
506    def xyTransform(self, xLabel="", yLabel=""):
507        """
508        Transforms x and y in View and set the scale
509        """
510        # Transform all the plots on the chart
511        for id in self.plot_dict.keys():
512            current_plot = self.plot_dict[id]
513            if current_plot.id == "fit":
514                self.removePlot(id)
515                continue
516
517            new_xlabel, new_ylabel, xscale, yscale =\
518                GuiUtils.xyTransform(current_plot, xLabel, yLabel)
519            self.xscale = xscale
520            self.yscale = yscale
521
522            # Plot the updated chart
523            self.removePlot(id)
524
525            # This assignment will wrap the label in Latex "$"
526            self.xLabel = new_xlabel
527            self.yLabel = new_ylabel
528            # Directly overwrite the data to avoid label reassignment
529            self._data = current_plot
530            self.plot()
531
532        pass # debug hook
533
534    def onFitDisplay(self, fit_data):
535        """
536        Add a linear fitting line to the chart
537        """
538        # Create new data structure with fitting result
539        tempx = fit_data[0]
540        tempy = fit_data[1]
541        self.fit_result.x = []
542        self.fit_result.y = []
543        self.fit_result.x = tempx
544        self.fit_result.y = tempy
545        self.fit_result.dx = None
546        self.fit_result.dy = None
547
548        #Remove another Fit, if exists
549        self.removePlot("fit")
550
551        self.fit_result.reset_view()
552        #self.offset_graph()
553
554        # Set plot properties
555        self.fit_result.id = 'fit'
556        self.fit_result.title = 'Fit'
557        self.fit_result.name = 'Fit'
558
559        # Plot the line
560        self.plot(data=self.fit_result, marker='-', hide_error=True)
561
562
563class Plotter(QtGui.QDialog, PlotterWidget):
564    def __init__(self, parent=None, quickplot=False):
565
566        QtGui.QDialog.__init__(self)
567        PlotterWidget.__init__(self, parent=self, manager=parent, quickplot=quickplot)
568        icon = QtGui.QIcon()
569        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
570        self.setWindowIcon(icon)
571
572
Note: See TracBrowser for help on using the repository browser.