source: sasview/src/sas/qtgui/Plotter2D.py @ 9a05a8d5

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

Minor code cleanup of Slicer Parameters Editor

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