source: sasview/src/sas/qtgui/Plotter2D.py @ f182f93

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 f182f93 was 116260a, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

enable real time updates of more 1D charts

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