source: sasview/src/sas/qtgui/Plotting/Plotter2D.py @ 895e7359

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

Converted more syntax not covered by 2to3

  • Property mode set to 100644
File size: 18.0 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        GuiUtils.updateModelItemWithPlot(self._item, new_plot, new_plot.id)
284        self.manager.communicator.plotUpdateSignal.emit([new_plot])
285
286    def setSlicer(self, slicer):
287        """
288        Clear the previous slicer and create a new one.
289        slicer: slicer class to create
290        """
291        # Clear current slicer
292        if self.slicer is not None:
293            self.slicer.clear()
294        # Create a new slicer
295        self.slicer_z += 1
296        self.slicer = slicer(self, self.ax, item=self._item, zorder=self.slicer_z)
297        self.ax.set_ylim(self.data.ymin, self.data.ymax)
298        self.ax.set_xlim(self.data.xmin, self.data.xmax)
299        # Draw slicer
300        self.figure.canvas.draw()
301        self.slicer.update()
302
303        # Reset the model on the Edit slicer parameters widget
304        self.param_model = self.slicer.model()
305        if self.slicer_widget:
306            self.slicer_widget.setModel(self.param_model)
307
308    def onSectorView(self):
309        """
310        Perform sector averaging on Q and draw sector slicer
311        """
312        self.setSlicer(slicer=SectorInteractor)
313
314    def onAnnulusView(self):
315        """
316        Perform sector averaging on Phi and draw annulus slicer
317        """
318        self.setSlicer(slicer=AnnulusInteractor)
319
320    def onBoxSum(self):
321        """
322        Perform 2D Data averaging Qx and Qy.
323        Display box slicer details.
324        """
325        self.onClearSlicer()
326        self.slicer_z += 1
327        self.slicer = BoxSumCalculator(self, self.ax, zorder=self.slicer_z)
328
329        self.ax.set_ylim(self.data.ymin, self.data.ymax)
330        self.ax.set_xlim(self.data.xmin, self.data.xmax)
331        self.figure.canvas.draw()
332        self.slicer.update()
333
334        # Get the BoxSumCalculator model.
335        self.box_sum_model = self.slicer.model()
336        # Pass the BoxSumCalculator model to the BoxSum widget
337        self.boxwidget = BoxSum(self, model=self.box_sum_model)
338        # Add the plot to the workspace
339        self.manager.parent.workspace().addWindow(self.boxwidget)
340        self.boxwidget.show()
341
342    def onBoxAveragingX(self):
343        """
344        Perform 2D data averaging on Qx
345        Create a new slicer.
346        """
347        self.setSlicer(slicer=BoxInteractorX)
348
349    def onBoxAveragingY(self):
350        """
351        Perform 2D data averaging on Qy
352        Create a new slicer .
353        """
354        self.setSlicer(slicer=BoxInteractorY)
355
356    def onColorMap(self):
357        """
358        Display the color map dialog and modify the plot's map accordingly
359        """
360        color_map_dialog = ColorMap(self, cmap=self.cmap,
361                                    vmin=self.vmin,
362                                    vmax=self.vmax,
363                                    data=self.data)
364
365        color_map_dialog.apply_signal.connect(self.onApplyMap)
366
367        if color_map_dialog.exec_() == QtGui.QDialog.Accepted:
368            self.onApplyMap(color_map_dialog.norm(), color_map_dialog.cmap())
369
370    def onApplyMap(self, v_values, cmap):
371        """
372        Update the chart color map based on data passed from the widget
373        """
374        self.cmap = str(cmap)
375        self.vmin, self.vmax = v_values
376        # Redraw the chart with new cmap
377        self.plot()
378
379    def showPlot(self, data, qx_data, qy_data, xmin, xmax, ymin, ymax,
380                 zmin, zmax, label='data2D', cmap=DEFAULT_CMAP, show_colorbar=True):
381        """
382        Render and show the current data
383        """
384        self.qx_data = qx_data
385        self.qy_data = qy_data
386        self.xmin = xmin
387        self.xmax = xmax
388        self.ymin = ymin
389        self.ymax = ymax
390        self.zmin = zmin
391        self.zmax = zmax
392        # If we don't have any data, skip.
393        if data is None:
394            return
395        if data.ndim == 0:
396            return
397        elif data.ndim == 1:
398            output = PlotUtilities.build_matrix(data, self.qx_data, self.qy_data)
399        else:
400            output = copy.deepcopy(data)
401
402        zmin_temp = self.zmin
403        # check scale
404        if self.scale == 'log_{10}':
405            try:
406                if  self.zmin <= 0  and len(output[output > 0]) > 0:
407                    zmin_temp = self.zmin
408                    output[output > 0] = numpy.log10(output[output > 0])
409                elif self.zmin <= 0:
410                    zmin_temp = self.zmin
411                    output[output > 0] = numpy.zeros(len(output))
412                    output[output <= 0] = MIN_Z
413                else:
414                    zmin_temp = self.zmin
415                    output[output > 0] = numpy.log10(output[output > 0])
416            except:
417                #Too many problems in 2D plot with scale
418                output[output > 0] = numpy.log10(output[output > 0])
419                pass
420
421        self.cmap = cmap
422        if self.dimension != 3:
423            #Re-adjust colorbar
424            self.figure.subplots_adjust(left=0.2, right=.8, bottom=.2)
425
426            zmax_temp = self.zmax
427            if self.vmin is not None:
428                zmin_temp = self.vmin
429                zmax_temp = self.vmax
430
431            im = self.ax.imshow(output, interpolation='nearest',
432                                # origin='lower',
433                                vmin=zmin_temp, vmax=zmax_temp,
434                                cmap=self.cmap,
435                                extent=(self.xmin, self.xmax,
436                                        self.ymin, self.ymax))
437
438            cbax = self.figure.add_axes([0.84, 0.2, 0.02, 0.7])
439
440            # Current labels for axes
441            self.ax.set_ylabel(self.y_label)
442            self.ax.set_xlabel(self.x_label)
443
444            # Title only for regular charts
445            if not self.quickplot:
446                self.ax.set_title(label=self._title)
447
448            if cbax is None:
449                ax.set_frame_on(False)
450                cb = self.figure.colorbar(im, shrink=0.8, aspect=20)
451            else:
452                cb = self.figure.colorbar(im, cax=cbax)
453
454            cb.update_bruteforce(im)
455            cb.set_label('$' + self.scale + '$')
456
457            self.vmin = cb.vmin
458            self.vmax = cb.vmax
459
460            if show_colorbar is False:
461                cb.remove()
462
463        else:
464            # clear the previous 2D from memory
465            self.figure.clear()
466
467            self.figure.subplots_adjust(left=0.1, right=.8, bottom=.1)
468
469            data_x, data_y = numpy.meshgrid(self._data.x_bins[0:-1],
470                                            self._data.y_bins[0:-1])
471
472            ax = Axes3D(self.figure)
473
474            # Disable rotation for large sets.
475            # TODO: Define "large" for a dataset
476            SET_TOO_LARGE = 500
477            if len(data_x) > SET_TOO_LARGE:
478                ax.disable_mouse_rotation()
479
480            self.figure.canvas.resizing = False
481            im = ax.plot_surface(data_x, data_y, output, rstride=1,
482                                 cstride=1, cmap=cmap,
483                                 linewidth=0, antialiased=False)
484            self.ax.set_axis_off()
485
486        if self.dimension != 3:
487            self.figure.canvas.draw_idle()
488        else:
489            self.figure.canvas.draw()
490
491    def update(self):
492        self.figure.canvas.draw()
493
494    def draw(self):
495        self.figure.canvas.draw()
496
497    def replacePlot(self, id, new_plot):
498        """
499        Replace data in current chart.
500        This effectively refreshes the chart with changes to one of its plots
501        """
502        self.plot(data=new_plot)
503
504
505class Plotter2D(QtGui.QDialog, Plotter2DWidget):
506    """
507    Plotter widget implementation
508    """
509    def __init__(self, parent=None, quickplot=False, dimension=2):
510        QtGui.QDialog.__init__(self)
511        Plotter2DWidget.__init__(self, manager=parent, quickplot=quickplot, dimension=dimension)
512        icon = QtGui.QIcon()
513        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
514        self.setWindowIcon(icon)
Note: See TracBrowser for help on using the repository browser.