source: sasview/src/sas/qtgui/Plotter2D.py @ 57b7ee2

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

Code review changes for Slicer Parameter Editor

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