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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 8fad50b was 8fad50b, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Replaced pylab.cm with mpl.cm (PK's code review)

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