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

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

Close subWindow properly. SASVIEW-1115

  • Property mode set to 100644
File size: 18.8 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        self.im = None
64
65        self.manager = manager
66
67    @property
68    def data(self):
69        return self._data
70
71    @data.setter
72    def data(self, data=None):
73        """ data setter """
74        self._data = data
75        self.qx_data = data.qx_data
76        self.qy_data = data.qy_data
77        self.xmin = data.xmin
78        self.xmax = data.xmax
79        self.ymin = data.ymin
80        self.ymax = data.ymax
81        self.zmin = data.zmin
82        self.zmax = data.zmax
83        self.label = data.name
84        self.xLabel = "%s(%s)"%(data._xaxis, data._xunit)
85        self.yLabel = "%s(%s)"%(data._yaxis, data._yunit)
86        self.title(title=data.title)
87
88    def plot(self, data=None, marker=None, show_colorbar=True):
89        """
90        Plot 2D self._data
91        marker - unused
92        """
93        # Assing data
94        if isinstance(data, Data2D):
95            self.data = data
96
97        if not self._data:
98            return
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 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.close_signal.disconnect()
231            # reset slicer_widget on "Edit Slicer Parameters" window close
232            self.slicer_widget = None
233
234        self.param_model = self.slicer.model()
235        # Pass the model to the Slicer Parameters widget
236        self.slicer_widget = SlicerParameters(model=self.param_model,
237                                              validate_method=self.slicer.validate)
238        self.slicer_widget.close_signal.connect(slicer_closed)
239        # Add the plot to the workspace
240        self.manager.parent.workspace().addSubWindow(self.slicer_widget)
241
242        self.slicer_widget.show()
243
244    def onCircularAverage(self):
245        """
246        Perform circular averaging on Data2D
247        """
248        # Find the best number of bins
249        npt = numpy.sqrt(len(self.data.data[numpy.isfinite(self.data.data)]))
250        npt = numpy.floor(npt)
251        # compute the maximum radius of data2D
252        self.qmax = max(numpy.fabs(self.data.xmax),
253                        numpy.fabs(self.data.xmin))
254        self.ymax = max(numpy.fabs(self.data.ymax),
255                        numpy.fabs(self.data.ymin))
256        self.radius = numpy.sqrt(numpy.power(self.qmax, 2) + numpy.power(self.ymax, 2))
257        #Compute beam width
258        bin_width = (self.qmax + self.qmax) / npt
259        # Create data1D circular average of data2D
260        circle = CircularAverage(r_min=0, r_max=self.radius, bin_width=bin_width)
261        circ = circle(self.data)
262        dxl = circ.dxl if hasattr(circ, "dxl") else None
263        dxw = circ.dxw if hasattr(circ, "dxw") else None
264
265        new_plot = Data1D(x=circ.x, y=circ.y, dy=circ.dy, dx=circ.dx)
266        new_plot.dxl = dxl
267        new_plot.dxw = dxw
268        new_plot.name = new_plot.title = "Circ avg " + self.data.name
269        new_plot.source = self.data.source
270        new_plot.interactive = True
271        new_plot.detector = self.data.detector
272
273        # Define axes if not done yet.
274        new_plot.xaxis("\\rm{Q}", "A^{-1}")
275        if hasattr(self.data, "scale") and \
276                    self.data.scale == 'linear':
277            new_plot.ytransform = 'y'
278            new_plot.yaxis("\\rm{Residuals} ", "normalized")
279        else:
280            new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}")
281
282        new_plot.group_id = "2daverage" + self.data.name
283        new_plot.id = "Circ avg " + self.data.name
284        new_plot.is_data = True
285        GuiUtils.updateModelItemWithPlot(self._item, new_plot, new_plot.id)
286
287        self.manager.communicator.plotUpdateSignal.emit([new_plot])
288
289    def setSlicer(self, slicer):
290        """
291        Clear the previous slicer and create a new one.
292        slicer: slicer class to create
293        """
294        # Clear current slicer
295        if self.slicer is not None:
296            self.slicer.clear()
297        # Create a new slicer
298        self.slicer_z += 1
299        self.slicer = slicer(self, self.ax, item=self._item, zorder=self.slicer_z)
300        self.ax.set_ylim(self.data.ymin, self.data.ymax)
301        self.ax.set_xlim(self.data.xmin, self.data.xmax)
302        # Draw slicer
303        self.figure.canvas.draw()
304        self.slicer.update()
305
306        # Reset the model on the Edit slicer parameters widget
307        self.param_model = self.slicer.model()
308        if self.slicer_widget:
309            self.slicer_widget.setModel(self.param_model)
310
311    def onSectorView(self):
312        """
313        Perform sector averaging on Q and draw sector slicer
314        """
315        self.setSlicer(slicer=SectorInteractor)
316
317    def onAnnulusView(self):
318        """
319        Perform sector averaging on Phi and draw annulus slicer
320        """
321        self.setSlicer(slicer=AnnulusInteractor)
322
323    def onBoxSum(self):
324        """
325        Perform 2D Data averaging Qx and Qy.
326        Display box slicer details.
327        """
328        self.onClearSlicer()
329        self.slicer_z += 1
330        self.slicer = BoxSumCalculator(self, self.ax, zorder=self.slicer_z)
331
332        self.ax.set_ylim(self.data.ymin, self.data.ymax)
333        self.ax.set_xlim(self.data.xmin, self.data.xmax)
334        self.figure.canvas.draw()
335        self.slicer.update()
336
337        def boxWidgetClosed():
338            # Need to disconnect the signal!!
339            self.boxwidget.closeWidgetSignal.disconnect()
340            # reset box on "Edit Slicer Parameters" window close
341            self.manager.parent.workspace().removeSubWindow(self.boxwidget_subwindow)
342            self.boxwidget = None
343
344        # Get the BoxSumCalculator model.
345        self.box_sum_model = self.slicer.model()
346        # Pass the BoxSumCalculator model to the BoxSum widget
347        self.boxwidget = BoxSum(self, model=self.box_sum_model)
348        # Add the plot to the workspace
349        self.boxwidget_subwindow = self.manager.parent.workspace().addSubWindow(self.boxwidget)
350        self.boxwidget.closeWidgetSignal.connect(boxWidgetClosed)
351
352        self.boxwidget.show()
353
354    def onBoxAveragingX(self):
355        """
356        Perform 2D data averaging on Qx
357        Create a new slicer.
358        """
359        self.setSlicer(slicer=BoxInteractorX)
360
361    def onBoxAveragingY(self):
362        """
363        Perform 2D data averaging on Qy
364        Create a new slicer .
365        """
366        self.setSlicer(slicer=BoxInteractorY)
367
368    def onColorMap(self):
369        """
370        Display the color map dialog and modify the plot's map accordingly
371        """
372        color_map_dialog = ColorMap(self, cmap=self.cmap,
373                                    vmin=self.vmin,
374                                    vmax=self.vmax,
375                                    data=self.data)
376
377        color_map_dialog.apply_signal.connect(self.onApplyMap)
378
379        if color_map_dialog.exec_() == QtWidgets.QDialog.Accepted:
380            self.onApplyMap(color_map_dialog.norm(), color_map_dialog.cmap())
381
382    def onApplyMap(self, v_values, cmap):
383        """
384        Update the chart color map based on data passed from the widget
385        """
386        self.cmap = str(cmap)
387        self.vmin, self.vmax = v_values
388        # Redraw the chart with new cmap
389        self.plot()
390
391    def showPlot(self, data, qx_data, qy_data, xmin, xmax, ymin, ymax,
392                 zmin, zmax, label='data2D', cmap=DEFAULT_CMAP, show_colorbar=True):
393        """
394        Render and show the current data
395        """
396        self.qx_data = qx_data
397        self.qy_data = qy_data
398        self.xmin = xmin
399        self.xmax = xmax
400        self.ymin = ymin
401        self.ymax = ymax
402        self.zmin = zmin
403        self.zmax = zmax
404        # If we don't have any data, skip.
405        if data is None:
406            return
407        if data.ndim == 0:
408            return
409        elif data.ndim == 1:
410            output = PlotUtilities.build_matrix(data, self.qx_data, self.qy_data)
411        else:
412            output = copy.deepcopy(data)
413
414        # get the x and y_bin arrays.
415        x_bins, y_bins = PlotUtilities.get_bins(self.qx_data, self.qy_data)
416        self._data.x_bins = x_bins
417        self._data.y_bins = y_bins
418
419        zmin_temp = self.zmin
420        # check scale
421        if self.scale == 'log_{10}':
422            try:
423                if  self.zmin <= 0  and len(output[output > 0]) > 0:
424                    zmin_temp = self.zmin
425                    output[output > 0] = numpy.log10(output[output > 0])
426                elif self.zmin <= 0:
427                    zmin_temp = self.zmin
428                    output[output > 0] = numpy.zeros(len(output))
429                    output[output <= 0] = MIN_Z
430                else:
431                    zmin_temp = self.zmin
432                    output[output > 0] = numpy.log10(output[output > 0])
433            except:
434                #Too many problems in 2D plot with scale
435                output[output > 0] = numpy.log10(output[output > 0])
436                pass
437
438        self.cmap = cmap
439        if self.dimension != 3:
440            #Re-adjust colorbar
441            self.figure.subplots_adjust(left=0.2, right=.8, bottom=.2)
442
443            zmax_temp = self.zmax
444            if self.vmin is not None:
445                zmin_temp = self.vmin
446                zmax_temp = self.vmax
447            if self.im is not None:
448                self.im.set_data(output)
449            else:
450                self.im = self.ax.imshow(output, interpolation='nearest',
451                                # origin='lower',
452                                vmin=zmin_temp, vmax=zmax_temp,
453                                cmap=self.cmap,
454                                extent=(self.xmin, self.xmax,
455                                        self.ymin, self.ymax))
456
457            cbax = self.figure.add_axes([0.88, 0.2, 0.02, 0.7])
458
459            # Current labels for axes
460            self.ax.set_ylabel(self.y_label)
461            self.ax.set_xlabel(self.x_label)
462
463            # Title only for regular charts
464            if not self.quickplot:
465                self.ax.set_title(label=self._title)
466
467            if cbax is None:
468                ax.set_frame_on(False)
469                cb = self.figure.colorbar(self.im, shrink=0.8, aspect=20)
470            else:
471                cb = self.figure.colorbar(self.im, cax=cbax)
472
473            cb.update_bruteforce(self.im)
474            cb.set_label('$' + self.scale + '$')
475
476            self.vmin = cb.vmin
477            self.vmax = cb.vmax
478
479            if show_colorbar is False:
480                cb.remove()
481
482        else:
483            # clear the previous 2D from memory
484            self.figure.clear()
485
486            self.figure.subplots_adjust(left=0.1, right=.8, bottom=.1)
487
488            data_x, data_y = numpy.meshgrid(self._data.x_bins[0:-1],
489                                            self._data.y_bins[0:-1])
490
491            ax = Axes3D(self.figure)
492
493            # Disable rotation for large sets.
494            # TODO: Define "large" for a dataset
495            SET_TOO_LARGE = 500
496            if len(data_x) > SET_TOO_LARGE:
497                ax.disable_mouse_rotation()
498
499            self.figure.canvas.resizing = False
500            im = ax.plot_surface(data_x, data_y, output, rstride=1,
501                                 cstride=1, cmap=cmap,
502                                 linewidth=0, antialiased=False)
503            self.ax.set_axis_off()
504
505        if self.dimension != 3:
506            self.figure.canvas.draw_idle()
507        else:
508            self.figure.canvas.draw()
509
510    def update(self):
511        self.figure.canvas.draw()
512
513    def draw(self):
514        self.figure.canvas.draw()
515
516    def replacePlot(self, id, new_plot):
517        """
518        Replace data in current chart.
519        This effectively refreshes the chart with changes to one of its plots
520        """
521        self.plot(data=new_plot)
522
523
524class Plotter2D(QtWidgets.QDialog, Plotter2DWidget):
525    """
526    Plotter widget implementation
527    """
528    def __init__(self, parent=None, quickplot=False, dimension=2):
529        QtWidgets.QDialog.__init__(self)
530        Plotter2DWidget.__init__(self, manager=parent, quickplot=quickplot, dimension=dimension)
531        icon = QtGui.QIcon()
532        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
533        self.setWindowIcon(icon)
Note: See TracBrowser for help on using the repository browser.