source: sasview/guitools/plottables.py @ 6ed101a

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 6ed101a was 6ed101a, checked in by Gervaise Alina <gervyh@…>, 16 years ago

more modification ..zoom is still bugging for logx logy

  • Property mode set to 100644
File size: 25.3 KB
Line 
1"""Prototype plottable object support.
2
3The main point of this prototype is to provide a clean separation between
4the style (plotter details: color, grids, widgets, etc.) and substance
5(application details: which information to plot).  Programmers should not be
6dictating line colours and plotting symbols.
7
8Unlike the problem of style in CSS or Word, where most paragraphs look
9the same, each line on a graph has to be distinguishable from its neighbours.
10Our solution is to provide parametric styles, in which a number of
11different classes of object (e.g., reflectometry data, reflectometry
12theory) representing multiple graph primitives cycle through a colour
13palette provided by the underlying plotter.
14
15A full treatment would provide perceptual dimensions of prominence and
16distinctiveness rather than a simple colour number.
17"""
18
19# Design question: who owns the color?
20# Is it a property of the plottable?
21# Or of the plottable as it exists on the graph?
22# Or if the graph?
23# If a plottable can appear on multiple graphs, in some case the
24# color should be the same on each graph in which it appears, and
25# in other cases (where multiple plottables from different graphs
26# coexist), the color should be assigned by the graph.  In any case
27# once a plottable is placed on the graph its color should not
28# depend on the other plottables on the graph.  Furthermore, if
29# a plottable is added and removed from a graph and added again,
30# it may be nice, but not necessary, to have the color persist.
31#
32# The safest approach seems to be to give ownership of color
33# to the graph, which will allocate the colors along with the
34# plottable.  The plottable will need to return the number of
35# colors that are needed.
36#
37# The situation is less clear for symbols.  It is less clear
38# how much the application requires that symbols be unique across
39# all plots on the graph.
40
41# Support for ancient python versions
42import copy
43import numpy
44import math
45
46if 'any' not in dir(__builtins__):
47    def any(L):
48        for cond in L:
49            if cond: return True
50        return False
51    def all(L):
52        for cond in L:
53            if not cond: return False
54        return True
55
56# Graph structure for holding multiple plottables
57class Graph:
58    """
59    Generic plottables graph structure.
60   
61    Plot styles are based on color/symbol lists.  The user gets to select
62    the list of colors/symbols/sizes to choose from, not the application
63    developer.  The programmer only gets to add/remove lines from the
64    plot and move to the next symbol/color.
65
66    Another dimension is prominence, which refers to line sizes/point sizes.
67
68    Axis transformations allow the user to select the coordinate view
69    which provides clarity to the data.  There is no way we can provide
70    every possible transformation for every application generically, so
71    the plottable objects themselves will need to provide the transformations.
72    Here are some examples from reflectometry:
73       independent: x -> f(x)
74          monitor scaling: y -> M*y
75          log:  y -> log(y if y > min else min)
76          cos:  y -> cos(y*pi/180)
77       dependent:   x -> f(x,y)
78          Q4:      y -> y*x^4
79          fresnel: y -> y*fresnel(x)
80       coordinated: x,y = f(x,y)
81          Q:    x -> 2*pi/L (cos(x*pi/180) - cos(y*pi/180))
82                y -> 2*pi/L (sin(x*pi/180) + sin(y*pi/180))
83       reducing: x,y = f(x1,x2,y1,y2)
84          spin asymmetry: x -> x1, y -> (y1 - y2)/(y1 + y2)
85          vector net: x -> x1, y -> y1*cos(y2*pi/180)
86    Multiple transformations are possible, such as Q4 spin asymmetry
87
88    Axes have further complications in that the units of what are being
89    plotted should correspond to the units on the axes.  Plotting multiple
90    types on the same graph should be handled gracefully, e.g., by creating
91    a separate tab for each available axis type, breaking into subplots,
92    showing multiple axes on the same plot, or generating inset plots.
93    Ultimately the decision should be left to the user.
94
95    Graph properties such as grids/crosshairs should be under user control,
96    as should the sizes of items such as axis fonts, etc.  No direct
97    access will be provided to the application.
98
99    Axis limits are mostly under user control.  If the user has zoomed or
100    panned then those limits are preserved even if new data is plotted.
101    The exception is when, e.g., scanning through a set of related lines
102    in which the user may want to fix the limits so that user can compare
103    the values directly.  Another exception is when creating multiple
104    graphs sharing the same limits, though this case may be important
105    enough that it is handled by the graph widget itself.  Axis limits
106    will of course have to understand the effects of axis transformations.
107
108    High level plottable objects may be composed of low level primitives.
109    Operations such as legend/hide/show copy/paste, etc. need to operate
110    on these primitives as a group.  E.g., allowing the user to have a
111    working canvas where they can drag lines they want to save and annotate
112    them.
113
114    Graphs need to be printable.  A page layout program for entire plots
115    would be nice.
116    """
117    def xaxis(self,name,units):
118        """Properties of the x axis.
119        """
120        if self.prop["xunit"] and units != self.prop["xunit"]:
121            pass
122            #print "Plottable: how do we handle non-commensurate units"
123        self.prop["xlabel"] = "%s (%s)"%(name,units)
124        self.prop["xunit"] = units
125
126    def yaxis(self,name,units):
127        """Properties of the y axis.
128        """
129        if self.prop["yunit"] and units != self.prop["yunit"]:
130            pass
131            #print "Plottable: how do we handle non-commensurate units"
132        self.prop["ylabel"] = "%s (%s)"%(name,units)
133        self.prop["yunit"] = units
134       
135    def title(self,name):
136        """Graph title
137        """
138        self.prop["title"] = name
139       
140    def get(self,key):
141        """Get the graph properties"""
142        if key=="color":
143            return self.color
144        elif key == "symbol":
145            return self.symbol
146        else:
147            return self.prop[key]
148
149    def set(self,**kw):
150        """Set the graph properties"""
151        for key in kw:
152            if key == "color":
153                self.color = kw[key]%len(self.colorlist)
154            elif key == "symbol":
155                self.symbol = kw[key]%len(self.symbollist)
156            else:
157                self.prop[key] = kw[key]
158
159    def isPlotted(self, plottable):
160        """Return True is the plottable is already on the graph"""
161        if plottable in self.plottables:
162            return True
163        return False 
164       
165    def add(self,plottable):
166        """Add a new plottable to the graph"""
167        # record the colour associated with the plottable
168        if not plottable in self.plottables:         
169            self.plottables[plottable]=self.color
170            self.color += plottable.colors()
171       
172    def changed(self):
173        """Detect if any graphed plottables have changed"""
174        return any([p.changed() for p in self.plottables])
175
176    def delete(self,plottable):
177        """Remove an existing plottable from the graph"""
178        if plottable in self.plottables:
179            del self.plottables[plottable]
180        if self.color > 0:
181            self.color =  self.color -1
182        else:
183            self.color =0 
184           
185
186    def reset(self):
187        """Reset the graph."""
188        self.color = 0
189        self.symbol = 0
190        self.prop = {"xlabel":"", "xunit":None,
191                     "ylabel":"","yunit":None,
192                     "title":""}
193        self.plottables = {}
194
195    def _make_labels(self):
196        # Find groups of related plottables
197        sets = {}
198        for p in self.plottables:
199            if p.__class__ in sets:
200                sets[p.__class__].append(p)
201            else:
202                sets[p.__class__] = [p]
203               
204        # Ask each plottable class for a set of unique labels
205        labels = {}
206        for c in sets:
207            labels.update(c.labels(sets[c]))
208       
209        return labels
210   
211    def returnPlottable(self):
212        return self.plottables
213
214    def render(self,plot):
215        """Redraw the graph"""
216        plot.clear()
217        plot.properties(self.prop)
218        labels = self._make_labels()
219        for p in self.plottables:
220            p.render(plot,color=self.plottables[p],symbol=0,label=labels[p])
221        plot.render()
222   
223    def clear(self,plot): 
224        plot.clear()
225
226    def __init__(self,**kw):
227        self.reset()
228        self.set(**kw)
229
230
231# Transform interface definition
232# No need to inherit from this class, just need to provide
233# the same methods.
234class Transform:
235    """Define a transform plugin to the plottable architecture.
236   
237    Transforms operate on axes.  The plottable defines the
238    set of transforms available for it, and the axes on which
239    they operate.  These transforms can operate on the x axis
240    only, the y axis only or on the x and y axes together.
241   
242    This infrastructure is not able to support transformations
243    such as log and polar plots as these require full control
244    over the drawing of axes and grids.
245   
246    A transform has a number of attributes.
247   
248    name: user visible name for the transform.  This will
249        appear in the context menu for the axis and the transform
250        menu for the graph.
251    type: operational axis.  This determines whether the
252        transform should appear on x,y or z axis context
253        menus, or if it should appear in the context menu for
254        the graph.
255    inventory: (not implemented)
256        a dictionary of user settable parameter names and
257        their associated types.  These should appear as keyword
258        arguments to the transform call.  For example, Fresnel
259        reflectivity requires the substrate density:
260             { 'rho': type.Value(10e-6/units.angstrom**2) }
261        Supply reasonable defaults in the callback so that
262        limited plotting clients work even though they cannot
263        set the inventory.
264    """
265       
266    def __call__(self,plottable,**kwargs):
267        """Transform the data.  Whenever a plottable is added
268        to the axes, the infrastructure will apply all required
269        transforms.  When the user selects a different representation
270        for the axes (via menu, script, or context menu), all
271        plottables on the axes will be transformed.  The
272        plottable should store the underlying data but set
273        the standard x,dx,y,dy,z,dz attributes appropriately.
274       
275        If the call raises a NotImplemented error the dataline
276        will not be plotted.  The associated string will usually
277        be 'Not a valid transform', though other strings are possible.
278        The application may or may not display the message to the
279        user, along with an indication of which plottable was at fault.
280        """
281        raise NotImplemented,"Not a valid transform"
282
283    # Related issues
284    # ==============
285    #
286    # log scale:
287    #    All axes have implicit log/linear scaling options.
288    #
289    # normalization:
290    #    Want to display raw counts vs detector efficiency correction
291    #    Want to normalize by time/monitor/proton current/intensity.
292    #    Want to display by eg. counts per 3 sec or counts per 10000 monitor.
293    #    Want to divide by footprint (ab initio, fitted or measured).
294    #    Want to scale by attenuator values.
295    #
296    # compare/contrast:
297    #    Want to average all visible lines with the same tag, and
298    #    display difference from one particular line.  Not a transform
299    #    issue?
300    #
301    # multiline graph:
302    #    How do we show/hide data parts.  E.g., data or theory, or
303    #    different polarization cross sections?  One way is with
304    #    tags: each plottable has a set of tags and the tags are
305    #    listed as check boxes above the plotting area.  Click a
306    #    tag and all plottables with that tag are hidden on the
307    #    plot and on the legend.
308    #
309    # nonconformant y-axes:
310    #    What do we do with temperature vs. Q and reflectivity vs. Q
311    #    on the same graph?
312    #
313    # 2D -> 1D:
314    #    Want various slices through the data.  Do transforms apply
315    #    to the sliced data as well?
316
317
318class Plottable:
319    def xaxis(self, name, units):
320        """
321            Set the name and unit of x_axis
322            @param name: the name of x-axis
323            @param units : the units of x_axis
324        """
325        self._xaxis = name
326        self._xunit = units
327
328    def yaxis(self, name, units):
329        """
330            Set the name and unit of y_axis
331            @param name: the name of y-axis
332            @param units : the units of y_axis
333        """
334        self._yaxis = name
335        self._yunit = units
336       
337    def get_xaxis(self):
338        """ Return the units and name of x-axis"""
339        return self._xaxis, self._xunit
340   
341    def get_yaxis(self):
342        """ Return the units and name of y- axis"""
343        return self._yaxis, self._yunit
344
345    @classmethod
346    def labels(cls,collection):
347        """
348        Construct a set of unique labels for a collection of plottables of
349        the same type.
350       
351        Returns a map from plottable to name.
352        """
353        n = len(collection)
354        map = {}
355        if n > 0:
356            basename = str(cls).split('.')[-1]
357            if n == 1:
358                map[collection[0]] = basename
359            else:
360                for i in xrange(len(collection)):
361                    map[collection[i]] = "%s %d"%(basename,i)
362        return map
363    ##Use the following if @classmethod doesn't work
364    # labels = classmethod(labels)
365    def getTransform(self,transx,transy):
366        self.view.transx = transx
367        self.view.transy = transy
368       
369    def __init__(self):
370        self.view = View()
371        self._xaxis = ""
372        self._xunit = ""
373        self._yaxis = ""
374        self._yunit = "" 
375       
376    def set_View(self,x,y):
377        """ Load View  """
378        self.x= x
379        self.y = y
380        self.reset_view()
381       
382    def reset_view(self):
383        """ Reload view with new value to plot"""
384        self.view = self.View(self.x, self.y, self.dx, self.dy)
385        #save the initial value
386        self.view.Xreel = self.view.x
387        self.view.Yreel = self.view.y
388        self.view.DXreel = self.view.dx
389        self.view.DYreel = self.view.dy
390       
391   
392    def render(self,plot):
393        """The base class makes sure the correct units are being used for
394        subsequent plottable. 
395       
396        For now it is assumed that the graphs are commensurate, and if you
397        put a Qx object on a Temperature graph then you had better hope
398        that it makes sense.
399        """
400       
401        plot.xaxis(self._xaxis, self._xunit)
402        plot.yaxis(self._yaxis, self._yunit)
403       
404    def colors(self):
405        """Return the number of colors need to render the object"""
406        return 1
407   
408    def transformView(self):
409       
410        self.view.transform( self.x, self.y, self.dx,self.dy)
411       
412    def returnValuesOfView(self):
413        return self.view.returnXview()
414   
415    def check_data_PlottableX(self): 
416        self.view.check_data_logX()
417       
418    def check_data_PlottableY(self): 
419        self.view.check_data_logY() 
420    def returnTransformationx(self,transx,transdx):
421        self.view.returntransformx(transx,transdx)
422    def returnTransformationy(self,transy,transdy):
423        self.view.returntransformy(transy,transdy)
424   
425    class View:
426        """
427            Representation of the data that might include a transformation
428        """
429        x = None
430        y = None
431        dx = None
432        dy = None
433       
434       
435        def __init__(self, x=None, y=None, dx=None, dy=None):
436            self.x = x
437            self.y = y
438            self.dx = dx
439            self.dy = dy
440            #to change x range to the reel range
441            self.Xreel = self.x
442            self.Yreel = self.y
443            self.DXreel = self.dx
444            self.DYreel = self.dy
445            self.transx =""
446            self.transy =""
447            self.funcx= None
448            self.funcy= None
449            self.funcdx= None
450            self.funcdy= None
451        def transform(self, x=None,y=None,dx=None, dy=None):
452            """
453                Transforms the x and dx vectors and stores the output.
454               
455                @param func: function to apply to the data
456                @param x: array of x values
457                @param dx: array of error values
458                @param errfunc: function to apply to errors
459            """
460           
461           
462            # Sanity check
463            if (x!=None) and (y!=None):
464                if dx and not len(x)==len(dx):
465                        raise ValueError, "Plottable.View: Given x and dx are not of the same length" 
466                # Check length of y array
467                if not len(y)==len(x):
468                    raise ValueError, "Plottable.View: Given y and x are not of the same length"
469           
470                if dy and not len(y)==len(dy):
471                    raise ValueError, "Plottable.View: Given y and dy are not of the same length"
472                self.x = []
473                self.y = []
474                self.dx = []
475                self.dy = []
476                tempx=[]
477                tempdx=[]
478                tempy=[]
479                tempdy=[]
480                if dx==None:
481                    dx=numpy.zeros(len(x))
482                if dy==None:
483                    dy=numpy.zeros(len(y))
484               
485                for i in range(len(x)):
486                    try:
487                         tempx =self.funcx(x[i],y[i])
488                         tempy =self.funcy(y[i],x[i])
489                         tempdx = self.funcdx(x[i], y[i], dx[i], dy[i])
490                         tempdy = self.funcdy(y[i], x[i], dy[i], dx[i])
491                       
492                         self.x.append(tempx)
493                         self.y.append(tempy)
494                         self.dx.append(tempdx)
495                         self.dy.append(tempdy)
496                    except:
497                         
498                         print "View.transform_x: skipping point %g" % x[i]
499                         print sys.exc_value   
500                # Sanity check
501                if not (len(self.x)==len(self.dx))and(len(self.x)==len(self.dy))\
502                and(len(self.x)==len(self.y))and(len(self.y)==len(self.dy)) :
503                        raise ValueError, "Plottable.View: Given x,y,dy and dx are not of the same length" 
504           
505        def returntransformx(self,funcx,funcdx):   
506            self.funcx= funcx
507            self.funcdx= funcdx
508           
509        def returntransformy(self,funcy,funcdy):   
510            self.funcy= funcy
511            self.funcdy= funcdy
512       
513        def returnXview(self):
514            return self.x,self.y,self.dx,self.dy
515       
516     
517        def reelXrange(self):
518            self.x= self.Xreel
519            self.y= self.Yreel
520            self.dx= self.DXreel
521            self.dy= self.DYreel
522       
523        def check_data_logX(self): 
524            tempx=[]
525            tempdx=[]
526            tempy=[]
527            tempdy=[]
528            if self.dx==None:
529                self.dx=numpy.zeros(len(self.x))
530            if self.dy==None:
531                self.dy=numpy.zeros(len(self.y))
532            if self.transx=="log10(x)" :
533                for i in range(len(self.x)):
534                    try:
535                        if (self.x[i]> 0):
536                           
537                            tempx.append(self.x[i])
538                            tempdx.append(self.dx[i])
539                            tempy.append(self.y[i])
540                            tempdy.append(self.dy[i])
541                    except:
542                        #print "View.transform_x: skipping point %g" %self.x[i]
543                        print sys.exc_value 
544                        pass 
545           
546            self.x=[]
547            self.dx=[]
548            self.y=[]
549            self.dy=[]
550            self.x=tempx
551            self.y=tempy
552            self.dx=tempdx
553            self.dy=tempdy
554           
555        def check_data_logY(self): 
556            tempx=[]
557            tempdx=[]
558            tempy=[]
559            tempdy=[]
560            if self.dx==None:
561                self.dx=numpy.zeros(len(self.x))
562            if self.dy==None:
563                self.dy=numpy.zeros(len(self.y))
564            if (self.transy == "log10(y)" ):
565                for i in range(len(self.x)):
566                     try:
567                        if (self.y[i]> 0):
568                            tempx.append(self.x[i])
569                            tempdx.append(self.dx[i])
570                            tempy.append(self.y[i])
571                            tempdy.append(self.dy[i])
572                     except:
573                        #print "View.transform_x: skipping point %g" %self.x[i]
574                        print sys.exc_value 
575                        pass
576               
577            self.x=[]
578            self.dx=[]
579            self.y=[]
580            self.dy=[]
581            self.x=tempx
582            self.y=tempy
583            self.dx=tempdx
584            self.dy=tempdy
585               
586           
587
588class Data1D(Plottable):
589    """Data plottable: scatter plot of x,y with errors in x and y.
590    """
591   
592    def __init__(self,x,y,dx=None,dy=None):
593        """Draw points specified by x[i],y[i] in the current color/symbol.
594        Uncertainty in x is given by dx[i], or by (xlo[i],xhi[i]) if the
595        uncertainty is asymmetric.  Similarly for y uncertainty.
596
597        The title appears on the legend.
598        The label, if it is different, appears on the status bar.
599        """
600        self.name = "data"
601        self.x = x
602        self.y = y
603        self.dx = dx
604        self.dy = dy
605        self.xaxis( 'q', 'A')
606        self.yaxis( 'intensity', 'cm')
607        self.view = self.View(self.x, self.y, self.dx, self.dy)
608       
609    def render(self,plot,**kw):
610        plot.points(self.view.x,self.view.y,dx=self.view.dx,dy=self.view.dy,**kw)
611     
612   
613    def changed(self):
614        return False
615
616    @classmethod
617    def labels(cls, collection):
618        """Build a label mostly unique within a collection"""
619        map = {}
620        for item in collection:
621            #map[item] = label(item, collection)
622            map[item] = r"$\rm{%s}$" % item.name
623        return map
624   
625class Theory1D(Plottable):
626    """Theory plottable: line plot of x,y with confidence interval y.
627    """
628    def __init__(self,x,y,dy=None):
629        """Draw lines specified in x[i],y[i] in the current color/symbol.
630        Confidence intervals in x are given by dx[i] or by (xlo[i],xhi[i])
631        if the limits are asymmetric.
632       
633        The title is the name that will show up on the legend.
634        """
635        self.name= "theo"
636        self.x = x
637        self.y = y
638        self.dy = dy
639       
640        self.view = self.View(self.x, self.y, None, self.dy)
641    def render(self,plot,**kw):
642        #plot.curve(self.x,self.y,dy=self.dy,**kw)
643        plot.curve(self.view.x,self.view.y,dy=self.view.dy,**kw)
644
645    def changed(self):
646        return False
647   
648    @classmethod
649    def labels(cls, collection):
650        """Build a label mostly unique within a collection"""
651        map = {}
652        for item in collection:
653            #map[item] = label(item, collection)
654            map[item] = r"$\rm{%s}$" % item.name
655        return map
656   
657   
658class Fit1D(Plottable):
659    """Fit plottable: composed of a data line plus a theory line.  This
660    is treated like a single object from the perspective of the graph,
661    except that it will have two legend entries, one for the data and
662    one for the theory.
663
664    The color of the data and theory will be shared."""
665
666    def __init__(self,data=None,theory=None):
667        self.data=data
668        self.theory=theory
669
670    def render(self,plot,**kw):
671        self.data.render(plot,**kw)
672        self.theory.render(plot,**kw)
673
674    def changed(self):
675        return self.data.changed() or self.theory.changed()
676
677######################################################
678
679def sample_graph():
680    import numpy as nx
681   
682    # Construct a simple graph
683    if False:
684        x = nx.array([1,2,3,4,5,6],'d')
685        y = nx.array([4,5,6,5,4,5],'d')
686        dy = nx.array([0.2, 0.3, 0.1, 0.2, 0.9, 0.3])
687    else:
688        x = nx.linspace(0,1.,10000)
689        y = nx.sin(2*nx.pi*x*2.8)
690        dy = nx.sqrt(100*nx.abs(y))/100
691    data = Data1D(x,y,dy=dy)
692    data.xaxis('distance', 'm')
693    data.yaxis('time', 's')
694    graph = Graph()
695    graph.title('Walking Results')
696    graph.add(data)
697    graph.add(Theory1D(x,y,dy=dy))
698
699    return graph
700
701def demo_plotter(graph):
702    import wx
703    #from pylab_plottables import Plotter
704    from mplplotter import Plotter
705
706    # Make a frame to show it
707    app = wx.PySimpleApp()
708    frame = wx.Frame(None,-1,'Plottables')
709    plotter = Plotter(frame)
710    frame.Show()
711
712    # render the graph to the pylab plotter
713    graph.render(plotter)
714   
715    class GraphUpdate:
716        callnum=0
717        def __init__(self,graph,plotter):
718            self.graph,self.plotter = graph,plotter
719        def __call__(self):
720            if self.graph.changed(): 
721                self.graph.render(self.plotter)
722                return True
723            return False
724        def onIdle(self,event):
725            #print "On Idle checker %d"%(self.callnum)
726            self.callnum = self.callnum+1
727            if self.__call__(): 
728                pass # event.RequestMore()
729    update = GraphUpdate(graph,plotter)
730    frame.Bind(wx.EVT_IDLE,update.onIdle)
731    app.MainLoop()
732
733import sys; print sys.version
734if __name__ == "__main__":
735    demo_plotter(sample_graph())
736   
Note: See TracBrowser for help on using the repository browser.