source: sasview/guitools/plottables.py @ 8e44d51

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

one more bug fixed

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