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

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

Slicer Parameters should also close the corresponding subwindow on exit.
Also: fixed toFloat() conversion.
SASVIEW-1115

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