source: sasview/src/sas/qtgui/Plotter.py @ 6fd4e36

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

Chi2 display + minor refactoring

  • Property mode set to 100644
File size: 24.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        # 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, color=None, marker=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        # Shortcuts
73        ax = self.ax
74        x = self._data.view.x
75        y = self._data.view.y
76
77        # Marker symbol. Passed marker is one of matplotlib.markers characters
78        # Alternatively, picked up from Data1D as an int index of PlotUtilities.SHAPES dict
79        if marker is None:
80            marker = self.data.symbol
81            # Try name first
82            try:
83                marker = PlotUtilities.SHAPES[marker]
84            except KeyError:
85                marker = PlotUtilities.SHAPES.values()[marker]
86
87        assert marker is not None
88        # Plot name
89        if self.data.title:
90            self.title(title=self.data.title)
91        else:
92            self.title(title=self.data.name)
93
94        # Error marker toggle
95        if hide_error is None:
96            hide_error = self.data.hide_error
97
98        # Plot color
99        if color is None:
100            color = self.data.custom_color
101
102        color = PlotUtilities.getValidColor(color)
103
104        markersize = self._data.markersize
105
106        # Draw non-standard markers
107        l_width = markersize * 0.4
108        if marker == '-' or marker == '--':
109            line = self.ax.plot(x, y, color=color, lw=l_width, marker='',
110                             linestyle=marker, label=self._title, zorder=10)[0]
111
112        elif marker == 'vline':
113            y_min = min(y)*9.0/10.0 if min(y) < 0 else 0.0
114            line = self.ax.vlines(x=x, ymin=y_min, ymax=y, color=color,
115                            linestyle='-', label=self._title, lw=l_width, zorder=1)
116
117        elif marker == 'step':
118            line = self.ax.step(x, y, color=color, marker='', linestyle='-',
119                                label=self._title, lw=l_width, zorder=1)[0]
120
121        else:
122            # plot data with/without errorbars
123            if hide_error:
124                line = ax.plot(x, y, marker=marker, color=color, markersize=markersize,
125                        linestyle='', label=self._title, picker=True)
126            else:
127                line = ax.errorbar(x, y,
128                            yerr=self._data.view.dy, xerr=None,
129                            capsize=2, linestyle='',
130                            barsabove=False,
131                            color=color,
132                            marker=marker,
133                            markersize=markersize,
134                            lolims=False, uplims=False,
135                            xlolims=False, xuplims=False,
136                            label=self._title,
137                            picker=True)
138
139        # Update the list of data sets (plots) in chart
140        self.plot_dict[self._data.id] = self.data
141
142        # Now add the legend with some customizations.
143        self.legend = ax.legend(loc='upper right', shadow=True)
144        if self.legend:
145            self.legend.set_picker(True)
146
147        # Current labels for axes
148        if self.y_label and not is_fit:
149            ax.set_ylabel(self.y_label)
150        if self.x_label and not is_fit:
151            ax.set_xlabel(self.x_label)
152
153        # Include scaling (log vs. linear)
154        ax.set_xscale(self.xscale)
155        ax.set_yscale(self.yscale)
156
157        # define the ranges
158        self.setRange = SetGraphRange(parent=self,
159            x_range=self.ax.get_xlim(), y_range=self.ax.get_ylim())
160
161        # refresh canvas
162        self.canvas.draw()
163
164    def createContextMenu(self):
165        """
166        Define common context menu and associated actions for the MPL widget
167        """
168        self.defaultContextMenu()
169
170        # Separate plots
171        self.addPlotsToContextMenu()
172
173        # Additional menu items
174        self.contextMenu.addSeparator()
175        self.actionAddText = self.contextMenu.addAction("Add Text")
176        self.actionRemoveText = self.contextMenu.addAction("Remove Text")
177        self.contextMenu.addSeparator()
178        self.actionChangeScale = self.contextMenu.addAction("Change Scale")
179        self.contextMenu.addSeparator()
180        self.actionSetGraphRange = self.contextMenu.addAction("Set Graph Range")
181        self.actionResetGraphRange =\
182            self.contextMenu.addAction("Reset Graph Range")
183        # Add the title change for dialogs
184        #if self.parent:
185        self.contextMenu.addSeparator()
186        self.actionWindowTitle = self.contextMenu.addAction("Window Title")
187
188        # Define the callbacks
189        self.actionAddText.triggered.connect(self.onAddText)
190        self.actionRemoveText.triggered.connect(self.onRemoveText)
191        self.actionChangeScale.triggered.connect(self.onScaleChange)
192        self.actionSetGraphRange.triggered.connect(self.onSetGraphRange)
193        self.actionResetGraphRange.triggered.connect(self.onResetGraphRange)
194        self.actionWindowTitle.triggered.connect(self.onWindowsTitle)
195
196    def addPlotsToContextMenu(self):
197        """
198        Adds operations on all plotted sets of data to the context menu
199        """
200        for id in self.plot_dict.keys():
201            plot = self.plot_dict[id]
202
203            name = plot.name if plot.name else plot.title
204            plot_menu = self.contextMenu.addMenu('&%s' % name)
205
206            self.actionDataInfo = plot_menu.addAction("&DataInfo")
207            self.actionDataInfo.triggered.connect(
208                                functools.partial(self.onDataInfo, plot))
209
210            self.actionSavePointsAsFile = plot_menu.addAction("&Save Points as a File")
211            self.actionSavePointsAsFile.triggered.connect(
212                                functools.partial(self.onSavePoints, plot))
213            plot_menu.addSeparator()
214
215            if plot.id != 'fit':
216                self.actionLinearFit = plot_menu.addAction('&Linear Fit')
217                self.actionLinearFit.triggered.connect(
218                                functools.partial(self.onLinearFit, id))
219                plot_menu.addSeparator()
220
221            self.actionRemovePlot = plot_menu.addAction("Remove")
222            self.actionRemovePlot.triggered.connect(
223                                functools.partial(self.onRemovePlot, id))
224
225            if not plot.is_data:
226                self.actionFreeze = plot_menu.addAction('&Freeze')
227                self.actionFreeze.triggered.connect(
228                                functools.partial(self.onFreeze, id))
229            plot_menu.addSeparator()
230
231            if plot.is_data:
232                self.actionHideError = plot_menu.addAction("Hide Error Bar")
233                if plot.dy is not None and plot.dy != []:
234                    if plot.hide_error:
235                        self.actionHideError.setText('Show Error Bar')
236                else:
237                    self.actionHideError.setEnabled(False)
238                self.actionHideError.triggered.connect(
239                                functools.partial(self.onToggleHideError, id))
240                plot_menu.addSeparator()
241
242            self.actionModifyPlot = plot_menu.addAction('&Modify Plot Property')
243            self.actionModifyPlot.triggered.connect(
244                                functools.partial(self.onModifyPlot, id))
245
246    def createContextMenuQuick(self):
247        """
248        Define context menu and associated actions for the quickplot MPL widget
249        """
250        # Default actions
251        self.defaultContextMenu()
252
253        # Additional actions
254        self.actionToggleGrid = self.contextMenu.addAction("Toggle Grid On/Off")
255        self.contextMenu.addSeparator()
256        self.actionChangeScale = self.contextMenu.addAction("Change Scale")
257
258        # Define the callbacks
259        self.actionToggleGrid.triggered.connect(self.onGridToggle)
260        self.actionChangeScale.triggered.connect(self.onScaleChange)
261
262    def onScaleChange(self):
263        """
264        Show a dialog allowing axes rescaling
265        """
266        if self.properties.exec_() == QtGui.QDialog.Accepted:
267            self.xLogLabel, self.yLogLabel = self.properties.getValues()
268            self.xyTransform(self.xLogLabel, self.yLogLabel)
269
270    def onAddText(self):
271        """
272        Show a dialog allowing adding custom text to the chart
273        """
274        if self.addText.exec_() == QtGui.QDialog.Accepted:
275            # Retrieve the new text, its font and color
276            extra_text = self.addText.text()
277            extra_font = self.addText.font()
278            extra_color = self.addText.color()
279
280            # Place the text on the screen at (0,0)
281            pos_x = self.x_click
282            pos_y = self.y_click
283
284            # Map QFont onto MPL font
285            mpl_font = FontProperties()
286            mpl_font.set_size(int(extra_font.pointSize()))
287            mpl_font.set_family(str(extra_font.family()))
288            mpl_font.set_weight(int(extra_font.weight()))
289            # MPL style names
290            styles = ['normal', 'italic', 'oblique']
291            # QFont::Style maps directly onto the above
292            try:
293                mpl_font.set_style(styles[extra_font.style()])
294            except:
295                pass
296
297            if len(extra_text) > 0:
298                new_text = self.ax.text(str(pos_x),
299                                        str(pos_y),
300                                        extra_text,
301                                        color=extra_color,
302                                        fontproperties=mpl_font)
303                # Update the list of annotations
304                self.textList.append(new_text)
305                self.canvas.draw_idle()
306
307    def onRemoveText(self):
308        """
309        Remove the most recently added text
310        """
311        num_text = len(self.textList)
312        if num_text < 1:
313            return
314        txt = self.textList[num_text - 1]
315        text_remove = txt.get_text()
316        txt.remove()
317        self.textList.remove(txt)
318
319        self.canvas.draw_idle()
320
321    def onSetGraphRange(self):
322        """
323        Show a dialog allowing setting the chart ranges
324        """
325        # min and max of data
326        if self.setRange.exec_() == QtGui.QDialog.Accepted:
327            x_range = self.setRange.xrange()
328            y_range = self.setRange.yrange()
329            if x_range is not None and y_range is not None:
330                self.ax.set_xlim(x_range)
331                self.ax.set_ylim(y_range)
332                self.canvas.draw_idle()
333
334    def onResetGraphRange(self):
335        """
336        Resets the chart X and Y ranges to their original values
337        """
338        x_range = (self.data.x.min(), self.data.x.max())
339        y_range = (self.data.y.min(), self.data.y.max())
340        if x_range is not None and y_range is not None:
341            self.ax.set_xlim(x_range)
342            self.ax.set_ylim(y_range)
343            self.canvas.draw_idle()
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 replacePlot(self, id, new_plot):
364        """
365        Remove plot 'id' and add 'new_plot' to the chart.
366        This effectlvely refreshes the chart with changes to one of its plots
367        """
368        self.removePlot(id)
369        self.plot(data=new_plot)
370
371    def onRemovePlot(self, id):
372        """
373        Responds to the plot delete action
374        """
375        self.removePlot(id)
376
377        if len(self.plot_dict) == 0:
378            # last plot: graph is empty must be the panel must be destroyed
379                self.parent.close()
380
381    def removePlot(self, id):
382        """
383        Deletes the selected plot from the chart
384        """
385        if id not in self.plot_dict:
386            return
387
388        selected_plot = self.plot_dict[id]
389
390        plot_dict = copy.deepcopy(self.plot_dict)
391
392        # Labels might have been changed
393        xl = self.ax.xaxis.label.get_text()
394        yl = self.ax.yaxis.label.get_text()
395
396        self.plot_dict = {}
397
398        plt.cla()
399        self.ax.cla()
400
401        for ids in plot_dict:
402            if ids != id:
403                self.plot(data=plot_dict[ids], hide_error=plot_dict[ids].hide_error)
404
405        # Reset the labels
406        self.ax.set_xlabel(xl)
407        self.ax.set_ylabel(yl)
408        self.canvas.draw()
409
410    def onFreeze(self, id):
411        """
412        Freezes the selected plot to a separate chart
413        """
414        plot = self.plot_dict[id]
415        self.manager.add_data(data_list=[plot])
416
417    def onModifyPlot(self, id):
418        """
419        Allows for MPL modifications to the selected plot
420        """
421        selected_plot = self.plot_dict[id]
422
423        # Old style color - single integer for enum color
424        # New style color - #hhhhhh
425        color = selected_plot.custom_color
426        # marker symbol and size
427        marker = selected_plot.symbol
428        marker_size = selected_plot.markersize
429        # plot name
430        legend = selected_plot.title
431
432        plotPropertiesWidget = PlotProperties(self,
433                                color=color,
434                                marker=marker,
435                                marker_size=marker_size,
436                                legend=legend)
437        if plotPropertiesWidget.exec_() == QtGui.QDialog.Accepted:
438            # Update Data1d
439            selected_plot.markersize = plotPropertiesWidget.markersize()
440            selected_plot.custom_color = plotPropertiesWidget.color()
441            selected_plot.symbol = plotPropertiesWidget.marker()
442            selected_plot.title = plotPropertiesWidget.legend()
443
444            # Redraw the plot
445            self.replacePlot(id, selected_plot)
446
447    def onToggleHideError(self, id):
448        """
449        Toggles hide error/show error menu item
450        """
451        selected_plot = self.plot_dict[id]
452        current = selected_plot.hide_error
453
454        # Flip the flag
455        selected_plot.hide_error = not current
456
457        plot_dict = copy.deepcopy(self.plot_dict)
458        self.plot_dict = {}
459
460        # Clean the canvas
461        plt.cla()
462        self.ax.cla()
463
464        # Recreate the plots but reverse the error flag for the current
465        for ids in plot_dict:
466            if ids == id:
467                self.plot(data=plot_dict[ids], hide_error=(not current))
468            else:
469                self.plot(data=plot_dict[ids], hide_error=plot_dict[ids].hide_error)               
470
471    def xyTransform(self, xLabel="", yLabel=""):
472        """
473        Transforms x and y in View and set the scale
474        """
475        # Transform all the plots on the chart
476        for id in self.plot_dict.keys():
477            current_plot = self.plot_dict[id]
478            if current_plot.id == "fit":
479                self.removePlot(id)
480                continue
481
482            new_xlabel, new_ylabel, xscale, yscale =\
483                GuiUtils.xyTransform(current_plot, xLabel, yLabel)
484            self.xscale = xscale
485            self.yscale = yscale
486
487            # Plot the updated chart
488            self.removePlot(id)
489
490            # This assignment will wrap the label in Latex "$"
491            self.xLabel = new_xlabel
492            self.yLabel = new_ylabel
493            # Directly overwrite the data to avoid label reassignment
494            self._data = current_plot
495            self.plot()
496
497        pass # debug hook
498
499    def onFitDisplay(self, fit_data):
500        """
501        Add a linear fitting line to the chart
502        """
503        # Create new data structure with fitting result
504        tempx = fit_data[0]
505        tempy = fit_data[1]
506        self.fit_result.x = []
507        self.fit_result.y = []
508        self.fit_result.x = tempx
509        self.fit_result.y = tempy
510        self.fit_result.dx = None
511        self.fit_result.dy = None
512
513        #Remove another Fit, if exists
514        self.removePlot("fit")
515
516        self.fit_result.reset_view()
517        #self.offset_graph()
518
519        # Set plot properties
520        self.fit_result.id = 'fit'
521        self.fit_result.title = 'Fit'
522        self.fit_result.name = 'Fit'
523
524        # Plot the line
525        self.plot(data=self.fit_result, marker='-', hide_error=True)
526
527    def onMplMouseDown(self, event):
528        """
529        Left button down and ready to drag
530        """
531        # Check that the LEFT button was pressed
532        if event.button == 1:
533            self.leftdown = True
534            ax = event.inaxes
535            for text in self.textList:
536                if text.contains(event)[0]: # If user has clicked on text
537                    self.selectedText = text
538                    return
539
540            if ax != None:
541                self.xInit, self.yInit = event.xdata, event.ydata
542                try:
543                    self.x_click = float(event.xdata)  # / size_x
544                    self.y_click = float(event.ydata)  # / size_y
545                except:
546                    self.position = None
547
548    def onMplMouseUp(self, event):
549        """
550        Set the data coordinates of the click
551        """
552        self.x_click = event.xdata
553        self.y_click = event.ydata
554
555        # Check that the LEFT button was released
556        if event.button == 1:
557            self.leftdown = False
558            #self.leftup = True
559            self.selectedText = None
560
561        #release the legend
562        if self.gotLegend == 1:
563            self.gotLegend = 0
564
565    def onMplMouseMotion(self, event):
566        """
567        Check if the left button is press and the mouse in moving.
568        Compute delta for x and y coordinates and then perform the drag
569        """
570        if self.gotLegend == 1 and self.leftdown:
571            self.onLegendMotion(event)
572            return
573
574        if self.leftdown and self.selectedText is not None:
575            # User has clicked on text and is dragging
576            ax = event.inaxes
577            if ax != None:
578                # Only move text if mouse is within axes
579                self.selectedText.set_position((event.xdata, event.ydata))
580                self.canvas.draw_idle()
581            else:
582                # User has dragged outside of axes
583                self.selectedText = None
584            return
585
586    def onMplPick(self, event):
587        """
588        On pick legend
589        """
590        legend = self.legend
591        if event.artist == legend:
592            # Get the box of the legend.
593            bbox = self.legend.get_window_extent()
594            # Get mouse coordinates at time of pick.
595            self.mouse_x = event.mouseevent.x
596            self.mouse_y = event.mouseevent.y
597            # Get legend coordinates at time of pick.
598            self.legend_x = bbox.xmin
599            self.legend_y = bbox.ymin
600            # Indicate we picked up the legend.
601            self.gotLegend = 1
602
603            #self.legend.legendPatch.set_alpha(0.5)
604
605    def onLegendMotion(self, event):
606        """
607        On legend in motion
608        """
609        ax = event.inaxes
610        if ax == None:
611            return
612        # Event occurred inside a plotting area
613        lo_x, hi_x = ax.get_xlim()
614        lo_y, hi_y = ax.get_ylim()
615        # How much the mouse moved.
616        x = mouse_diff_x = self.mouse_x - event.x
617        y = mouse_diff_y = self.mouse_y - event.y
618        # Put back inside
619        if x < lo_x:
620            x = lo_x
621        if x > hi_x:
622            x = hi_x
623        if y < lo_y:
624            y = lo_y
625        if y > hi_y:
626            y = hi_y
627        # Move the legend from its previous location by that same amount
628        loc_in_canvas = self.legend_x - mouse_diff_x, \
629                        self.legend_y - mouse_diff_y
630        # Transform into legend coordinate system
631        trans_axes = self.legend.parent.transAxes.inverted()
632        loc_in_norm_axes = trans_axes.transform_point(loc_in_canvas)
633        self.legend_pos_loc = tuple(loc_in_norm_axes)
634        self.legend._loc = self.legend_pos_loc
635        # self.canvas.draw()
636        self.canvas.draw_idle()
637
638    def onMplWheel(self, event):
639        """
640        Process mouse wheel as zoom events
641        """
642        ax = event.inaxes
643        step = event.step
644
645        if ax != None:
646            # Event occurred inside a plotting area
647            lo, hi = ax.get_xlim()
648            lo, hi = PlotUtilities.rescale(lo, hi, step,
649                              pt=event.xdata, scale=ax.get_xscale())
650            if not self.xscale == 'log' or lo > 0:
651                self._scale_xlo = lo
652                self._scale_xhi = hi
653                ax.set_xlim((lo, hi))
654
655            lo, hi = ax.get_ylim()
656            lo, hi = PlotUtilities.rescale(lo, hi, step, pt=event.ydata,
657                              scale=ax.get_yscale())
658            if not self.yscale == 'log' or lo > 0:
659                self._scale_ylo = lo
660                self._scale_yhi = hi
661                ax.set_ylim((lo, hi))
662        else:
663            # Check if zoom happens in the axes
664            xdata, ydata = None, None
665            x, y = event.x, event.y
666
667            for ax in self.axes:
668                insidex, _ = ax.xaxis.contains(event)
669                if insidex:
670                    xdata, _ = ax.transAxes.inverted().transform_point((x, y))
671                insidey, _ = ax.yaxis.contains(event)
672                if insidey:
673                    _, ydata = ax.transAxes.inverted().transform_point((x, y))
674            if xdata is not None:
675                lo, hi = ax.get_xlim()
676                lo, hi = PlotUtilities.rescale(lo, hi, step,
677                                  bal=xdata, scale=ax.get_xscale())
678                if not self.xscale == 'log' or lo > 0:
679                    self._scale_xlo = lo
680                    self._scale_xhi = hi
681                    ax.set_xlim((lo, hi))
682            if ydata is not None:
683                lo, hi = ax.get_ylim()
684                lo, hi = PlotUtilities.rescale(lo, hi, step, bal=ydata,
685                                  scale=ax.get_yscale())
686                if not self.yscale == 'log' or lo > 0:
687                    self._scale_ylo = lo
688                    self._scale_yhi = hi
689                    ax.set_ylim((lo, hi))
690        self.canvas.draw_idle()
691
692
693class Plotter(QtGui.QDialog, PlotterWidget):
694    def __init__(self, parent=None, quickplot=False):
695
696        QtGui.QDialog.__init__(self)
697        PlotterWidget.__init__(self, parent=self, manager=parent, quickplot=quickplot)
698        icon = QtGui.QIcon()
699        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
700        self.setWindowIcon(icon)
701
702
Note: See TracBrowser for help on using the repository browser.