source: sasview/src/sas/qtgui/Plotter2D.py @ 86f88d1

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

Validate Table View entries in 2D slicer parameter editor - prototype for future use in fitting

  • 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                                              validate_method=self.slicer.validate)
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.