source: sasview/src/sas/qtgui/Plotter2D.py @ b789967

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

More minor fixes to plotting

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