source: sasview/src/sas/qtgui/Plotter2D.py @ 7b8485f

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

Code review for slicer edit functionality

  • 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.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
27
28# Minimum value of Z for which we will present data.
29MIN_Z = -32
30
31class Plotter2DWidget(PlotterBase):
32    """
33    2D Plot widget for use with a QDialog
34    """
35    def __init__(self, parent=None, manager=None, quickplot=False, dimension=2):
36        self.dimension = dimension
37        super(Plotter2DWidget, self).__init__(parent, manager=manager, quickplot=quickplot)
38
39        self.cmap = DEFAULT_CMAP.name
40        # Default scale
41        self.scale = 'log_{10}'
42        # to set the order of lines drawn first.
43        self.slicer_z = 5
44        # Reference to the current slicer
45        self.slicer = None
46        self.slicer_widget = 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            if self.slicer.__class__.__name__ != "BoxSumCalculator":
163                self.actionEditSlicer = self.contextMenu.addAction("&Edit Slicer Parameters")
164                self.actionEditSlicer.triggered.connect(self.onEditSlicer)
165        self.contextMenu.addSeparator()
166        self.actionColorMap = self.contextMenu.addAction("&2D Color Map")
167        self.actionColorMap.triggered.connect(self.onColorMap)
168        self.contextMenu.addSeparator()
169        self.actionChangeScale = self.contextMenu.addAction("Toggle Linear/Log Scale")
170        self.actionChangeScale.triggered.connect(self.onToggleScale)
171
172    def createContextMenuQuick(self):
173        """
174        Define context menu and associated actions for the quickplot MPL widget
175        """
176        self.defaultContextMenu()
177
178        if self.dimension == 2:
179            self.actionToggleGrid = self.contextMenu.addAction("Toggle Grid On/Off")
180            self.contextMenu.addSeparator()
181        self.actionChangeScale = self.contextMenu.addAction("Toggle Linear/Log Scale")
182
183        # Define the callbacks
184        self.actionChangeScale.triggered.connect(self.onToggleScale)
185        if self.dimension == 2:
186            self.actionToggleGrid.triggered.connect(self.onGridToggle)
187
188    def onToggleScale(self, event):
189        """
190        Toggle axis and replot image
191        """
192        # self.scale predefined in the baseclass
193        if self.scale == 'log_{10}':
194            self.scale = 'linear'
195        else:
196            self.scale = 'log_{10}'
197
198        self.plot()
199
200    def onClearSlicer(self):
201        """
202        Remove all sclicers from the chart
203        """
204        if self.slicer is None:
205            return
206
207        self.slicer.clear()
208        self.canvas.draw()
209        self.slicer = None
210
211    def onEditSlicer(self):
212        """
213        Present a small dialog for manipulating the current slicer
214        """
215        assert self.slicer
216        # Only show the dialog if not currently shown
217        if self.slicer_widget:
218            return
219        def slicer_closed():
220            # Need to disconnect the signal!!
221            self.slicer_widget.close_signal.disconnect()
222            # reset slicer_widget on "Edit Slicer Parameters" window close
223            self.slicer_widget = None
224
225        self.param_model = self.slicer.model()
226        # Pass the model to the Slicer Parameters widget
227        self.slicer_widget = SlicerParameters(model=self.param_model)
228        self.slicer_widget.close_signal.connect(slicer_closed)
229        # Add the plot to the workspace
230        self.manager.parent.workspace().addWindow(self.slicer_widget)
231
232        self.slicer_widget.show()
233
234    def onCircularAverage(self):
235        """
236        Perform circular averaging on Data2D
237        """
238        # Find the best number of bins
239        npt = numpy.sqrt(len(self.data.data[numpy.isfinite(self.data.data)]))
240        npt = numpy.floor(npt)
241        # compute the maximum radius of data2D
242        self.qmax = max(numpy.fabs(self.data.xmax),
243                        numpy.fabs(self.data.xmin))
244        self.ymax = max(numpy.fabs(self.data.ymax),
245                        numpy.fabs(self.data.ymin))
246        self.radius = numpy.sqrt(numpy.power(self.qmax, 2) + numpy.power(self.ymax, 2))
247        #Compute beam width
248        bin_width = (self.qmax + self.qmax) / npt
249        # Create data1D circular average of data2D
250        circle = CircularAverage(r_min=0, r_max=self.radius, bin_width=bin_width)
251        circ = circle(self.data)
252        dxl = circ.dxl if hasattr(circ, "dxl") else None
253        dxw = circ.dxw if hasattr(circ, "dxw") else None
254
255        new_plot = Data1D(x=circ.x, y=circ.y, dy=circ.dy, dx=circ.dx)
256        new_plot.dxl = dxl
257        new_plot.dxw = dxw
258        new_plot.name = new_plot.title = "Circ avg " + self.data.name
259        new_plot.source = self.data.source
260        new_plot.interactive = True
261        new_plot.detector = self.data.detector
262
263        # Define axes if not done yet.
264        new_plot.xaxis("\\rm{Q}", "A^{-1}")
265        if hasattr(self.data, "scale") and \
266                    self.data.scale == 'linear':
267            new_plot.ytransform = 'y'
268            new_plot.yaxis("\\rm{Residuals} ", "normalized")
269        else:
270            new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}")
271
272        new_plot.group_id = "2daverage" + self.data.name
273        new_plot.id = "Circ avg " + self.data.name
274        new_plot.is_data = True
275        variant_plot = QtCore.QVariant(new_plot)
276        GuiUtils.updateModelItemWithPlot(self._item, variant_plot, new_plot.id)
277        # TODO: force immediate display (?)
278
279    def setSlicer(self, slicer):
280        """
281        Clear the previous slicer and create a new one.
282        slicer: slicer class to create
283        """
284        # Clear current slicer
285        if self.slicer is not None:
286            self.slicer.clear()
287        # Create a new slicer
288        self.slicer_z += 1
289        self.slicer = slicer(self, self.ax, item=self._item, zorder=self.slicer_z)
290        self.ax.set_ylim(self.data.ymin, self.data.ymax)
291        self.ax.set_xlim(self.data.xmin, self.data.xmax)
292        # Draw slicer
293        self.figure.canvas.draw()
294        self.slicer.update()
295
296        # Reset the model on the Edit slicer parameters widget
297        self.param_model = self.slicer.model()
298        if self.slicer_widget:
299            self.slicer_widget.setModel(self.param_model)
300
301
302    def onSectorView(self):
303        """
304        Perform sector averaging on Q and draw sector slicer
305        """
306        self.setSlicer(slicer=SectorInteractor)
307
308
309    def onAnnulusView(self):
310        """
311        Perform sector averaging on Phi and draw annulus slicer
312        """
313        self.setSlicer(slicer=AnnulusInteractor)
314
315    def onBoxSum(self):
316        """
317        Perform 2D Data averaging Qx and Qy.
318        Display box slicer details.
319        """
320        self.onClearSlicer()
321        self.slicer_z += 1
322        self.slicer = BoxSumCalculator(self, self.ax, zorder=self.slicer_z)
323
324        self.ax.set_ylim(self.data.ymin, self.data.ymax)
325        self.ax.set_xlim(self.data.xmin, self.data.xmax)
326        self.figure.canvas.draw()
327        self.slicer.update()
328
329        # Get the BoxSumCalculator model.
330        self.box_sum_model = self.slicer.model()
331        # Pass the BoxSumCalculator model to the BoxSum widget
332        self.boxwidget = BoxSum(self, model=self.box_sum_model)
333        # Add the plot to the workspace
334        self.manager.parent.workspace().addWindow(self.boxwidget)
335        self.boxwidget.show()
336
337    def onBoxAveragingX(self):
338        """
339        Perform 2D data averaging on Qx
340        Create a new slicer.
341        """
342        self.setSlicer(slicer=BoxInteractorX)
343
344    def onBoxAveragingY(self):
345        """
346        Perform 2D data averaging on Qy
347        Create a new slicer .
348        """
349        self.setSlicer(slicer=BoxInteractorY)
350
351    def onColorMap(self):
352        """
353        Display the color map dialog and modify the plot's map accordingly
354        """
355        color_map_dialog = ColorMap(self, cmap=self.cmap,
356                                    vmin=self.vmin,
357                                    vmax=self.vmax,
358                                    data=self.data)
359
360        color_map_dialog.apply_signal.connect(self.onApplyMap)
361
362        if color_map_dialog.exec_() == QtGui.QDialog.Accepted:
363            self.onApplyMap(color_map_dialog.norm(), color_map_dialog.cmap())
364
365    def onApplyMap(self, v_values, cmap):
366        """
367        Update the chart color map based on data passed from the widget
368        """
369        self.cmap = str(cmap)
370        self.vmin, self.vmax = v_values
371        # Redraw the chart with new cmap
372        self.plot()
373
374    def showPlot(self, data, qx_data, qy_data, xmin, xmax, ymin, ymax,
375                 zmin, zmax, label='data2D', cmap=DEFAULT_CMAP):
376        """
377        Render and show the current data
378        """
379        self.qx_data = qx_data
380        self.qy_data = qy_data
381        self.xmin = xmin
382        self.xmax = xmax
383        self.ymin = ymin
384        self.ymax = ymax
385        self.zmin = zmin
386        self.zmax = zmax
387        # If we don't have any data, skip.
388        if data is None:
389            return
390        if data.ndim == 1:
391            output = PlotUtilities.build_matrix(data, self.qx_data, self.qy_data)
392        else:
393            output = copy.deepcopy(data)
394
395        zmin_temp = self.zmin
396        # check scale
397        if self.scale == 'log_{10}':
398            try:
399                if  self.zmin <= 0  and len(output[output > 0]) > 0:
400                    zmin_temp = self.zmin
401                    output[output > 0] = numpy.log10(output[output > 0])
402                elif self.zmin <= 0:
403                    zmin_temp = self.zmin
404                    output[output > 0] = numpy.zeros(len(output))
405                    output[output <= 0] = MIN_Z
406                else:
407                    zmin_temp = self.zmin
408                    output[output > 0] = numpy.log10(output[output > 0])
409            except:
410                #Too many problems in 2D plot with scale
411                output[output > 0] = numpy.log10(output[output > 0])
412                pass
413
414        self.cmap = cmap
415        if self.dimension != 3:
416            #Re-adjust colorbar
417            self.figure.subplots_adjust(left=0.2, right=.8, bottom=.2)
418
419            zmax_temp = self.zmax
420            if self.vmin is not None:
421                zmin_temp = self.vmin
422                zmax_temp = self.vmax
423
424            im = self.ax.imshow(output, interpolation='nearest',
425                                origin='lower',
426                                vmin=zmin_temp, vmax=zmax_temp,
427                                cmap=self.cmap,
428                                extent=(self.xmin, self.xmax,
429                                        self.ymin, self.ymax))
430
431            cbax = self.figure.add_axes([0.84, 0.2, 0.02, 0.7])
432
433            # Current labels for axes
434            self.ax.set_ylabel(self.y_label)
435            self.ax.set_xlabel(self.x_label)
436
437            # Title only for regular charts
438            if not self.quickplot:
439                self.ax.set_title(label=self._title)
440
441            if cbax is None:
442                ax.set_frame_on(False)
443                cb = self.figure.colorbar(im, shrink=0.8, aspect=20)
444            else:
445                cb = self.figure.colorbar(im, cax=cbax)
446
447            cb.update_bruteforce(im)
448            cb.set_label('$' + self.scale + '$')
449
450            self.vmin = cb.vmin
451            self.vmax = cb.vmax
452
453        else:
454            # clear the previous 2D from memory
455            self.figure.clear()
456
457            self.figure.subplots_adjust(left=0.1, right=.8, bottom=.1)
458
459            data_x, data_y = numpy.meshgrid(self._data.x_bins[0:-1],
460                                            self._data.y_bins[0:-1])
461
462            ax = Axes3D(self.figure)
463
464            # Disable rotation for large sets.
465            # TODO: Define "large" for a dataset
466            SET_TOO_LARGE = 500
467            if len(data_x) > SET_TOO_LARGE:
468                ax.disable_mouse_rotation()
469
470            self.figure.canvas.resizing = False
471            im = ax.plot_surface(data_x, data_y, output, rstride=1,
472                                 cstride=1, cmap=cmap,
473                                 linewidth=0, antialiased=False)
474            self.ax.set_axis_off()
475
476        if self.dimension != 3:
477            self.figure.canvas.draw_idle()
478        else:
479            self.figure.canvas.draw()
480
481    def update(self):
482        self.figure.canvas.draw()
483
484    def draw(self):
485        self.figure.canvas.draw()
486
487
488class Plotter2D(QtGui.QDialog, Plotter2DWidget):
489    """
490    Plotter widget implementation
491    """
492    def __init__(self, parent=None, quickplot=False, dimension=2):
493        QtGui.QDialog.__init__(self)
494        Plotter2DWidget.__init__(self, manager=parent, quickplot=quickplot, dimension=dimension)
495        icon = QtGui.QIcon()
496        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
497        self.setWindowIcon(icon)
Note: See TracBrowser for help on using the repository browser.