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

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

More Qt5 related fixes.

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