source: sasview/src/sas/qtgui/Plotter2D.py @ 965fbd8

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 965fbd8 was 3bdbfcc, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Reimplementation of the slicer functionality

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