source: sasview/guiframe/local_perspectives/plotting/Plotter2D.py @ 7fff5cd

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 7fff5cd was 7fff5cd, checked in by Jae Cho <jhjcho@…>, 15 years ago

minor change

  • Property mode set to 100644
File size: 19.6 KB
RevLine 
[1bf33c1]1"""
2This software was developed by the University of Tennessee as part of the
3Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
4project funded by the US National Science Foundation.
5
6See the license text in license.txt
7
8copyright 2008, University of Tennessee
9"""
10
11
12import wx
[ea290ee]13import sys, math
[0d9dae8]14import pylab
15
[1bf33c1]16import danse.common.plottools
17from danse.common.plottools.PlotPanel import PlotPanel
18from danse.common.plottools.plottables import Graph,Data1D
[0bd2cd8]19from sans.guicomm.events import EVT_NEW_PLOT
[0d9dae8]20from sans.guicomm.events import EVT_SLICER_PARS
21from sans.guicomm.events import StatusEvent ,NewPlotEvent,SlicerEvent
22from sans.guiframe.utils import PanelMenu
[1bf33c1]23from binder import BindArtist
24from Plotter1D import ModelPanel1D
[0d9dae8]25(InternalEvent, EVT_INTERNAL)   = wx.lib.newevent.NewEvent()
26
[1bf33c1]27
[0d9dae8]28
29DEFAULT_QMAX = 0.05
[1bf33c1]30DEFAULT_QSTEP = 0.001
31DEFAULT_BEAM = 0.005
[ef0c170]32BIN_WIDTH = 1.0
[0d9dae8]33
34
35
36
[1bf33c1]37class ModelPanel2D( ModelPanel1D):
38    """
39        Plot panel for use with the GUI manager
40    """
41   
42    ## Internal name for the AUI manager
43    window_name = "plotpanel"
44    ## Title to appear on top of the window
45    window_caption = "Plot Panel"
46    ## Flag to tell the GUI manager that this panel is not
47    #  tied to any perspective
48    ALWAYS_ON = True
49    ## Group ID
50    group_id = None
51   
52   
53    def __init__(self, parent, id = -1,data2d=None, color = None,\
54        dpi = None, style = wx.NO_FULL_REPAINT_ON_RESIZE, **kwargs):
55        """
56            Initialize the panel
57        """
58        ModelPanel1D.__init__(self, parent, id = id, style = style, **kwargs)
59       
60        ## Reference to the parent window
61        self.parent = parent
[6c0568b]62        ## Dictionary containing Plottables
[1bf33c1]63        self.plots = {}
[6c0568b]64        ## Save reference of the current plotted
65        self.data2D = data2d
[1bf33c1]66        ## Unique ID (from gui_manager)
67        self.uid = None
68        ## Action IDs for internal call-backs
69        self.action_ids = {}
[6c0568b]70        ## Create Artist and bind it
[1bf33c1]71        self.connect = BindArtist(self.subplot.figure)
[6c0568b]72        ## Beam stop
[1bf33c1]73        self.beamstop_radius = DEFAULT_BEAM
[6c0568b]74        ## to set the order of lines drawn first.
[f15ed33]75        self.slicer_z = 5
[6c0568b]76        ## Reference to the current slicer
[1bf33c1]77        self.slicer = None
[6c0568b]78        ## event to send slicer info
[d468daa]79        self.Bind(EVT_INTERNAL, self._onEVT_INTERNAL)
[1bf33c1]80       
[6c0568b]81        self.axes_frozen = False
82        ## panel that contains result from slicer motion (ex: Boxsum info)
[54cc36a]83        self.panel_slicer=None
[1bf33c1]84        ## Graph       
85        self.graph = Graph()
86        self.graph.xaxis("\\rm{Q}", 'A^{-1}')
87        self.graph.yaxis("\\rm{Intensity} ","cm^{-1}")
88        self.graph.render(self)
[0d9dae8]89       
90       
[1bf33c1]91    def _onEVT_1DREPLOT(self, event):
92        """
93            Data is ready to be displayed
[4b91fd1]94           
95            #TODO: this name should be changed to something more appropriate
96            # Don't forget that changing this name will mean changing code
97            # in plotting.py
98             
[1bf33c1]99            @param event: data event
100        """
[6c0568b]101        ## Update self.data2d with the current plot
102        self.data2D = event.plot
[15550f4]103       
[1bf33c1]104        #TODO: Check for existence of plot attribute
[6c0568b]105       
[1bf33c1]106        # Check whether this is a replot. If we ask for a replot
107        # and the plottable no longer exists, ignore the event.
108        if hasattr(event, "update") and event.update==True \
109            and event.plot.name not in self.plots.keys():
110            return
[ab8f936]111       
[1bf33c1]112        if hasattr(event, "reset"):
113            self._reset()
114        is_new = True
115        if event.plot.name in self.plots.keys():
116            # Check whether the class of plottable changed
117            if not event.plot.__class__==self.plots[event.plot.name].__class__:
[ab8f936]118                #overwrite a plottable using the same name
[1bf33c1]119                self.graph.delete(self.plots[event.plot.name])
120            else:
[ab8f936]121                # plottable is already draw on the panel
[1bf33c1]122                is_new = False
[ab8f936]123           
124        if is_new:
125            # a new plottable overwrites a plotted one  using the same id
126            for plottable in self.plots.itervalues():
[e48a62e]127                if hasattr(event.plot,"id"):
128                    if event.plot.id==plottable.id :
129                        self.graph.delete(plottable)
[ab8f936]130           
131            self.plots[event.plot.name] = event.plot
132            self.graph.add(self.plots[event.plot.name])
133        else:
[4b91fd1]134            # Update the plottable with the new data
135           
136            #TODO: we should have a method to do this,
137            #      something along the lines of:
138            #      plottable1.update_data_from_plottable(plottable2)
139           
140            self.plots[event.plot.name].xmin = event.plot.xmin
141            self.plots[event.plot.name].xmax = event.plot.xmax
142            self.plots[event.plot.name].ymin = event.plot.ymin
143            self.plots[event.plot.name].ymax = event.plot.ymax
144            self.plots[event.plot.name].data = event.plot.data
145            self.plots[event.plot.name].err_data = event.plot.err_data
[ac9a5f6]146            # update qmax with the new xmax of data plotted
147            self.qmax= event.plot.xmax
[4b91fd1]148           
[ac9a5f6]149        self.slicer= None
[1bf33c1]150        # Check axis labels
151        #TODO: Should re-factor this
[6c0568b]152        ## render the graph with its new content
[7fff5cd]153               
154        #data2D: put 'Pixel (Number)' for axis title and unit in case of having no detector info and none in _units
155        if len(self.data2D.detector) < 1: 
156            if len(event.plot._xunit)< 1 and len(event.plot._yunit) < 1:
157                event.plot._xaxis = 'Pixel'
158                event.plot._yaxis = 'Pixel'
159                event.plot._xunit = 'Number'
160                event.plot._yunit = 'Number'
161
[1bf33c1]162        self.graph.xaxis(event.plot._xaxis, event.plot._xunit)
163        self.graph.yaxis(event.plot._yaxis, event.plot._yunit)
[0690e1d]164        self.graph.title(self.data2D.name)
[1bf33c1]165        self.graph.render(self)
166        self.subplot.figure.canvas.draw_idle()
167
168
169    def onContextMenu(self, event):
170        """
171            2D plot context menu
172            @param event: wx context event
173        """
[15550f4]174       
[1bf33c1]175        slicerpop = PanelMenu()
176        slicerpop.set_plots(self.plots)
177        slicerpop.set_graph(self.graph)
[9a585d0]178             
179        id = wx.NewId()
180        slicerpop.Append(id, '&Save image')
181        wx.EVT_MENU(self, id, self.onSaveImage)
182       
183        id = wx.NewId()
184        slicerpop.Append(id,'&Print image', 'Print image ')
185        wx.EVT_MENU(self, id, self.onPrint)
186       
[1ce365f8]187        id = wx.NewId()
[18eba35]188        slicerpop.Append(id,'&Print Preview', 'image preview for print')
[1ce365f8]189        wx.EVT_MENU(self, id, self.onPrinterPreview)
190       
[9a585d0]191        slicerpop.AppendSeparator()
[7fff5cd]192        if len(self.data2D.detector) == 1:       
[0e13148]193           
194            item_list = self.parent.get_context_menu(self.graph)
195            if (not item_list==None) and (not len(item_list)==0):
196                   
197                    for item in item_list:
198                        try:
199                            id = wx.NewId()
200                            slicerpop.Append(id, item[0], item[1])
201                            wx.EVT_MENU(self, id, item[2])
202                        except:
203                            wx.PostEvent(self.parent, StatusEvent(status=\
204                            "ModelPanel1D.onContextMenu: bad menu item  %s"%sys.exc_value))
205                            pass
206                    slicerpop.AppendSeparator()
207           
[15550f4]208            id = wx.NewId()
209            slicerpop.Append(id, '&Perform circular average')
210            wx.EVT_MENU(self, id, self.onCircular) 
211           
212            id = wx.NewId()
213            slicerpop.Append(id, '&Sector [Q view]')
214            wx.EVT_MENU(self, id, self.onSectorQ) 
215           
216            id = wx.NewId()
217            slicerpop.Append(id, '&Annulus [Phi view ]')
218            wx.EVT_MENU(self, id, self.onSectorPhi) 
219           
[92c2345]220            id = wx.NewId()
[15550f4]221            slicerpop.Append(id, '&Box Sum')
222            wx.EVT_MENU(self, id, self.onBoxSum) 
[6c0568b]223           
[15550f4]224            id = wx.NewId()
225            slicerpop.Append(id, '&Box averaging in Qx')
226            wx.EVT_MENU(self, id, self.onBoxavgX) 
227           
228            id = wx.NewId()
229            slicerpop.Append(id, '&Box averaging in Qy')
230            wx.EVT_MENU(self, id, self.onBoxavgY) 
231           
232            if self.slicer !=None :
[eba08f1a]233                id = wx.NewId()
[15550f4]234                slicerpop.Append(id, '&Clear slicer')
235                wx.EVT_MENU(self, id,  self.onClearSlicer) 
[6c0568b]236               
[15550f4]237                if self.slicer.__class__.__name__ !="BoxSum":
238                    id = wx.NewId()
239                    slicerpop.Append(id, '&Edit Slicer Parameters')
240                    wx.EVT_MENU(self, id, self._onEditSlicer) 
241                   
242            slicerpop.AppendSeparator() 
[0e13148]243           
244        id = wx.NewId()
245        slicerpop.Append(id, '&Detector Parameters')
246        wx.EVT_MENU(self, id, self._onEditDetector) 
247       
[9a585d0]248       
[1bf33c1]249        id = wx.NewId()
250        slicerpop.Append(id, '&Toggle Linear/Log scale')
251        wx.EVT_MENU(self, id, self._onToggleScale) 
[d468daa]252                 
[1bf33c1]253        pos = event.GetPosition()
254        pos = self.ScreenToClient(pos)
255        self.PopupMenu(slicerpop, pos)
[8bd764d]256   
[6c0568b]257   
[ea290ee]258    def _onEditDetector(self, event):
[6d920cd]259        """
[6c0568b]260            Allow to view and edits  detector parameters
261            @param event: wx.menu event
[6d920cd]262        """
[6c0568b]263       
[ea290ee]264        import detector_dialog
[9a585d0]265        dialog = detector_dialog.DetectorDialog(self, -1,base=self.parent)
[6c0568b]266        ## info of current detector and data2D
[ea290ee]267        xnpts = len(self.data2D.x_bins)
268        ynpts = len(self.data2D.y_bins)
269        xmax = max(self.data2D.xmin, self.data2D.xmax)
270        ymax = max(self.data2D.ymin, self.data2D.ymax)
271        qmax = math.sqrt(math.pow(xmax,2)+math.pow(ymax,2))
272        beam = self.data2D.xmin
273        zmin = self.zmin_2D
274        zmax = self.zmax_2D
[6c0568b]275        ## set dialog window content
[ea290ee]276        dialog.setContent(xnpts=xnpts,ynpts=ynpts,qmax=qmax,
277                           beam=self.data2D.xmin,
278                           zmin = self.zmin_2D,
279                          zmax = self.zmax_2D)
280        if dialog.ShowModal() == wx.ID_OK:
281            evt = dialog.getContent()
282            self.zmin_2D = evt.zmin
283            self.zmax_2D = evt.zmax
284       
285        dialog.Destroy()
[6c0568b]286        ## Redraw the current image
[ea290ee]287        self.image(data= self.data2D.data,
288                   xmin= self.data2D.xmin,
289                   xmax= self.data2D.xmax,
290                   ymin= self.data2D.ymin,
291                   ymax= self.data2D.ymax,
292                   zmin= self.zmin_2D,
293                   zmax= self.zmax_2D,
294                   color=0,symbol=0,label='data2D')
295        self.subplot.figure.canvas.draw_idle()
[1bf33c1]296       
[6c0568b]297       
298 
[1bf33c1]299    def freeze_axes(self):
300        self.axes_frozen = True
301       
302    def thaw_axes(self):
303        self.axes_frozen = False
304       
305    def onMouseMotion(self,event):
306        pass
307    def onWheel(self, event):
[6c0568b]308        pass 
309     
[1bf33c1]310    def update(self, draw=True):
311        """
312            Respond to changes in the model by recalculating the
313            profiles and resetting the widgets.
314        """
315        self.draw()
316       
317       
318    def _getEmptySlicerEvent(self):
[6c0568b]319        """
320            create an empty slicervent
321        """
[1bf33c1]322        return SlicerEvent(type=None,
323                           params=None,
324                           obj_class=None)
325    def _onEVT_INTERNAL(self, event):
326        """
[6c0568b]327            Draw the slicer
328            @param event: wx.lib.newevent (SlicerEvent) containing slicer
329            parameter
[1bf33c1]330        """
331        self._setSlicer(event.slicer)
332           
333    def _setSlicer(self, slicer):
[6c0568b]334        """
335            Clear the previous slicer and create a new one.Post an internal
336            event.
337            @param slicer: slicer class to create
338        """
[1bf33c1]339       
[6c0568b]340        ## Clear current slicer
[1bf33c1]341        if not self.slicer == None: 
342            self.slicer.clear()           
[6c0568b]343        ## Create a new slicer   
[1bf33c1]344        self.slicer_z += 1
345        self.slicer = slicer(self, self.subplot, zorder=self.slicer_z)
[240c805]346        self.subplot.set_ylim(self.data2D.ymin, self.data2D.ymax)
347        self.subplot.set_xlim(self.data2D.xmin, self.data2D.xmax)
[6c0568b]348        ## Draw slicer
[1bf33c1]349        self.update()
350        self.slicer.update()
[6c0568b]351        wx.PostEvent(self.parent, StatusEvent(status=\
352                        "Plotter2D._setSlicer  %s"%self.slicer.__class__.__name__))
[1bf33c1]353        # Post slicer event
354        event = self._getEmptySlicerEvent()
355        event.type = self.slicer.__class__.__name__
[54cc36a]356       
[1bf33c1]357        event.obj_class = self.slicer.__class__
358        event.params = self.slicer.get_params()
[d468daa]359        wx.PostEvent(self, event)
[1bf33c1]360
[6c0568b]361
[1bf33c1]362    def onCircular(self, event):
363        """
364            perform circular averaging on Data2D
[6c0568b]365            @param event: wx.menu event
[1bf33c1]366        """
367       
368        from DataLoader.manipulations import CircularAverage
[6c0568b]369        ## compute the maximum radius of data2D
[216efab]370        self.qmax= max(math.fabs(self.data2D.xmax),math.fabs(self.data2D.xmin))
371        self.ymax=max(math.fabs(self.data2D.ymax),math.fabs(self.data2D.ymin))
372        self.radius= math.sqrt( math.pow(self.qmax,2)+math.pow(self.ymax,2)) 
[6c0568b]373        ##Compute beam width
[8f584c9]374        bin_width = (self.qmax +self.qmax)/100
[6c0568b]375        ## Create data1D circular average of data2D
[c73d871]376        Circle = CircularAverage( r_min=0, r_max=self.radius, bin_width=bin_width)
[1bf33c1]377        circ = Circle(self.data2D)
[6c0568b]378       
[1bf33c1]379        from sans.guiframe.dataFitting import Data1D
380        if hasattr(circ,"dxl"):
381            dxl= circ.dxl
382        else:
383            dxl= None
384        if hasattr(circ,"dxw"):
385            dxw= circ.dxw
386        else:
387            dxw= None
[ef0c170]388       
[1bf33c1]389        new_plot = Data1D(x=circ.x,y=circ.y,dy=circ.dy,dxl=dxl,dxw=dxw)
390        new_plot.name = "Circ avg "+ self.data2D.name
391        new_plot.source=self.data2D.source
[50cbace]392        #new_plot.info=self.data2D.info
[1bf33c1]393        new_plot.interactive = True
394        new_plot.detector =self.data2D.detector
[6c0568b]395        ## If the data file does not tell us what the axes are, just assume...
[8f584c9]396        new_plot.xaxis("\\rm{Q}","A^{-1}")
[1bf33c1]397        new_plot.yaxis("\\rm{Intensity} ","cm^{-1}")
398        new_plot.group_id = "Circ avg "+ self.data2D.name
[8b30e62]399        new_plot.id = "Circ avg "+ self.data2D.name
[6c0568b]400       
[1bf33c1]401        wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=new_plot.name))
[ef0c170]402       
[6c0568b]403       
[1bf33c1]404    def _onEditSlicer(self, event):
[6c0568b]405        """
406            Is available only when a slicer is drawn.Create a dialog
407            window where the user can enter value to reset slicer
408            parameters.
409            @param event: wx.menu event
410        """
[1bf33c1]411        if self.slicer !=None:
412            from SlicerParameters import SlicerParameterPanel
[4f8a00c]413            dialog = SlicerParameterPanel(self, -1, "Slicer Parameters")
[1bf33c1]414            dialog.set_slicer(self.slicer.__class__.__name__,
415                            self.slicer.get_params())
416            if dialog.ShowModal() == wx.ID_OK:
417                dialog.Destroy() 
418       
[6c0568b]419       
[1bf33c1]420    def onSectorQ(self, event):
421        """
[6c0568b]422            Perform sector averaging on Q and draw sector slicer
[1bf33c1]423        """
[ef0c170]424        from SectorSlicer import SectorInteractor
[1bf33c1]425        self.onClearSlicer(event)
[d468daa]426        wx.PostEvent(self, InternalEvent(slicer= SectorInteractor))
[1bf33c1]427       
428    def onSectorPhi(self, event):
429        """
[6c0568b]430            Perform sector averaging on Phi and draw annulus slicer
[1bf33c1]431        """
[ef0c170]432        from AnnulusSlicer import AnnulusInteractor
[1bf33c1]433        self.onClearSlicer(event)
[d468daa]434        wx.PostEvent(self, InternalEvent(slicer= AnnulusInteractor))
[1bf33c1]435       
[7ab9241]436    def onBoxSum(self,event):
437        from boxSum import BoxSum
438        self.onClearSlicer(event)
[6c0568b]439                   
[54cc36a]440        self.slicer_z += 1
441        self.slicer =  BoxSum(self, self.subplot, zorder=self.slicer_z)
[6c0568b]442       
[54cc36a]443        self.subplot.set_ylim(self.data2D.ymin, self.data2D.ymax)
444        self.subplot.set_xlim(self.data2D.xmin, self.data2D.xmax)
445       
446        self.update()
447        self.slicer.update()
[6c0568b]448        ## Value used to initially set the slicer panel
449        type = self.slicer.__class__.__name__
450        params = self.slicer.get_params()
451        ## Create a new panel to display results of summation of Data2D
[8a7a21b]452        from slicerpanel import SlicerPanel
[6c0568b]453        new_panel = SlicerPanel(parent= self.parent, id= -1,
454                                    base= self, type= type,
455                                    params= params, style= wx.RAISED_BORDER)
456       
457        new_panel.window_caption=self.slicer.__class__.__name__+" "+ str(self.data2D.name)
458        new_panel.window_name = self.slicer.__class__.__name__+" "+ str(self.data2D.name)
459        ## Store a reference of the new created panel
[54cc36a]460        self.panel_slicer= new_panel
[6c0568b]461        ## save the window_caption of the new panel in the current slicer
[0bd2cd8]462        self.slicer.set_panel_name( name= new_panel.window_caption)
[6c0568b]463        ## post slicer panel to guiframe to display it
[54cc36a]464        from sans.guicomm.events import SlicerPanelEvent
465        wx.PostEvent(self.parent, SlicerPanelEvent (panel= self.panel_slicer))
[6c0568b]466
[8a7a21b]467       
468    def onBoxavgX(self,event):
[6c0568b]469        """
470            Perform 2D data averaging on Qx
471            Create a new slicer .
472            @param event: wx.menu event
473        """
[8a7a21b]474        from boxSlicer import BoxInteractorX
[38224f10]475        self.onClearSlicer(event)
[d468daa]476        wx.PostEvent(self, InternalEvent(slicer= BoxInteractorX))
477       
478       
[8a7a21b]479    def onBoxavgY(self,event):
[6c0568b]480        """
481            Perform 2D data averaging on Qy
482            Create a new slicer .
483            @param event: wx.menu event
484        """
[8a7a21b]485        from boxSlicer import BoxInteractorY
486        self.onClearSlicer(event)
[d468daa]487        wx.PostEvent(self, InternalEvent(slicer= BoxInteractorY))
[6c0568b]488       
[b319def8]489       
[1bf33c1]490    def onClearSlicer(self, event):
491        """
492            Clear the slicer on the plot
493        """
494        if not self.slicer==None:
495            self.slicer.clear()
496            self.subplot.figure.canvas.draw()
497            self.slicer = None
498       
499            # Post slicer None event
500            event = self._getEmptySlicerEvent()
[d468daa]501            wx.PostEvent(self, event)
[1bf33c1]502         
[6c0568b]503   
[1bf33c1]504    def _onToggleScale(self, event):
505        """
506            toggle pixel scale and replot image
507        """
508        if self.scale == 'log':
509            self.scale = 'linear'
510        else:
511            self.scale = 'log'
[6c0568b]512        self.image(self.data2D.data,self.xmin_2D,self.xmax_2D,self.ymin_2D,
[1bf33c1]513                   self.ymax_2D,self.zmin_2D ,self.zmax_2D )
514        wx.PostEvent(self.parent, StatusEvent(status="Image is in %s scale"%self.scale))
515       
[0f6d05f8]516        """     
517            #TODO: this name should be changed to something more appropriate
518            # Don't forget that changing this name will mean changing code
519            # in plotting.py
520             
521            # Update the plottable with the new data
522           
523            #TODO: we should have a method to do this,
524            #      something along the lines of:
525            #      plottable1.update_data_from_plottable(plottable2)
526           
527            self.plots[event.plot.name].xmin = event.plot.xmin
528            self.plots[event.plot.name].xmax = event.plot.xmax
529            self.plots[event.plot.name].ymin = event.plot.ymin
530            self.plots[event.plot.name].ymax = event.plot.ymax
531            self.plots[event.plot.name].data = event.plot.data
532            self.plots[event.plot.name].err_data = event.plot.err_data
[15550f4]533        """
Note: See TracBrowser for help on using the repository browser.