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

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

Merged ESS_GUI_image_viewer

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