source: sasview/src/sas/qtgui/Plotter2D.py @ 0274aea

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

Changes to Qmodel now visible in Plot2D charts

  • Property mode set to 100644
File size: 17.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.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, marker=None):
84        """
85        Plot 2D self._data
86        marker - unused
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                                              validate_method=self.slicer.validate)
230        self.slicer_widget.close_signal.connect(slicer_closed)
231        # Add the plot to the workspace
232        self.manager.parent.workspace().addWindow(self.slicer_widget)
233
234        self.slicer_widget.show()
235
236    def onCircularAverage(self):
237        """
238        Perform circular averaging on Data2D
239        """
240        # Find the best number of bins
241        npt = numpy.sqrt(len(self.data.data[numpy.isfinite(self.data.data)]))
242        npt = numpy.floor(npt)
243        # compute the maximum radius of data2D
244        self.qmax = max(numpy.fabs(self.data.xmax),
245                        numpy.fabs(self.data.xmin))
246        self.ymax = max(numpy.fabs(self.data.ymax),
247                        numpy.fabs(self.data.ymin))
248        self.radius = numpy.sqrt(numpy.power(self.qmax, 2) + numpy.power(self.ymax, 2))
249        #Compute beam width
250        bin_width = (self.qmax + self.qmax) / npt
251        # Create data1D circular average of data2D
252        circle = CircularAverage(r_min=0, r_max=self.radius, bin_width=bin_width)
253        circ = circle(self.data)
254        dxl = circ.dxl if hasattr(circ, "dxl") else None
255        dxw = circ.dxw if hasattr(circ, "dxw") else None
256
257        new_plot = Data1D(x=circ.x, y=circ.y, dy=circ.dy, dx=circ.dx)
258        new_plot.dxl = dxl
259        new_plot.dxw = dxw
260        new_plot.name = new_plot.title = "Circ avg " + self.data.name
261        new_plot.source = self.data.source
262        new_plot.interactive = True
263        new_plot.detector = self.data.detector
264
265        # Define axes if not done yet.
266        new_plot.xaxis("\\rm{Q}", "A^{-1}")
267        if hasattr(self.data, "scale") and \
268                    self.data.scale == 'linear':
269            new_plot.ytransform = 'y'
270            new_plot.yaxis("\\rm{Residuals} ", "normalized")
271        else:
272            new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}")
273
274        new_plot.group_id = "2daverage" + self.data.name
275        new_plot.id = "Circ avg " + self.data.name
276        new_plot.is_data = True
277        variant_plot = QtCore.QVariant(new_plot)
278        GuiUtils.updateModelItemWithPlot(self._item, variant_plot, new_plot.id)
279        # TODO: force immediate display (?)
280
281    def setSlicer(self, slicer):
282        """
283        Clear the previous slicer and create a new one.
284        slicer: slicer class to create
285        """
286        # Clear current slicer
287        if self.slicer is not None:
288            self.slicer.clear()
289        # Create a new slicer
290        self.slicer_z += 1
291        self.slicer = slicer(self, self.ax, item=self._item, zorder=self.slicer_z)
292        self.ax.set_ylim(self.data.ymin, self.data.ymax)
293        self.ax.set_xlim(self.data.xmin, self.data.xmax)
294        # Draw slicer
295        self.figure.canvas.draw()
296        self.slicer.update()
297
298        # Reset the model on the Edit slicer parameters widget
299        self.param_model = self.slicer.model()
300        if self.slicer_widget:
301            self.slicer_widget.setModel(self.param_model)
302
303    def onSectorView(self):
304        """
305        Perform sector averaging on Q and draw sector slicer
306        """
307        self.setSlicer(slicer=SectorInteractor)
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    def replacePlot(self, id, new_plot):
488        """
489        Replace data in current chart.
490        This effectlvely refreshes the chart with changes to one of its plots
491        """
492        self.plot(data=new_plot)
493
494
495class Plotter2D(QtGui.QDialog, Plotter2DWidget):
496    """
497    Plotter widget implementation
498    """
499    def __init__(self, parent=None, quickplot=False, dimension=2):
500        QtGui.QDialog.__init__(self)
501        Plotter2DWidget.__init__(self, manager=parent, quickplot=quickplot, dimension=dimension)
502        icon = QtGui.QIcon()
503        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
504        self.setWindowIcon(icon)
Note: See TracBrowser for help on using the repository browser.