source: sasview/src/sas/qtgui/Plotter2D.py @ d7ff531

ESS_GUIESS_GUI_DocsESS_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 d7ff531 was 116260a, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

enable real time updates of more 1D charts

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