source: sasview/src/sas/qtgui/Plotter2D.py @ 9f25bce

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

Towards more 1D plots responding to data change.
Minor bug fixes.

  • Property mode set to 100644
File size: 17.8 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
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    def onSectorView(self):
303        """
304        Perform sector averaging on Q and draw sector slicer
305        """
306        self.setSlicer(slicer=SectorInteractor)
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    def replacePlot(self, id, new_plot):
487        """
488        Replace data in current chart.
489        This effectlvely refreshes the chart with changes to one of its plots
490        """
491        self.plot(data=new_plot)
492
493
494class Plotter2D(QtGui.QDialog, Plotter2DWidget):
495    """
496    Plotter widget implementation
497    """
498    def __init__(self, parent=None, quickplot=False, dimension=2):
499        QtGui.QDialog.__init__(self)
500        Plotter2DWidget.__init__(self, manager=parent, quickplot=quickplot, dimension=dimension)
501        icon = QtGui.QIcon()
502        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
503        self.setWindowIcon(icon)
Note: See TracBrowser for help on using the repository browser.