source: sasview/guiframe/local_perspectives/plotting/Plotter2D.py @ 6eea960

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 6eea960 was c5a769e, checked in by Jae Cho <jhjcho@…>, 14 years ago

more touch: move delete plot to p_panel from data_panel…

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