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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 133812c7 was 133812c7, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Merged ESS_GUI

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