source: sasview/guitools/plottables.py @ 106ef4d

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 106ef4d was 370e587, checked in by Gervaise Alina <gervyh@…>, 16 years ago

add comments

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