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

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

More Qt5 related fixes

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