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

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

More Qt5 related fixes

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