source: sasview/src/sas/qtgui/Plotting/Plotter2D.py @ 6964d44

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

Removed qtgui dependency on sasgui and wx SASVIEW-590

  • 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
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):
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)
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):
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 == 1:
397            output = PlotUtilities.build_matrix(data, self.qx_data, self.qy_data)
398        else:
399            output = copy.deepcopy(data)
400
401        zmin_temp = self.zmin
402        # check scale
403        if self.scale == 'log_{10}':
404            try:
405                if  self.zmin <= 0  and len(output[output > 0]) > 0:
406                    zmin_temp = self.zmin
407                    output[output > 0] = numpy.log10(output[output > 0])
408                elif self.zmin <= 0:
409                    zmin_temp = self.zmin
410                    output[output > 0] = numpy.zeros(len(output))
411                    output[output <= 0] = MIN_Z
412                else:
413                    zmin_temp = self.zmin
414                    output[output > 0] = numpy.log10(output[output > 0])
415            except:
416                #Too many problems in 2D plot with scale
417                output[output > 0] = numpy.log10(output[output > 0])
418                pass
419
420        self.cmap = cmap
421        if self.dimension != 3:
422            #Re-adjust colorbar
423            self.figure.subplots_adjust(left=0.2, right=.8, bottom=.2)
424
425            zmax_temp = self.zmax
426            if self.vmin is not None:
427                zmin_temp = self.vmin
428                zmax_temp = self.vmax
429
430            im = self.ax.imshow(output, interpolation='nearest',
431                                origin='lower',
432                                vmin=zmin_temp, vmax=zmax_temp,
433                                cmap=self.cmap,
434                                extent=(self.xmin, self.xmax,
435                                        self.ymin, self.ymax))
436
437            cbax = self.figure.add_axes([0.84, 0.2, 0.02, 0.7])
438
439            # Current labels for axes
440            self.ax.set_ylabel(self.y_label)
441            self.ax.set_xlabel(self.x_label)
442
443            # Title only for regular charts
444            if not self.quickplot:
445                self.ax.set_title(label=self._title)
446
447            if cbax is None:
448                ax.set_frame_on(False)
449                cb = self.figure.colorbar(im, shrink=0.8, aspect=20)
450            else:
451                cb = self.figure.colorbar(im, cax=cbax)
452
453            cb.update_bruteforce(im)
454            cb.set_label('$' + self.scale + '$')
455
456            self.vmin = cb.vmin
457            self.vmax = cb.vmax
458
459        else:
460            # clear the previous 2D from memory
461            self.figure.clear()
462
463            self.figure.subplots_adjust(left=0.1, right=.8, bottom=.1)
464
465            data_x, data_y = numpy.meshgrid(self._data.x_bins[0:-1],
466                                            self._data.y_bins[0:-1])
467
468            ax = Axes3D(self.figure)
469
470            # Disable rotation for large sets.
471            # TODO: Define "large" for a dataset
472            SET_TOO_LARGE = 500
473            if len(data_x) > SET_TOO_LARGE:
474                ax.disable_mouse_rotation()
475
476            self.figure.canvas.resizing = False
477            im = ax.plot_surface(data_x, data_y, output, rstride=1,
478                                 cstride=1, cmap=cmap,
479                                 linewidth=0, antialiased=False)
480            self.ax.set_axis_off()
481
482        if self.dimension != 3:
483            self.figure.canvas.draw_idle()
484        else:
485            self.figure.canvas.draw()
486
487    def update(self):
488        self.figure.canvas.draw()
489
490    def draw(self):
491        self.figure.canvas.draw()
492
493    def replacePlot(self, id, new_plot):
494        """
495        Replace data in current chart.
496        This effectlvely refreshes the chart with changes to one of its plots
497        """
498        self.plot(data=new_plot)
499
500
501class Plotter2D(QtGui.QDialog, Plotter2DWidget):
502    """
503    Plotter widget implementation
504    """
505    def __init__(self, parent=None, quickplot=False, dimension=2):
506        QtGui.QDialog.__init__(self)
507        Plotter2DWidget.__init__(self, manager=parent, quickplot=quickplot, dimension=dimension)
508        icon = QtGui.QIcon()
509        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
510        self.setWindowIcon(icon)
Note: See TracBrowser for help on using the repository browser.