source: sasview/src/sas/qtgui/Plotting/Plotter2D.py @ 6b43c58

Last change on this file since 6b43c58 was f4a1433, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Merge branch 'master' into ESS_GUI

  • Property mode set to 100644
File size: 18.1 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
12from sas.sascalc.dataloader.manipulations import CircularAverage
13
14from sas.qtgui.Plotting.PlotterData import Data1D
15from sas.qtgui.Plotting.PlotterData import Data2D
16
17import sas.qtgui.Plotting.PlotUtilities as PlotUtilities
18import sas.qtgui.Utilities.GuiUtils as GuiUtils
19from sas.qtgui.Plotting.PlotterBase import PlotterBase
20from sas.qtgui.Plotting.ColorMap import ColorMap
21from sas.qtgui.Plotting.BoxSum import BoxSum
22from sas.qtgui.Plotting.SlicerParameters import SlicerParameters
23from sas.qtgui.Plotting.Binder import BindArtist
24
25# TODO: move to sas.qtgui namespace
26from sas.qtgui.Plotting.Slicers.BoxSlicer import BoxInteractorX
27from sas.qtgui.Plotting.Slicers.BoxSlicer import BoxInteractorY
28from sas.qtgui.Plotting.Slicers.AnnulusSlicer import AnnulusInteractor
29from sas.qtgui.Plotting.Slicers.SectorSlicer import SectorInteractor
30from sas.qtgui.Plotting.Slicers.BoxSum import BoxSumCalculator
31
32# Minimum value of Z for which we will present data.
33MIN_Z = -32
34
35class Plotter2DWidget(PlotterBase):
36    """
37    2D Plot widget for use with a QDialog
38    """
39    def __init__(self, parent=None, manager=None, quickplot=False, dimension=2):
40        self.dimension = dimension
41        super(Plotter2DWidget, self).__init__(parent, manager=manager, quickplot=quickplot)
42
43        self.cmap = DEFAULT_CMAP.name
44        # Default scale
45        self.scale = 'log_{10}'
46        # to set the order of lines drawn first.
47        self.slicer_z = 5
48        # Reference to the current slicer
49        self.slicer = None
50        self.slicer_widget = None
51        # Create Artist and bind it
52        self.connect = BindArtist(self.figure)
53        self.vmin = None
54        self.vmax = None
55
56        self.manager = manager
57
58    @property
59    def data(self):
60        return self._data
61
62    @data.setter
63    def data(self, data=None):
64        """ data setter """
65        self._data = data
66        self.qx_data = data.qx_data
67        self.qy_data = data.qy_data
68        self.xmin = data.xmin
69        self.xmax = data.xmax
70        self.ymin = data.ymin
71        self.ymax = data.ymax
72        self.zmin = data.zmin
73        self.zmax = data.zmax
74        self.label = data.name
75        self.xLabel = "%s(%s)"%(data._xaxis, data._xunit)
76        self.yLabel = "%s(%s)"%(data._yaxis, data._yunit)
77        self.title(title=data.title)
78
79    @property
80    def item(self):
81        ''' getter for this plot's QStandardItem '''
82        return self._item
83
84    @item.setter
85    def item(self, item=None):
86        ''' setter for this plot's QStandardItem '''
87        self._item = item
88
89    def plot(self, data=None, marker=None, show_colorbar=True):
90        """
91        Plot 2D self._data
92        marker - unused
93        """
94        # Assing data
95        if isinstance(data, Data2D):
96            self.data = data
97
98        assert self._data
99
100        # Toggle the scale
101        zmin_2D_temp, zmax_2D_temp = self.calculateDepth()
102
103        # Prepare and show the plot
104        self.showPlot(data=self.data.data,
105                      qx_data=self.qx_data,
106                      qy_data=self.qy_data,
107                      xmin=self.xmin,
108                      xmax=self.xmax,
109                      ymin=self.ymin, ymax=self.ymax,
110                      cmap=self.cmap, zmin=zmin_2D_temp,
111                      zmax=zmax_2D_temp, show_colorbar=show_colorbar)
112
113    def calculateDepth(self):
114        """
115        Re-calculate the plot depth parameters depending on the scale
116        """
117        # Toggle the scale
118        zmin_temp = self.zmin
119        zmax_temp = self.zmax
120        # self.scale predefined in the baseclass
121        if self.scale == 'log_{10}':
122            if self.zmin is not None:
123                zmin_temp = numpy.power(10, self.zmin)
124            if self.zmax is not None:
125                zmax_temp = numpy.power(10, self.zmax)
126        else:
127            if self.zmin is not None:
128                # min log value: no log(negative)
129                zmin_temp = MIN_Z if self.zmin <= 0 else numpy.log10(self.zmin)
130            if self.zmax is not None:
131                zmax_temp = numpy.log10(self.zmax)
132
133        return (zmin_temp, zmax_temp)
134
135
136    def createContextMenu(self):
137        """
138        Define common context menu and associated actions for the MPL widget
139        """
140        self.defaultContextMenu()
141
142        self.contextMenu.addSeparator()
143        self.actionDataInfo = self.contextMenu.addAction("&DataInfo")
144        self.actionDataInfo.triggered.connect(
145             functools.partial(self.onDataInfo, self.data))
146
147        self.actionSavePointsAsFile = self.contextMenu.addAction("&Save Points as a File")
148        self.actionSavePointsAsFile.triggered.connect(
149             functools.partial(self.onSavePoints, self.data))
150        self.contextMenu.addSeparator()
151
152        self.actionCircularAverage = self.contextMenu.addAction("&Perform Circular Average")
153        self.actionCircularAverage.triggered.connect(self.onCircularAverage)
154
155        self.actionSectorView = self.contextMenu.addAction("&Sector [Q View]")
156        self.actionSectorView.triggered.connect(self.onSectorView)
157        self.actionAnnulusView = self.contextMenu.addAction("&Annulus [Phi View]")
158        self.actionAnnulusView.triggered.connect(self.onAnnulusView)
159        self.actionBoxSum = self.contextMenu.addAction("&Box Sum")
160        self.actionBoxSum.triggered.connect(self.onBoxSum)
161        self.actionBoxAveragingX = self.contextMenu.addAction("&Box Averaging in Qx")
162        self.actionBoxAveragingX.triggered.connect(self.onBoxAveragingX)
163        self.actionBoxAveragingY = self.contextMenu.addAction("&Box Averaging in Qy")
164        self.actionBoxAveragingY.triggered.connect(self.onBoxAveragingY)
165        # Additional items for slicer interaction
166        if self.slicer:
167            self.actionClearSlicer = self.contextMenu.addAction("&Clear Slicer")
168            self.actionClearSlicer.triggered.connect(self.onClearSlicer)
169            if self.slicer.__class__.__name__ != "BoxSumCalculator":
170                self.actionEditSlicer = self.contextMenu.addAction("&Edit Slicer Parameters")
171                self.actionEditSlicer.triggered.connect(self.onEditSlicer)
172        self.contextMenu.addSeparator()
173        self.actionColorMap = self.contextMenu.addAction("&2D Color Map")
174        self.actionColorMap.triggered.connect(self.onColorMap)
175        self.contextMenu.addSeparator()
176        self.actionChangeScale = self.contextMenu.addAction("Toggle Linear/Log Scale")
177        self.actionChangeScale.triggered.connect(self.onToggleScale)
178
179    def createContextMenuQuick(self):
180        """
181        Define context menu and associated actions for the quickplot MPL widget
182        """
183        self.defaultContextMenu()
184
185        if self.dimension == 2:
186            self.actionToggleGrid = self.contextMenu.addAction("Toggle Grid On/Off")
187            self.contextMenu.addSeparator()
188        self.actionChangeScale = self.contextMenu.addAction("Toggle Linear/Log Scale")
189
190        # Define the callbacks
191        self.actionChangeScale.triggered.connect(self.onToggleScale)
192        if self.dimension == 2:
193            self.actionToggleGrid.triggered.connect(self.onGridToggle)
194
195    def onToggleScale(self, event):
196        """
197        Toggle axis and replot image
198        """
199        # self.scale predefined in the baseclass
200        if self.scale == 'log_{10}':
201            self.scale = 'linear'
202        else:
203            self.scale = 'log_{10}'
204
205        self.plot()
206
207    def onClearSlicer(self):
208        """
209        Remove all sclicers from the chart
210        """
211        if self.slicer is None:
212            return
213
214        self.slicer.clear()
215        self.canvas.draw()
216        self.slicer = None
217
218    def onEditSlicer(self):
219        """
220        Present a small dialog for manipulating the current slicer
221        """
222        assert self.slicer
223        # Only show the dialog if not currently shown
224        if self.slicer_widget:
225            return
226        def slicer_closed():
227            # Need to disconnect the signal!!
228            self.slicer_widget.close_signal.disconnect()
229            # reset slicer_widget on "Edit Slicer Parameters" window close
230            self.slicer_widget = None
231
232        self.param_model = self.slicer.model()
233        # Pass the model to the Slicer Parameters widget
234        self.slicer_widget = SlicerParameters(model=self.param_model,
235                                              validate_method=self.slicer.validate)
236        self.slicer_widget.close_signal.connect(slicer_closed)
237        # Add the plot to the workspace
238        self.manager.parent.workspace().addWindow(self.slicer_widget)
239
240        self.slicer_widget.show()
241
242    def onCircularAverage(self):
243        """
244        Perform circular averaging on Data2D
245        """
246        # Find the best number of bins
247        npt = numpy.sqrt(len(self.data.data[numpy.isfinite(self.data.data)]))
248        npt = numpy.floor(npt)
249        # compute the maximum radius of data2D
250        self.qmax = max(numpy.fabs(self.data.xmax),
251                        numpy.fabs(self.data.xmin))
252        self.ymax = max(numpy.fabs(self.data.ymax),
253                        numpy.fabs(self.data.ymin))
254        self.radius = numpy.sqrt(numpy.power(self.qmax, 2) + numpy.power(self.ymax, 2))
255        #Compute beam width
256        bin_width = (self.qmax + self.qmax) / npt
257        # Create data1D circular average of data2D
258        circle = CircularAverage(r_min=0, r_max=self.radius, bin_width=bin_width)
259        circ = circle(self.data)
260        dxl = circ.dxl if hasattr(circ, "dxl") else None
261        dxw = circ.dxw if hasattr(circ, "dxw") else None
262
263        new_plot = Data1D(x=circ.x, y=circ.y, dy=circ.dy, dx=circ.dx)
264        new_plot.dxl = dxl
265        new_plot.dxw = dxw
266        new_plot.name = new_plot.title = "Circ avg " + self.data.name
267        new_plot.source = self.data.source
268        new_plot.interactive = True
269        new_plot.detector = self.data.detector
270
271        # Define axes if not done yet.
272        new_plot.xaxis("\\rm{Q}", "A^{-1}")
273        if hasattr(self.data, "scale") and \
274                    self.data.scale == 'linear':
275            new_plot.ytransform = 'y'
276            new_plot.yaxis("\\rm{Residuals} ", "normalized")
277        else:
278            new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}")
279
280        new_plot.group_id = "2daverage" + self.data.name
281        new_plot.id = "Circ avg " + self.data.name
282        new_plot.is_data = True
283        variant_plot = QtCore.QVariant(new_plot)
284        GuiUtils.updateModelItemWithPlot(self._item, variant_plot, new_plot.id)
285        self.manager.communicator.plotUpdateSignal.emit([new_plot])
286
287    def setSlicer(self, slicer):
288        """
289        Clear the previous slicer and create a new one.
290        slicer: slicer class to create
291        """
292        # Clear current slicer
293        if self.slicer is not None:
294            self.slicer.clear()
295        # Create a new slicer
296        self.slicer_z += 1
297        self.slicer = slicer(self, self.ax, item=self._item, zorder=self.slicer_z)
298        self.ax.set_ylim(self.data.ymin, self.data.ymax)
299        self.ax.set_xlim(self.data.xmin, self.data.xmax)
300        # Draw slicer
301        self.figure.canvas.draw()
302        self.slicer.update()
303
304        # Reset the model on the Edit slicer parameters widget
305        self.param_model = self.slicer.model()
306        if self.slicer_widget:
307            self.slicer_widget.setModel(self.param_model)
308
309    def onSectorView(self):
310        """
311        Perform sector averaging on Q and draw sector slicer
312        """
313        self.setSlicer(slicer=SectorInteractor)
314
315    def onAnnulusView(self):
316        """
317        Perform sector averaging on Phi and draw annulus slicer
318        """
319        self.setSlicer(slicer=AnnulusInteractor)
320
321    def onBoxSum(self):
322        """
323        Perform 2D Data averaging Qx and Qy.
324        Display box slicer details.
325        """
326        self.onClearSlicer()
327        self.slicer_z += 1
328        self.slicer = BoxSumCalculator(self, self.ax, zorder=self.slicer_z)
329
330        self.ax.set_ylim(self.data.ymin, self.data.ymax)
331        self.ax.set_xlim(self.data.xmin, self.data.xmax)
332        self.figure.canvas.draw()
333        self.slicer.update()
334
335        # Get the BoxSumCalculator model.
336        self.box_sum_model = self.slicer.model()
337        # Pass the BoxSumCalculator model to the BoxSum widget
338        self.boxwidget = BoxSum(self, model=self.box_sum_model)
339        # Add the plot to the workspace
340        self.manager.parent.workspace().addWindow(self.boxwidget)
341        self.boxwidget.show()
342
343    def onBoxAveragingX(self):
344        """
345        Perform 2D data averaging on Qx
346        Create a new slicer.
347        """
348        self.setSlicer(slicer=BoxInteractorX)
349
350    def onBoxAveragingY(self):
351        """
352        Perform 2D data averaging on Qy
353        Create a new slicer .
354        """
355        self.setSlicer(slicer=BoxInteractorY)
356
357    def onColorMap(self):
358        """
359        Display the color map dialog and modify the plot's map accordingly
360        """
361        color_map_dialog = ColorMap(self, cmap=self.cmap,
362                                    vmin=self.vmin,
363                                    vmax=self.vmax,
364                                    data=self.data)
365
366        color_map_dialog.apply_signal.connect(self.onApplyMap)
367
368        if color_map_dialog.exec_() == QtGui.QDialog.Accepted:
369            self.onApplyMap(color_map_dialog.norm(), color_map_dialog.cmap())
370
371    def onApplyMap(self, v_values, cmap):
372        """
373        Update the chart color map based on data passed from the widget
374        """
375        self.cmap = str(cmap)
376        self.vmin, self.vmax = v_values
377        # Redraw the chart with new cmap
378        self.plot()
379
380    def showPlot(self, data, qx_data, qy_data, xmin, xmax, ymin, ymax,
381                 zmin, zmax, label='data2D', cmap=DEFAULT_CMAP, show_colorbar=True):
382        """
383        Render and show the current data
384        """
385        self.qx_data = qx_data
386        self.qy_data = qy_data
387        self.xmin = xmin
388        self.xmax = xmax
389        self.ymin = ymin
390        self.ymax = ymax
391        self.zmin = zmin
392        self.zmax = zmax
393        # If we don't have any data, skip.
394        if data is None:
395            return
396        if data.ndim == 0:
397            return
398        elif data.ndim == 1:
399            output = PlotUtilities.build_matrix(data, self.qx_data, self.qy_data)
400        else:
401            output = copy.deepcopy(data)
402
403        zmin_temp = self.zmin
404        # check scale
405        if self.scale == 'log_{10}':
406            try:
407                if  self.zmin <= 0  and len(output[output > 0]) > 0:
408                    zmin_temp = self.zmin
409                    output[output > 0] = numpy.log10(output[output > 0])
410                elif self.zmin <= 0:
411                    zmin_temp = self.zmin
412                    output[output > 0] = numpy.zeros(len(output))
413                    output[output <= 0] = MIN_Z
414                else:
415                    zmin_temp = self.zmin
416                    output[output > 0] = numpy.log10(output[output > 0])
417            except:
418                #Too many problems in 2D plot with scale
419                output[output > 0] = numpy.log10(output[output > 0])
420                pass
421
422        self.cmap = cmap
423        if self.dimension != 3:
424            #Re-adjust colorbar
425            self.figure.subplots_adjust(left=0.2, right=.8, bottom=.2)
426
427            zmax_temp = self.zmax
428            if self.vmin is not None:
429                zmin_temp = self.vmin
430                zmax_temp = self.vmax
431
432            im = self.ax.imshow(output, interpolation='nearest',
433                                # origin='lower',
434                                vmin=zmin_temp, vmax=zmax_temp,
435                                cmap=self.cmap,
436                                extent=(self.xmin, self.xmax,
437                                        self.ymin, self.ymax))
438
439            cbax = self.figure.add_axes([0.84, 0.2, 0.02, 0.7])
440
441            # Current labels for axes
442            self.ax.set_ylabel(self.y_label)
443            self.ax.set_xlabel(self.x_label)
444
445            # Title only for regular charts
446            if not self.quickplot:
447                self.ax.set_title(label=self._title)
448
449            if cbax is None:
450                ax.set_frame_on(False)
451                cb = self.figure.colorbar(im, shrink=0.8, aspect=20)
452            else:
453                cb = self.figure.colorbar(im, cax=cbax)
454
455            cb.update_bruteforce(im)
456            cb.set_label('$' + self.scale + '$')
457
458            self.vmin = cb.vmin
459            self.vmax = cb.vmax
460
461            if show_colorbar is False:
462                cb.remove()
463
464        else:
465            # clear the previous 2D from memory
466            self.figure.clear()
467
468            self.figure.subplots_adjust(left=0.1, right=.8, bottom=.1)
469
470            data_x, data_y = numpy.meshgrid(self._data.x_bins[0:-1],
471                                            self._data.y_bins[0:-1])
472
473            ax = Axes3D(self.figure)
474
475            # Disable rotation for large sets.
476            # TODO: Define "large" for a dataset
477            SET_TOO_LARGE = 500
478            if len(data_x) > SET_TOO_LARGE:
479                ax.disable_mouse_rotation()
480
481            self.figure.canvas.resizing = False
482            im = ax.plot_surface(data_x, data_y, output, rstride=1,
483                                 cstride=1, cmap=cmap,
484                                 linewidth=0, antialiased=False)
485            self.ax.set_axis_off()
486
487        if self.dimension != 3:
488            self.figure.canvas.draw_idle()
489        else:
490            self.figure.canvas.draw()
491
492    def update(self):
493        self.figure.canvas.draw()
494
495    def draw(self):
496        self.figure.canvas.draw()
497
498    def replacePlot(self, id, new_plot):
499        """
500        Replace data in current chart.
501        This effectively refreshes the chart with changes to one of its plots
502        """
503        self.plot(data=new_plot)
504
505
506class Plotter2D(QtGui.QDialog, Plotter2DWidget):
507    """
508    Plotter widget implementation
509    """
510    def __init__(self, parent=None, quickplot=False, dimension=2):
511        QtGui.QDialog.__init__(self)
512        Plotter2DWidget.__init__(self, manager=parent, quickplot=quickplot, dimension=dimension)
513        icon = QtGui.QIcon()
514        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
515        self.setWindowIcon(icon)
Note: See TracBrowser for help on using the repository browser.