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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 6bc0840 was 2f55df6, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

Slicer Parameters should also close the corresponding subwindow on exit.
Also: fixed toFloat() conversion.
SASVIEW-1115

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