source: sasview/sansguiframe/src/sans/guiframe/local_perspectives/plotting/Plotter2D.py @ e4a703a

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 e4a703a was e4a703a, checked in by Jae Cho <jhjcho@…>, 13 years ago

keep the xylimit on update plot

  • Property mode set to 100644
File size: 23.4 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        xlo = None
169        xhi = None
170        ylo = None 
171        yhi = None
172        ## Update self.data2d with the current plot
173        self.data2D = data
174        if data.id in self.plots.keys():
175            #replace
176            xlo, xhi = self.subplot.get_xlim()
177            ylo, yhi = self.subplot.get_ylim()
178            self.graph.replace(data)
179            self.plots[data.id] = data
180        else:
181            self.plots[data.id] = data
182            self.graph.add(self.plots[data.id]) 
183            # update qmax with the new xmax of data plotted
184            self.qmax = data.xmax
185           
186        self.slicer = None
187        # Check axis labels
188        #TODO: Should re-factor this
189        ## render the graph with its new content
190               
191        #data2D: put 'Pixel (Number)' for axis title and unit in case of having no detector info and none in _units
192        if len(self.data2D.detector) < 1: 
193            if len(data._xunit) < 1 and len(data._yunit) < 1:
194                data._xaxis = '\\rm{x}'
195                data._yaxis = '\\rm{y}'
196                data._xunit = 'pixel'
197                data._yunit = 'pixel'
198        self.graph.xaxis(data._xaxis, data._xunit)
199        self.graph.yaxis(data._yaxis, data._yunit)
200        self.graph.title(self.data2D.name)
201        self.graph.render(self)
202        self.draw_plot()
203        #self.subplot.figure.canvas.draw_idle()
204        ## store default value of zmin and zmax
205        self.default_zmin_ctl = self.zmin_2D
206        self.default_zmax_ctl = self.zmax_2D
207        # Recover the x,y limits
208        if (xlo and xhi and ylo and yhi) != None:
209            if (xlo > data.xmin and xhi < data.xmax and\
210                        ylo > data.ymin and yhi < data.ymax):
211                self.subplot.set_xlim((xlo, xhi))     
212                self.subplot.set_ylim((ylo, yhi)) 
213
214    def onContextMenu(self, event):
215        """
216        2D plot context menu
217       
218        :param event: wx context event
219       
220        """
221        slicerpop = PanelMenu()
222        slicerpop.set_plots(self.plots)
223        slicerpop.set_graph(self.graph)
224             
225        id = wx.NewId()
226        slicerpop.Append(id, '&Save Image')
227        wx.EVT_MENU(self, id, self.onSaveImage)
228       
229        id = wx.NewId()
230        slicerpop.Append(id,'&Print Image', 'Print image')
231        wx.EVT_MENU(self, id, self.onPrint)
232       
233        id = wx.NewId()
234        slicerpop.Append(id,'&Print Preview', 'Print preview')
235        wx.EVT_MENU(self, id, self.onPrinterPreview)
236
237        id = wx.NewId()
238        slicerpop.Append(id, '&Copy to Clipboard', 'Copy to the clipboard')
239        wx.EVT_MENU(self, id, self.OnCopyFigureMenu)
240        slicerpop.AppendSeparator()
241        # saving data
242        plot = self.data2D
243        id = wx.NewId()
244        name = plot.name
245        slicerpop.Append(id, "&Save as a file (DAT)" )
246        self.action_ids[str(id)] = plot
247        wx.EVT_MENU(self, id, self._onSave)
248
249        slicerpop.AppendSeparator()
250        if len(self.data2D.detector) == 1:       
251           
252            item_list = self.parent.get_context_menu(self)
253            if (not item_list == None) and (not len(item_list) == 0) and\
254                self.data2D.name.split(" ")[0] != 'Residuals': 
255                # The line above; Not for trunk
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            # For Masked Data
272            if not plot.mask.all():
273                id = wx.NewId()
274                slicerpop.Append(id, '&Masked circular average')
275                wx.EVT_MENU(self, id, self.onMaskedCircular) 
276            id = wx.NewId()
277            slicerpop.Append(id, '&Sector [Q view]')
278            wx.EVT_MENU(self, id, self.onSectorQ) 
279            id = wx.NewId()
280            slicerpop.Append(id, '&Annulus [Phi view ]')
281            wx.EVT_MENU(self, id, self.onSectorPhi) 
282            id = wx.NewId()
283            slicerpop.Append(id, '&Box Sum')
284            wx.EVT_MENU(self, id, self.onBoxSum) 
285            id = wx.NewId()
286            slicerpop.Append(id, '&Box averaging in Qx')
287            wx.EVT_MENU(self, id, self.onBoxavgX) 
288            id = wx.NewId()
289            slicerpop.Append(id, '&Box averaging in Qy')
290            wx.EVT_MENU(self, id, self.onBoxavgY) 
291            if self.slicer != None:
292                id = wx.NewId()
293                slicerpop.Append(id, '&Clear slicer')
294                wx.EVT_MENU(self, id,  self.onClearSlicer) 
295                if self.slicer.__class__.__name__  != "BoxSum":
296                    id = wx.NewId()
297                    slicerpop.Append(id, '&Edit Slicer Parameters')
298                    wx.EVT_MENU(self, id, self._onEditSlicer) 
299            slicerpop.AppendSeparator() 
300        id = wx.NewId()
301        slicerpop.Append(id, '&2D Color Map')
302        wx.EVT_MENU(self, id, self._onEditDetector)
303        id = wx.NewId()
304        slicerpop.Append(id, '&Toggle Linear/Log scale')
305        wx.EVT_MENU(self, id, self._onToggleScale) 
306        pos = event.GetPosition()
307        pos = self.ScreenToClient(pos)
308        self.PopupMenu(slicerpop, pos)
309   
310    def _onEditDetector(self, event):
311        """
312        Allow to view and edits  detector parameters
313       
314        :param event: wx.menu event
315       
316        """
317        import detector_dialog
318        dialog = detector_dialog.DetectorDialog(self, -1,base=self.parent,
319                       reset_zmin_ctl =self.default_zmin_ctl,
320                       reset_zmax_ctl = self.default_zmax_ctl,cmap=self.cmap)
321        ## info of current detector and data2D
322        xnpts = len(self.data2D.x_bins)
323        ynpts = len(self.data2D.y_bins)
324        xmax = max(self.data2D.xmin, self.data2D.xmax)
325        ymax = max(self.data2D.ymin, self.data2D.ymax)
326        qmax = math.sqrt(math.pow(xmax, 2) + math.pow(ymax, 2))
327        beam = self.data2D.xmin
328        ## set dialog window content
329        dialog.setContent(xnpts=xnpts,ynpts=ynpts,qmax=qmax,
330                           beam=self.data2D.xmin,
331                           zmin = self.zmin_2D,
332                          zmax = self.zmax_2D)
333        if dialog.ShowModal() == wx.ID_OK:
334            evt = dialog.getContent()
335            self.zmin_2D = evt.zmin
336            self.zmax_2D = evt.zmax
337            self.cmap = evt.cmap
338        dialog.Destroy()
339        ## Redraw the current image
340        self.image(data=self.data2D.data,
341                   qx_data=self.data2D.qx_data,
342                   qy_data=self.data2D.qy_data,
343                   xmin= self.data2D.xmin,
344                   xmax= self.data2D.xmax,
345                   ymin= self.data2D.ymin,
346                   ymax= self.data2D.ymax,
347                   zmin= self.zmin_2D,
348                   zmax= self.zmax_2D,
349                   cmap= self.cmap,
350                   color=0, symbol=0, label=self.data2D.name)
351        self.subplot.figure.canvas.draw_idle()
352       
353    def freeze_axes(self):
354        """
355        """
356        self.axes_frozen = True
357       
358    def thaw_axes(self):
359        """
360        """
361        self.axes_frozen = False
362       
363    def onMouseMotion(self,event):
364        """
365        """
366        pass
367   
368    def onWheel(self, event):
369        """
370        """
371        pass 
372     
373    def update(self, draw=True):
374        """
375        Respond to changes in the model by recalculating the
376        profiles and resetting the widgets.
377        """
378        self.draw_plot()
379       
380    def _getEmptySlicerEvent(self):
381        """
382        create an empty slicervent
383        """
384        return SlicerEvent(type=None, params=None, obj_class=None)
385       
386    def _onEVT_INTERNAL(self, event):
387        """
388        Draw the slicer
389       
390        :param event: wx.lib.newevent (SlicerEvent) containing slicer
391            parameter
392           
393        """
394        self._setSlicer(event.slicer)
395           
396    def _setSlicer(self, slicer):
397        """
398        Clear the previous slicer and create a new one.Post an internal
399        event.
400       
401        :param slicer: slicer class to create
402       
403        """
404        ## Clear current slicer
405        if not self.slicer == None: 
406            self.slicer.clear()           
407        ## Create a new slicer   
408        self.slicer_z += 1
409        self.slicer = slicer(self, self.subplot, zorder=self.slicer_z)
410        self.subplot.set_ylim(self.data2D.ymin, self.data2D.ymax)
411        self.subplot.set_xlim(self.data2D.xmin, self.data2D.xmax)
412        ## Draw slicer
413        self.update()
414        self.slicer.update()
415        msg = "Plotter2D._setSlicer  %s"%self.slicer.__class__.__name__
416        wx.PostEvent(self.parent, StatusEvent(status=msg))
417        # Post slicer event
418        event = self._getEmptySlicerEvent()
419        event.type = self.slicer.__class__.__name__
420        event.obj_class = self.slicer.__class__
421        event.params = self.slicer.get_params()
422        wx.PostEvent(self, event)
423       
424    def onMaskedCircular(self, event):
425        """
426        perform circular averaging on Data2D with mask if it exists
427       
428        :param event: wx.menu event
429       
430        """
431        self.onCircular(event, True)
432       
433    def onCircular(self, event, ismask=False):
434        """
435        perform circular averaging on Data2D
436       
437        :param event: wx.menu event
438       
439        """
440        # Find the best number of bins
441        npt = math.sqrt(len(self.data2D.data[numpy.isfinite(self.data2D.data)]))
442        npt = math.floor(npt)
443        from sans.dataloader.manipulations import CircularAverage
444        ## compute the maximum radius of data2D
445        self.qmax = max(math.fabs(self.data2D.xmax), 
446                        math.fabs(self.data2D.xmin))
447        self.ymax = max(math.fabs(self.data2D.ymax),
448                        math.fabs(self.data2D.ymin))
449        self.radius = math.sqrt(math.pow(self.qmax, 2)+ math.pow(self.ymax, 2)) 
450        ##Compute beam width
451        bin_width = (self.qmax + self.qmax)/npt
452        ## Create data1D circular average of data2D
453        Circle = CircularAverage(r_min=0, r_max=self.radius, 
454                                 bin_width=bin_width)
455        circ = Circle(self.data2D, ismask=ismask)
456        from sans.guiframe.dataFitting import Data1D
457        if hasattr(circ, "dxl"):
458            dxl = circ.dxl
459        else:
460            dxl = None
461        if hasattr(circ, "dxw"):
462            dxw = circ.dxw
463        else:
464            dxw = None
465
466        new_plot = Data1D(x=circ.x, y=circ.y, dy=circ.dy, dx=circ.dx)
467        new_plot.dxl = dxl
468        new_plot.dxw = dxw
469        new_plot.name = "Circ avg " + self.data2D.name
470        new_plot.source = self.data2D.source
471        #new_plot.info = self.data2D.info
472        new_plot.interactive = True
473        new_plot.detector = self.data2D.detector
474       
475        ## If the data file does not tell us what the axes are, just assume...
476        new_plot.xaxis("\\rm{Q}", "A^{-1}")
477        if hasattr(self.data2D, "scale") and \
478                    self.data2D.scale == 'linear':
479            new_plot.ytransform = 'y'
480            new_plot.yaxis("\\rm{Residuals} ", "normalized")
481        else:
482            new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}")
483
484        new_plot.group_id = "Circ avg " + self.data2D.name
485        new_plot.id = "Circ avg " + self.data2D.name
486        new_plot.is_data = True
487        wx.PostEvent(self.parent, 
488                     NewPlotEvent(plot=new_plot, title=new_plot.name))
489       
490    def _onEditSlicer(self, event):
491        """
492        Is available only when a slicer is drawn.Create a dialog
493        window where the user can enter value to reset slicer
494        parameters.
495       
496        :param event: wx.menu event
497       
498        """
499        if self.slicer != None:
500            from SlicerParameters import SlicerParameterPanel
501            dialog = SlicerParameterPanel(self, -1, "Slicer Parameters")
502            dialog.set_slicer(self.slicer.__class__.__name__,
503                            self.slicer.get_params())
504            if dialog.ShowModal() == wx.ID_OK:
505                dialog.Destroy() 
506       
507    def onSectorQ(self, event):
508        """
509        Perform sector averaging on Q and draw sector slicer
510        """
511        from SectorSlicer import SectorInteractor
512        self.onClearSlicer(event)
513        wx.PostEvent(self, InternalEvent(slicer=SectorInteractor))
514       
515    def onSectorPhi(self, event):
516        """
517        Perform sector averaging on Phi and draw annulus slicer
518        """
519        from AnnulusSlicer import AnnulusInteractor
520        self.onClearSlicer(event)
521        wx.PostEvent(self, InternalEvent(slicer=AnnulusInteractor))
522       
523    def onBoxSum(self, event):
524        """
525        """
526        from boxSum import BoxSum
527        self.onClearSlicer(event)
528        self.slicer_z += 1
529        self.slicer =  BoxSum(self, self.subplot, zorder=self.slicer_z)
530        self.subplot.set_ylim(self.data2D.ymin, self.data2D.ymax)
531        self.subplot.set_xlim(self.data2D.xmin, self.data2D.xmax)
532        self.update()
533        self.slicer.update()
534        ## Value used to initially set the slicer panel
535        type = self.slicer.__class__.__name__
536        params = self.slicer.get_params()
537        ## Create a new panel to display results of summation of Data2D
538        from slicerpanel import SlicerPanel
539        new_panel = SlicerPanel(parent=self.parent, id=-1,
540                                    base=self, type=type,
541                                    params=params, style=wx.RAISED_BORDER)
542       
543        new_panel.window_caption = self.slicer.__class__.__name__ + " " + \
544                                    str(self.data2D.name)
545        new_panel.window_name = self.slicer.__class__.__name__+ " " + \
546                                    str(self.data2D.name)
547        ## Store a reference of the new created panel
548        self.panel_slicer = new_panel
549        ## save the window_caption of the new panel in the current slicer
550        self.slicer.set_panel_name(name=new_panel.window_caption)
551        ## post slicer panel to guiframe to display it
552        from sans.guiframe.events import SlicerPanelEvent
553        wx.PostEvent(self.parent, SlicerPanelEvent(panel=self.panel_slicer,
554                                                    main_panel=self))
555
556    def onBoxavgX(self,event):
557        """
558        Perform 2D data averaging on Qx
559        Create a new slicer .
560       
561        :param event: wx.menu event
562        """
563        from boxSlicer import BoxInteractorX
564        self.onClearSlicer(event)
565        wx.PostEvent(self, InternalEvent(slicer=BoxInteractorX))
566       
567    def onBoxavgY(self,event):
568        """
569        Perform 2D data averaging on Qy
570        Create a new slicer .
571       
572        :param event: wx.menu event
573       
574        """
575        from boxSlicer import BoxInteractorY
576        self.onClearSlicer(event)
577        wx.PostEvent(self, InternalEvent(slicer=BoxInteractorY))
578       
579    def onClearSlicer(self, event):
580        """
581        Clear the slicer on the plot
582        """
583        if not self.slicer == None:
584            self.slicer.clear()
585            self.subplot.figure.canvas.draw()
586            self.slicer = None
587            # Post slicer None event
588            event = self._getEmptySlicerEvent()
589            wx.PostEvent(self, event)
590           
591    def _onSave(self, evt):
592        """
593        Save a data set to a dat(text) file
594       
595        :param evt: Menu event
596       
597        """
598        id = str(evt.GetId())
599        if id in self.action_ids:         
600           
601            path = None
602            wildcard = "IGOR/DAT 2D file in Q_map (*.dat)|*.DAT"
603            dlg = wx.FileDialog(self, "Choose a file",
604                                self._default_save_location,
605                                 "", wildcard , wx.SAVE)
606           
607            if dlg.ShowModal() == wx.ID_OK:
608                path = dlg.GetPath()
609                # ext_num = 0 for .txt, ext_num = 1 for .xml
610                # This is MAC Fix
611                ext_num = dlg.GetFilterIndex()
612                if ext_num == 0:
613                    format = '.dat'
614                else:
615                    format = ''
616                path = os.path.splitext(path)[0] + format
617                mypath = os.path.basename(path)
618               
619                #TODO: This is bad design. The DataLoader is designed
620                #to recognize extensions.
621                # It should be a simple matter of calling the .
622                #save(file, data, '.xml') method
623                # of the DataLoader.loader.Loader class.
624                from sans.dataloader.loader import  Loader
625                #Instantiate a loader
626                loader = Loader() 
627                data = self.data2D
628
629                format = ".dat"
630                if os.path.splitext(mypath)[1].lower() == format:
631                    # Make sure the ext included in the file name
632                    # especially on MAC
633                    fName = os.path.splitext(path)[0] + format
634                    loader.save(fName, data, format)
635                try:
636                    self._default_save_location = os.path.dirname(path)
637                except:
638                    pass   
639            dlg.Destroy()
Note: See TracBrowser for help on using the repository browser.