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

Last change on this file since 4a9786d8 was f4a1433, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Merge branch 'master' into ESS_GUI

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