source: sasview/guiframe/local_perspectives/plotting/Plotter2D.py @ 8cd029b

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 8cd029b was ed8ad21, checked in by Jae Cho <jhjcho@…>, 14 years ago

added 2D save as a file in context menu

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