source: sasview/src/sas/qtgui/Plotter2D.py @ 57b7ee2

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

Code review changes for Slicer Parameter Editor

  • Property mode set to 100644
File size: 17.5 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(self, model=self.param_model)
229        self.slicer_widget.close_signal.connect(slicer_closed)
230
231        self.slicer_widget.show()
232
233    def onCircularAverage(self):
234        """
235        Perform circular averaging on Data2D
236        """
237        # Find the best number of bins
238        npt = numpy.sqrt(len(self.data.data[numpy.isfinite(self.data.data)]))
239        npt = numpy.floor(npt)
240        # compute the maximum radius of data2D
241        self.qmax = max(numpy.fabs(self.data.xmax),
242                        numpy.fabs(self.data.xmin))
243        self.ymax = max(numpy.fabs(self.data.ymax),
244                        numpy.fabs(self.data.ymin))
245        self.radius = numpy.sqrt(numpy.power(self.qmax, 2) + numpy.power(self.ymax, 2))
246        #Compute beam width
247        bin_width = (self.qmax + self.qmax) / npt
248        # Create data1D circular average of data2D
249        circle = CircularAverage(r_min=0, r_max=self.radius, bin_width=bin_width)
250        circ = circle(self.data)
251        dxl = circ.dxl if hasattr(circ, "dxl") else None
252        dxw = circ.dxw if hasattr(circ, "dxw") else None
253
254        new_plot = Data1D(x=circ.x, y=circ.y, dy=circ.dy, dx=circ.dx)
255        new_plot.dxl = dxl
256        new_plot.dxw = dxw
257        new_plot.name = new_plot.title = "Circ avg " + self.data.name
258        new_plot.source = self.data.source
259        new_plot.interactive = True
260        new_plot.detector = self.data.detector
261
262        # Define axes if not done yet.
263        new_plot.xaxis("\\rm{Q}", "A^{-1}")
264        if hasattr(self.data, "scale") and \
265                    self.data.scale == 'linear':
266            new_plot.ytransform = 'y'
267            new_plot.yaxis("\\rm{Residuals} ", "normalized")
268        else:
269            new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}")
270
271        new_plot.group_id = "2daverage" + self.data.name
272        new_plot.id = "Circ avg " + self.data.name
273        new_plot.is_data = True
274        variant_plot = QtCore.QVariant(new_plot)
275        GuiUtils.updateModelItemWithPlot(self._item, variant_plot, new_plot.id)
276        # TODO: force immediate display (?)
277
278    def setSlicer(self, slicer):
279        """
280        Clear the previous slicer and create a new one.
281        slicer: slicer class to create
282        """
283        # Clear current slicer
284        if self.slicer is not None:
285            self.slicer.clear()
286        # Create a new slicer
287        self.slicer_z += 1
288        self.slicer = slicer(self, self.ax, item=self._item, zorder=self.slicer_z)
289        self.ax.set_ylim(self.data.ymin, self.data.ymax)
290        self.ax.set_xlim(self.data.xmin, self.data.xmax)
291        # Draw slicer
292        self.figure.canvas.draw()
293        self.slicer.update()
294
295        # Reset the model on the Edit slicer parameters widget
296        self.param_model = self.slicer.model()
297        if self.slicer_widget:
298            self.slicer_widget.setModel(self.param_model)
299
300
301    def onSectorView(self):
302        """
303        Perform sector averaging on Q and draw sector slicer
304        """
305        self.setSlicer(slicer=SectorInteractor)
306
307
308    def onAnnulusView(self):
309        """
310        Perform sector averaging on Phi and draw annulus slicer
311        """
312        self.setSlicer(slicer=AnnulusInteractor)
313
314    def onBoxSum(self):
315        """
316        Perform 2D Data averaging Qx and Qy.
317        Display box slicer details.
318        """
319        self.onClearSlicer()
320        self.slicer_z += 1
321        self.slicer = BoxSumCalculator(self, self.ax, zorder=self.slicer_z)
322
323        self.ax.set_ylim(self.data.ymin, self.data.ymax)
324        self.ax.set_xlim(self.data.xmin, self.data.xmax)
325        self.figure.canvas.draw()
326        self.slicer.update()
327
328        # Get the BoxSumCalculator model.
329        self.box_sum_model = self.slicer.model()
330        # Pass the BoxSumCalculator model to the BoxSum widget
331        self.boxwidget = BoxSum(self, model=self.box_sum_model)
332        # Add the plot to the workspace
333        self.manager.parent.workspace().addWindow(self.boxwidget)
334        self.boxwidget.show()
335
336    def onBoxAveragingX(self):
337        """
338        Perform 2D data averaging on Qx
339        Create a new slicer.
340        """
341        self.setSlicer(slicer=BoxInteractorX)
342
343    def onBoxAveragingY(self):
344        """
345        Perform 2D data averaging on Qy
346        Create a new slicer .
347        """
348        self.setSlicer(slicer=BoxInteractorY)
349
350    def onColorMap(self):
351        """
352        Display the color map dialog and modify the plot's map accordingly
353        """
354        color_map_dialog = ColorMap(self, cmap=self.cmap,
355                                    vmin=self.vmin,
356                                    vmax=self.vmax,
357                                    data=self.data)
358
359        color_map_dialog.apply_signal.connect(self.onApplyMap)
360
361        if color_map_dialog.exec_() == QtGui.QDialog.Accepted:
362            self.onApplyMap(color_map_dialog.norm(), color_map_dialog.cmap())
363
364    def onApplyMap(self, v_values, cmap):
365        """
366        Update the chart color map based on data passed from the widget
367        """
368        self.cmap = str(cmap)
369        self.vmin, self.vmax = v_values
370        # Redraw the chart with new cmap
371        self.plot()
372
373    def showPlot(self, data, qx_data, qy_data, xmin, xmax, ymin, ymax,
374                 zmin, zmax, label='data2D', cmap=DEFAULT_CMAP):
375        """
376        Render and show the current data
377        """
378        self.qx_data = qx_data
379        self.qy_data = qy_data
380        self.xmin = xmin
381        self.xmax = xmax
382        self.ymin = ymin
383        self.ymax = ymax
384        self.zmin = zmin
385        self.zmax = zmax
386        # If we don't have any data, skip.
387        if data is None:
388            return
389        if data.ndim == 1:
390            output = PlotUtilities.build_matrix(data, self.qx_data, self.qy_data)
391        else:
392            output = copy.deepcopy(data)
393
394        zmin_temp = self.zmin
395        # check scale
396        if self.scale == 'log_{10}':
397            try:
398                if  self.zmin <= 0  and len(output[output > 0]) > 0:
399                    zmin_temp = self.zmin
400                    output[output > 0] = numpy.log10(output[output > 0])
401                elif self.zmin <= 0:
402                    zmin_temp = self.zmin
403                    output[output > 0] = numpy.zeros(len(output))
404                    output[output <= 0] = MIN_Z
405                else:
406                    zmin_temp = self.zmin
407                    output[output > 0] = numpy.log10(output[output > 0])
408            except:
409                #Too many problems in 2D plot with scale
410                output[output > 0] = numpy.log10(output[output > 0])
411                pass
412
413        self.cmap = cmap
414        if self.dimension != 3:
415            #Re-adjust colorbar
416            self.figure.subplots_adjust(left=0.2, right=.8, bottom=.2)
417
418            zmax_temp = self.zmax
419            if self.vmin is not None:
420                zmin_temp = self.vmin
421                zmax_temp = self.vmax
422
423            im = self.ax.imshow(output, interpolation='nearest',
424                                origin='lower',
425                                vmin=zmin_temp, vmax=zmax_temp,
426                                cmap=self.cmap,
427                                extent=(self.xmin, self.xmax,
428                                        self.ymin, self.ymax))
429
430            cbax = self.figure.add_axes([0.84, 0.2, 0.02, 0.7])
431
432            # Current labels for axes
433            self.ax.set_ylabel(self.y_label)
434            self.ax.set_xlabel(self.x_label)
435
436            # Title only for regular charts
437            if not self.quickplot:
438                self.ax.set_title(label=self._title)
439
440            if cbax is None:
441                ax.set_frame_on(False)
442                cb = self.figure.colorbar(im, shrink=0.8, aspect=20)
443            else:
444                cb = self.figure.colorbar(im, cax=cbax)
445
446            cb.update_bruteforce(im)
447            cb.set_label('$' + self.scale + '$')
448
449            self.vmin = cb.vmin
450            self.vmax = cb.vmax
451
452        else:
453            # clear the previous 2D from memory
454            self.figure.clear()
455
456            self.figure.subplots_adjust(left=0.1, right=.8, bottom=.1)
457
458            data_x, data_y = numpy.meshgrid(self._data.x_bins[0:-1],
459                                            self._data.y_bins[0:-1])
460
461            ax = Axes3D(self.figure)
462
463            # Disable rotation for large sets.
464            # TODO: Define "large" for a dataset
465            SET_TOO_LARGE = 500
466            if len(data_x) > SET_TOO_LARGE:
467                ax.disable_mouse_rotation()
468
469            self.figure.canvas.resizing = False
470            im = ax.plot_surface(data_x, data_y, output, rstride=1,
471                                 cstride=1, cmap=cmap,
472                                 linewidth=0, antialiased=False)
473            self.ax.set_axis_off()
474
475        if self.dimension != 3:
476            self.figure.canvas.draw_idle()
477        else:
478            self.figure.canvas.draw()
479
480    def update(self):
481        self.figure.canvas.draw()
482
483    def draw(self):
484        self.figure.canvas.draw()
485
486
487class Plotter2D(QtGui.QDialog, Plotter2DWidget):
488    """
489    Plotter widget implementation
490    """
491    def __init__(self, parent=None, quickplot=False, dimension=2):
492        QtGui.QDialog.__init__(self)
493        Plotter2DWidget.__init__(self, manager=parent, quickplot=quickplot, dimension=dimension)
494        icon = QtGui.QIcon()
495        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
496        self.setWindowIcon(icon)
Note: See TracBrowser for help on using the repository browser.