source: sasview/src/sas/qtgui/Plotting/Plotter2D.py @ e20870b

ESS_GUIESS_GUI_Pr_fixesESS_GUI_iss879ESS_GUI_iss959ESS_GUI_project_save
Last change on this file since e20870b was e20870b, checked in by Piotr Rozyczko <rozyczko@…>, 5 months ago

Masking dialog for fitting

  • Property mode set to 100644
File size: 18.5 KB
Line 
1import copy
2import numpy
3import pylab
4import functools
5import logging
6
7from PyQt5 import QtCore
8from PyQt5 import QtGui
9from PyQt5 import QtWidgets
10
11DEFAULT_CMAP = pylab.cm.jet
12
13#import sys
14#print("SYS.PATH = ", sys.path)
15import matplotlib as mpl
16mpl.use("Qt5Agg")
17
18from mpl_toolkits.mplot3d import Axes3D
19
20from sas.sascalc.dataloader.manipulations import CircularAverage
21
22from sas.qtgui.Plotting.PlotterData import Data1D
23from sas.qtgui.Plotting.PlotterData import Data2D
24
25import sas.qtgui.Plotting.PlotUtilities as PlotUtilities
26import sas.qtgui.Utilities.GuiUtils as GuiUtils
27from sas.qtgui.Plotting.PlotterBase import PlotterBase
28from sas.qtgui.Plotting.ColorMap import ColorMap
29from sas.qtgui.Plotting.BoxSum import BoxSum
30from sas.qtgui.Plotting.SlicerParameters import SlicerParameters
31from sas.qtgui.Plotting.Binder import BindArtist
32
33# TODO: move to sas.qtgui namespace
34from sas.qtgui.Plotting.Slicers.BoxSlicer import BoxInteractorX
35from sas.qtgui.Plotting.Slicers.BoxSlicer import BoxInteractorY
36from sas.qtgui.Plotting.Slicers.AnnulusSlicer import AnnulusInteractor
37from sas.qtgui.Plotting.Slicers.SectorSlicer import SectorInteractor
38from sas.qtgui.Plotting.Slicers.BoxSum import BoxSumCalculator
39
40# Minimum value of Z for which we will present data.
41MIN_Z = -32
42
43class Plotter2DWidget(PlotterBase):
44    """
45    2D Plot widget for use with a QDialog
46    """
47    def __init__(self, parent=None, manager=None, quickplot=False, dimension=2):
48        self.dimension = dimension
49        super(Plotter2DWidget, self).__init__(parent, manager=manager, quickplot=quickplot)
50
51        self.cmap = DEFAULT_CMAP.name
52        # Default scale
53        self.scale = 'log_{10}'
54        # to set the order of lines drawn first.
55        self.slicer_z = 5
56        # Reference to the current slicer
57        self.slicer = None
58        self.slicer_widget = None
59        # Create Artist and bind it
60        self.connect = BindArtist(self.figure)
61        self.vmin = None
62        self.vmax = None
63        self.im = None
64
65        self.manager = manager
66
67    @property
68    def data(self):
69        return self._data
70
71    @data.setter
72    def data(self, data=None):
73        """ data setter """
74        self._data = data
75        self.qx_data = data.qx_data
76        self.qy_data = data.qy_data
77        self.xmin = data.xmin
78        self.xmax = data.xmax
79        self.ymin = data.ymin
80        self.ymax = data.ymax
81        self.zmin = data.zmin
82        self.zmax = data.zmax
83        self.label = data.name
84        self.xLabel = "%s(%s)"%(data._xaxis, data._xunit)
85        self.yLabel = "%s(%s)"%(data._yaxis, data._yunit)
86        self.title(title=data.title)
87
88    @property
89    def item(self):
90        ''' getter for this plot's QStandardItem '''
91        return self._item
92
93    @item.setter
94    def item(self, item=None):
95        ''' setter for this plot's QStandardItem '''
96        self._item = item
97
98    def plot(self, data=None, marker=None, show_colorbar=True):
99        """
100        Plot 2D self._data
101        marker - unused
102        """
103        # Assing data
104        if isinstance(data, Data2D):
105            self.data = data
106
107        assert self._data
108
109        # Toggle the scale
110        zmin_2D_temp, zmax_2D_temp = self.calculateDepth()
111
112        # Prepare and show the plot
113        self.showPlot(data=self.data.data,
114                      qx_data=self.qx_data,
115                      qy_data=self.qy_data,
116                      xmin=self.xmin,
117                      xmax=self.xmax,
118                      ymin=self.ymin, ymax=self.ymax,
119                      cmap=self.cmap, zmin=zmin_2D_temp,
120                      zmax=zmax_2D_temp, show_colorbar=show_colorbar)
121
122    def calculateDepth(self):
123        """
124        Re-calculate the plot depth parameters depending on the scale
125        """
126        # Toggle the scale
127        zmin_temp = self.zmin if self.zmin else MIN_Z
128        zmax_temp = self.zmax
129        # self.scale predefined in the baseclass
130        # in numpy > 1.12 power(int, -int) raises ValueException
131        # "Integers to negative integer powers are not allowed."
132        if self.scale == 'log_{10}':
133            if self.zmin is not None:
134                zmin_temp = numpy.power(10.0, self.zmin)
135            if self.zmax is not None:
136                zmax_temp = numpy.power(10.0, self.zmax)
137        else:
138            if self.zmin is not None:
139                # min log value: no log(negative)
140                zmin_temp = MIN_Z if self.zmin <= 0 else numpy.log10(self.zmin)
141            if self.zmax is not None:
142                zmax_temp = numpy.log10(self.zmax)
143
144        return (zmin_temp, zmax_temp)
145
146
147    def createContextMenu(self):
148        """
149        Define common context menu and associated actions for the MPL widget
150        """
151        self.defaultContextMenu()
152
153        self.contextMenu.addSeparator()
154        self.actionDataInfo = self.contextMenu.addAction("&DataInfo")
155        self.actionDataInfo.triggered.connect(
156             functools.partial(self.onDataInfo, self.data))
157
158        self.actionSavePointsAsFile = self.contextMenu.addAction("&Save Points as a File")
159        self.actionSavePointsAsFile.triggered.connect(
160             functools.partial(self.onSavePoints, self.data))
161        self.contextMenu.addSeparator()
162
163        self.actionCircularAverage = self.contextMenu.addAction("&Perform Circular Average")
164        self.actionCircularAverage.triggered.connect(self.onCircularAverage)
165
166        self.actionSectorView = self.contextMenu.addAction("&Sector [Q View]")
167        self.actionSectorView.triggered.connect(self.onSectorView)
168        self.actionAnnulusView = self.contextMenu.addAction("&Annulus [Phi View]")
169        self.actionAnnulusView.triggered.connect(self.onAnnulusView)
170        self.actionBoxSum = self.contextMenu.addAction("&Box Sum")
171        self.actionBoxSum.triggered.connect(self.onBoxSum)
172        self.actionBoxAveragingX = self.contextMenu.addAction("&Box Averaging in Qx")
173        self.actionBoxAveragingX.triggered.connect(self.onBoxAveragingX)
174        self.actionBoxAveragingY = self.contextMenu.addAction("&Box Averaging in Qy")
175        self.actionBoxAveragingY.triggered.connect(self.onBoxAveragingY)
176        # Additional items for slicer interaction
177        if self.slicer:
178            self.actionClearSlicer = self.contextMenu.addAction("&Clear Slicer")
179            self.actionClearSlicer.triggered.connect(self.onClearSlicer)
180            if self.slicer.__class__.__name__ != "BoxSumCalculator":
181                self.actionEditSlicer = self.contextMenu.addAction("&Edit Slicer Parameters")
182                self.actionEditSlicer.triggered.connect(self.onEditSlicer)
183        self.contextMenu.addSeparator()
184        self.actionColorMap = self.contextMenu.addAction("&2D Color Map")
185        self.actionColorMap.triggered.connect(self.onColorMap)
186        self.contextMenu.addSeparator()
187        self.actionChangeScale = self.contextMenu.addAction("Toggle Linear/Log Scale")
188        self.actionChangeScale.triggered.connect(self.onToggleScale)
189
190    def createContextMenuQuick(self):
191        """
192        Define context menu and associated actions for the quickplot MPL widget
193        """
194        self.defaultContextMenu()
195
196        if self.dimension == 2:
197            self.actionToggleGrid = self.contextMenu.addAction("Toggle Grid On/Off")
198            self.contextMenu.addSeparator()
199        self.actionChangeScale = self.contextMenu.addAction("Toggle Linear/Log Scale")
200
201        # Define the callbacks
202        self.actionChangeScale.triggered.connect(self.onToggleScale)
203        if self.dimension == 2:
204            self.actionToggleGrid.triggered.connect(self.onGridToggle)
205
206    def onToggleScale(self, event):
207        """
208        Toggle axis and replot image
209        """
210        # self.scale predefined in the baseclass
211        if self.scale == 'log_{10}':
212            self.scale = 'linear'
213        else:
214            self.scale = 'log_{10}'
215
216        self.plot()
217
218    def onClearSlicer(self):
219        """
220        Remove all sclicers from the chart
221        """
222        if self.slicer is None:
223            return
224
225        self.slicer.clear()
226        self.canvas.draw()
227        self.slicer = None
228
229    def onEditSlicer(self):
230        """
231        Present a small dialog for manipulating the current slicer
232        """
233        assert self.slicer
234        # Only show the dialog if not currently shown
235        if self.slicer_widget:
236            return
237        def slicer_closed():
238            # Need to disconnect the signal!!
239            self.slicer_widget.close_signal.disconnect()
240            # reset slicer_widget on "Edit Slicer Parameters" window close
241            self.slicer_widget = None
242
243        self.param_model = self.slicer.model()
244        # Pass the model to the Slicer Parameters widget
245        self.slicer_widget = SlicerParameters(model=self.param_model,
246                                              validate_method=self.slicer.validate)
247        self.slicer_widget.close_signal.connect(slicer_closed)
248        # Add the plot to the workspace
249        self.manager.parent.workspace().addSubWindow(self.slicer_widget)
250
251        self.slicer_widget.show()
252
253    def onCircularAverage(self):
254        """
255        Perform circular averaging on Data2D
256        """
257        # Find the best number of bins
258        npt = numpy.sqrt(len(self.data.data[numpy.isfinite(self.data.data)]))
259        npt = numpy.floor(npt)
260        # compute the maximum radius of data2D
261        self.qmax = max(numpy.fabs(self.data.xmax),
262                        numpy.fabs(self.data.xmin))
263        self.ymax = max(numpy.fabs(self.data.ymax),
264                        numpy.fabs(self.data.ymin))
265        self.radius = numpy.sqrt(numpy.power(self.qmax, 2) + numpy.power(self.ymax, 2))
266        #Compute beam width
267        bin_width = (self.qmax + self.qmax) / npt
268        # Create data1D circular average of data2D
269        circle = CircularAverage(r_min=0, r_max=self.radius, bin_width=bin_width)
270        circ = circle(self.data)
271        dxl = circ.dxl if hasattr(circ, "dxl") else None
272        dxw = circ.dxw if hasattr(circ, "dxw") else None
273
274        new_plot = Data1D(x=circ.x, y=circ.y, dy=circ.dy, dx=circ.dx)
275        new_plot.dxl = dxl
276        new_plot.dxw = dxw
277        new_plot.name = new_plot.title = "Circ avg " + self.data.name
278        new_plot.source = self.data.source
279        new_plot.interactive = True
280        new_plot.detector = self.data.detector
281
282        # Define axes if not done yet.
283        new_plot.xaxis("\\rm{Q}", "A^{-1}")
284        if hasattr(self.data, "scale") and \
285                    self.data.scale == 'linear':
286            new_plot.ytransform = 'y'
287            new_plot.yaxis("\\rm{Residuals} ", "normalized")
288        else:
289            new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}")
290
291        new_plot.group_id = "2daverage" + self.data.name
292        new_plot.id = "Circ avg " + self.data.name
293        new_plot.is_data = True
294        GuiUtils.updateModelItemWithPlot(self._item, new_plot, new_plot.id)
295
296        self.manager.communicator.plotUpdateSignal.emit([new_plot])
297
298    def setSlicer(self, slicer):
299        """
300        Clear the previous slicer and create a new one.
301        slicer: slicer class to create
302        """
303        # Clear current slicer
304        if self.slicer is not None:
305            self.slicer.clear()
306        # Create a new slicer
307        self.slicer_z += 1
308        self.slicer = slicer(self, self.ax, item=self._item, zorder=self.slicer_z)
309        self.ax.set_ylim(self.data.ymin, self.data.ymax)
310        self.ax.set_xlim(self.data.xmin, self.data.xmax)
311        # Draw slicer
312        self.figure.canvas.draw()
313        self.slicer.update()
314
315        # Reset the model on the Edit slicer parameters widget
316        self.param_model = self.slicer.model()
317        if self.slicer_widget:
318            self.slicer_widget.setModel(self.param_model)
319
320    def onSectorView(self):
321        """
322        Perform sector averaging on Q and draw sector slicer
323        """
324        self.setSlicer(slicer=SectorInteractor)
325
326    def onAnnulusView(self):
327        """
328        Perform sector averaging on Phi and draw annulus slicer
329        """
330        self.setSlicer(slicer=AnnulusInteractor)
331
332    def onBoxSum(self):
333        """
334        Perform 2D Data averaging Qx and Qy.
335        Display box slicer details.
336        """
337        self.onClearSlicer()
338        self.slicer_z += 1
339        self.slicer = BoxSumCalculator(self, self.ax, zorder=self.slicer_z)
340
341        self.ax.set_ylim(self.data.ymin, self.data.ymax)
342        self.ax.set_xlim(self.data.xmin, self.data.xmax)
343        self.figure.canvas.draw()
344        self.slicer.update()
345
346        # Get the BoxSumCalculator model.
347        self.box_sum_model = self.slicer.model()
348        # Pass the BoxSumCalculator model to the BoxSum widget
349        self.boxwidget = BoxSum(self, model=self.box_sum_model)
350        # Add the plot to the workspace
351        self.manager.parent.workspace().addSubWindow(self.boxwidget)
352        self.boxwidget.show()
353
354    def onBoxAveragingX(self):
355        """
356        Perform 2D data averaging on Qx
357        Create a new slicer.
358        """
359        self.setSlicer(slicer=BoxInteractorX)
360
361    def onBoxAveragingY(self):
362        """
363        Perform 2D data averaging on Qy
364        Create a new slicer .
365        """
366        self.setSlicer(slicer=BoxInteractorY)
367
368    def onColorMap(self):
369        """
370        Display the color map dialog and modify the plot's map accordingly
371        """
372        color_map_dialog = ColorMap(self, cmap=self.cmap,
373                                    vmin=self.vmin,
374                                    vmax=self.vmax,
375                                    data=self.data)
376
377        color_map_dialog.apply_signal.connect(self.onApplyMap)
378
379        if color_map_dialog.exec_() == QtWidgets.QDialog.Accepted:
380            self.onApplyMap(color_map_dialog.norm(), color_map_dialog.cmap())
381
382    def onApplyMap(self, v_values, cmap):
383        """
384        Update the chart color map based on data passed from the widget
385        """
386        self.cmap = str(cmap)
387        self.vmin, self.vmax = v_values
388        # Redraw the chart with new cmap
389        self.plot()
390
391    def showPlot(self, data, qx_data, qy_data, xmin, xmax, ymin, ymax,
392                 zmin, zmax, label='data2D', cmap=DEFAULT_CMAP, show_colorbar=True):
393        """
394        Render and show the current data
395        """
396        self.qx_data = qx_data
397        self.qy_data = qy_data
398        self.xmin = xmin
399        self.xmax = xmax
400        self.ymin = ymin
401        self.ymax = ymax
402        self.zmin = zmin
403        self.zmax = zmax
404        # If we don't have any data, skip.
405        if data is None:
406            return
407        if data.ndim == 0:
408            return
409        elif data.ndim == 1:
410            output = PlotUtilities.build_matrix(data, self.qx_data, self.qy_data)
411        else:
412            output = copy.deepcopy(data)
413
414        zmin_temp = self.zmin
415        # check scale
416        if self.scale == 'log_{10}':
417            try:
418                if  self.zmin <= 0  and len(output[output > 0]) > 0:
419                    zmin_temp = self.zmin
420                    output[output > 0] = numpy.log10(output[output > 0])
421                elif self.zmin <= 0:
422                    zmin_temp = self.zmin
423                    output[output > 0] = numpy.zeros(len(output))
424                    output[output <= 0] = MIN_Z
425                else:
426                    zmin_temp = self.zmin
427                    output[output > 0] = numpy.log10(output[output > 0])
428            except:
429                #Too many problems in 2D plot with scale
430                output[output > 0] = numpy.log10(output[output > 0])
431                pass
432
433        self.cmap = cmap
434        if self.dimension != 3:
435            #Re-adjust colorbar
436            self.figure.subplots_adjust(left=0.2, right=.8, bottom=.2)
437
438            zmax_temp = self.zmax
439            if self.vmin is not None:
440                zmin_temp = self.vmin
441                zmax_temp = self.vmax
442            if self.im is not None:
443                self.im.set_data(output)
444            else:
445                self.im = self.ax.imshow(output, interpolation='nearest',
446                                # origin='lower',
447                                vmin=zmin_temp, vmax=zmax_temp,
448                                cmap=self.cmap,
449                                extent=(self.xmin, self.xmax,
450                                        self.ymin, self.ymax))
451
452            cbax = self.figure.add_axes([0.84, 0.2, 0.02, 0.7])
453
454            # Current labels for axes
455            self.ax.set_ylabel(self.y_label)
456            self.ax.set_xlabel(self.x_label)
457
458            # Title only for regular charts
459            if not self.quickplot:
460                self.ax.set_title(label=self._title)
461
462            if cbax is None:
463                ax.set_frame_on(False)
464                cb = self.figure.colorbar(self.im, shrink=0.8, aspect=20)
465            else:
466                cb = self.figure.colorbar(self.im, cax=cbax)
467
468            cb.update_bruteforce(self.im)
469            cb.set_label('$' + self.scale + '$')
470
471            self.vmin = cb.vmin
472            self.vmax = cb.vmax
473
474            if show_colorbar is False:
475                cb.remove()
476
477        else:
478            # clear the previous 2D from memory
479            self.figure.clear()
480
481            self.figure.subplots_adjust(left=0.1, right=.8, bottom=.1)
482
483            data_x, data_y = numpy.meshgrid(self._data.x_bins[0:-1],
484                                            self._data.y_bins[0:-1])
485
486            ax = Axes3D(self.figure)
487
488            # Disable rotation for large sets.
489            # TODO: Define "large" for a dataset
490            SET_TOO_LARGE = 500
491            if len(data_x) > SET_TOO_LARGE:
492                ax.disable_mouse_rotation()
493
494            self.figure.canvas.resizing = False
495            im = ax.plot_surface(data_x, data_y, output, rstride=1,
496                                 cstride=1, cmap=cmap,
497                                 linewidth=0, antialiased=False)
498            self.ax.set_axis_off()
499
500        if self.dimension != 3:
501            self.figure.canvas.draw_idle()
502        else:
503            self.figure.canvas.draw()
504
505    def update(self):
506        self.figure.canvas.draw()
507
508    def draw(self):
509        self.figure.canvas.draw()
510
511    def replacePlot(self, id, new_plot):
512        """
513        Replace data in current chart.
514        This effectively refreshes the chart with changes to one of its plots
515        """
516        self.plot(data=new_plot)
517
518
519class Plotter2D(QtWidgets.QDialog, Plotter2DWidget):
520    """
521    Plotter widget implementation
522    """
523    def __init__(self, parent=None, quickplot=False, dimension=2):
524        QtWidgets.QDialog.__init__(self)
525        Plotter2DWidget.__init__(self, manager=parent, quickplot=quickplot, dimension=dimension)
526        icon = QtGui.QIcon()
527        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
528        self.setWindowIcon(icon)
Note: See TracBrowser for help on using the repository browser.