source: sasview/guitools/plottables.py @ 89a436a

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 89a436a was 061775ff, checked in by Gervaise Alina <gervyh@…>, 16 years ago

update theory and fitdialog .i have put none as error for the fit line so far

  • Property mode set to 100644
File size: 20.4 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
177    def reset(self):
178        """Reset the graph."""
179        self.color = 0
180        self.symbol = 0
181        self.prop = {"xlabel":"", "xunit":None,
182                     "ylabel":"","yunit":None,
183                     "title":""}
184        self.plottables = {}
185
186    def _make_labels(self):
187        # Find groups of related plottables
188        sets = {}
189        for p in self.plottables:
190            if p.__class__ in sets:
191                sets[p.__class__].append(p)
192            else:
193                sets[p.__class__] = [p]
194               
195        # Ask each plottable class for a set of unique labels
196        labels = {}
197        for c in sets:
198            labels.update(c.labels(sets[c]))
199       
200        return labels
201   
202    def returnPlottable(self):
203        return self.plottables
204
205    def render(self,plot):
206        """Redraw the graph"""
207        plot.clear()
208        plot.properties(self.prop)
209        labels = self._make_labels()
210        for p in self.plottables:
211            p.render(plot,color=self.plottables[p],symbol=0,label=labels[p])
212        plot.render()
213
214    def __init__(self,**kw):
215        self.reset()
216        self.set(**kw)
217
218
219# Transform interface definition
220# No need to inherit from this class, just need to provide
221# the same methods.
222class Transform:
223    """Define a transform plugin to the plottable architecture.
224   
225    Transforms operate on axes.  The plottable defines the
226    set of transforms available for it, and the axes on which
227    they operate.  These transforms can operate on the x axis
228    only, the y axis only or on the x and y axes together.
229   
230    This infrastructure is not able to support transformations
231    such as log and polar plots as these require full control
232    over the drawing of axes and grids.
233   
234    A transform has a number of attributes.
235   
236    name: user visible name for the transform.  This will
237        appear in the context menu for the axis and the transform
238        menu for the graph.
239    type: operational axis.  This determines whether the
240        transform should appear on x,y or z axis context
241        menus, or if it should appear in the context menu for
242        the graph.
243    inventory: (not implemented)
244        a dictionary of user settable parameter names and
245        their associated types.  These should appear as keyword
246        arguments to the transform call.  For example, Fresnel
247        reflectivity requires the substrate density:
248             { 'rho': type.Value(10e-6/units.angstrom**2) }
249        Supply reasonable defaults in the callback so that
250        limited plotting clients work even though they cannot
251        set the inventory.
252    """
253       
254    def __call__(self,plottable,**kwargs):
255        """Transform the data.  Whenever a plottable is added
256        to the axes, the infrastructure will apply all required
257        transforms.  When the user selects a different representation
258        for the axes (via menu, script, or context menu), all
259        plottables on the axes will be transformed.  The
260        plottable should store the underlying data but set
261        the standard x,dx,y,dy,z,dz attributes appropriately.
262       
263        If the call raises a NotImplemented error the dataline
264        will not be plotted.  The associated string will usually
265        be 'Not a valid transform', though other strings are possible.
266        The application may or may not display the message to the
267        user, along with an indication of which plottable was at fault.
268        """
269        raise NotImplemented,"Not a valid transform"
270
271    # Related issues
272    # ==============
273    #
274    # log scale:
275    #    All axes have implicit log/linear scaling options.
276    #
277    # normalization:
278    #    Want to display raw counts vs detector efficiency correction
279    #    Want to normalize by time/monitor/proton current/intensity.
280    #    Want to display by eg. counts per 3 sec or counts per 10000 monitor.
281    #    Want to divide by footprint (ab initio, fitted or measured).
282    #    Want to scale by attenuator values.
283    #
284    # compare/contrast:
285    #    Want to average all visible lines with the same tag, and
286    #    display difference from one particular line.  Not a transform
287    #    issue?
288    #
289    # multiline graph:
290    #    How do we show/hide data parts.  E.g., data or theory, or
291    #    different polarization cross sections?  One way is with
292    #    tags: each plottable has a set of tags and the tags are
293    #    listed as check boxes above the plotting area.  Click a
294    #    tag and all plottables with that tag are hidden on the
295    #    plot and on the legend.
296    #
297    # nonconformant y-axes:
298    #    What do we do with temperature vs. Q and reflectivity vs. Q
299    #    on the same graph?
300    #
301    # 2D -> 1D:
302    #    Want various slices through the data.  Do transforms apply
303    #    to the sliced data as well?
304
305
306class Plottable:
307    def xaxis(self, name, units):
308        self._xaxis = name
309        self._xunit = units
310
311    def yaxis(self, name, units):
312        self._yaxis = name
313        self._yunit = units
314
315    @classmethod
316    def labels(cls,collection):
317        """
318        Construct a set of unique labels for a collection of plottables of
319        the same type.
320       
321        Returns a map from plottable to name.
322        """
323        n = len(collection)
324        map = {}
325        if n > 0:
326            basename = str(cls).split('.')[-1]
327            if n == 1:
328                map[collection[0]] = basename
329            else:
330                for i in xrange(len(collection)):
331                    map[collection[i]] = "%s %d"%(basename,i)
332        return map
333    ##Use the following if @classmethod doesn't work
334    # labels = classmethod(labels)
335
336    def __init__(self):
337        self.view = View()
338    def set_View(self,x,y):
339        """ Load View  """
340        self.x= x
341        self.y = y
342        self.reset_view()
343       
344    def reset_view(self):
345        """ Reload view with new value to plot"""
346        self.view = self.View(self.x, self.y, self.dx, self.dy)
347        print "the value of view x",self.view.x
348        print "the value of view y",self.view.y
349       
350   
351    def render(self,plot):
352        """The base class makes sure the correct units are being used for
353        subsequent plottable. 
354       
355        For now it is assumed that the graphs are commensurate, and if you
356        put a Qx object on a Temperature graph then you had better hope
357        that it makes sense.
358        """
359       
360        plot.xaxis(self._xaxis, self._xunit)
361        plot.yaxis(self._yaxis, self._yunit)
362       
363    def colors(self):
364        """Return the number of colors need to render the object"""
365        return 1
366   
367    def transform_x(self, func, errfunc):
368        """
369            @param func: reference to x transformation function
370           
371        """
372        self.view.transform_x(func, errfunc, self.x, self.dx)
373   
374    def transform_y(self, func, errfunc):
375        """
376            @param func: reference to y transformation function
377           
378        """
379        self.view.transform_y(func, errfunc, self.y, self.dy)
380   
381    class View:
382        """
383            Representation of the data that might include a transformation
384        """
385        x = None
386        y = None
387        dx = None
388        dy = None
389       
390        def __init__(self, x=None, y=None, dx=None, dy=None):
391            self.x = x
392            self.y = y
393            self.dx = dx
394            self.dy = dy
395           
396        def transform_x(self, func, errfunc, x, dx):
397            """
398                Transforms the x and dx vectors and stores the output.
399               
400                @param func: function to apply to the data
401                @param x: array of x values
402                @param dx: array of error values
403                @param errfunc: function to apply to errors
404            """
405            import copy
406            import numpy
407            # Sanity check
408            if dx and not len(x)==len(dx):
409                raise ValueError, "Plottable.View: Given x and dx are not of the same length"
410           
411           
412            self.x = numpy.zeros(len(x))
413            self.dx = numpy.zeros(len(x))
414           
415            for i in range(len(x)):
416                self.x[i] = func(x[i])
417                if dx !=None:
418                    self.dx[i] = errfunc(x[i], dx[i])
419                else:
420                   self.dx[i] = errfunc(x[i])       
421        def transform_y(self, func, errfunc, y, dy):
422            """
423                Transforms the x and dx vectors and stores the output.
424               
425                @param func: function to apply to the data
426                @param y: array of y values
427                @param dy: array of error values
428                @param errfunc: function to apply to errors
429            """
430            import copy
431            import numpy
432            # Sanity check
433            if dy and not len(y)==len(dy):
434                raise ValueError, "Plottable.View: Given y and dy are not of the same length"
435           
436            self.y = numpy.zeros(len(y))
437            self.dy = numpy.zeros(len(y))
438           
439            for i in range(len(y)):
440                 self.y[i] = func(y[i])
441                 if dy !=None:
442                     self.dy[i] = errfunc(y[i], dy[i])
443                 else:
444                     self.dy[i] = errfunc(y[i])
445     
446class Data1D(Plottable):
447    """Data plottable: scatter plot of x,y with errors in x and y.
448    """
449   
450    def __init__(self,x,y,dx=None,dy=None):
451        """Draw points specified by x[i],y[i] in the current color/symbol.
452        Uncertainty in x is given by dx[i], or by (xlo[i],xhi[i]) if the
453        uncertainty is asymmetric.  Similarly for y uncertainty.
454
455        The title appears on the legend.
456        The label, if it is different, appears on the status bar.
457        """
458        self.name = "data"
459        self.x = x
460        self.y = y
461        self.dx = dx
462        self.dy = dy
463       
464        self.view = self.View(self.x, self.y, self.dx, self.dy)
465       
466    def render(self,plot,**kw):
467        plot.points(self.view.x,self.view.y,dx=self.view.dx,dy=self.view.dy,**kw)
468        #plot.points(self.x,self.y,dx=self.dx,dy=self.dy,**kw)
469   
470    def changed(self):
471        return False
472
473    @classmethod
474    def labels(cls, collection):
475        """Build a label mostly unique within a collection"""
476        map = {}
477        for item in collection:
478            #map[item] = label(item, collection)
479            map[item] = r"$\rm{%s}$" % item.name
480        return map
481   
482class Theory1D(Plottable):
483    """Theory plottable: line plot of x,y with confidence interval y.
484    """
485    def __init__(self,x,y,dy=None):
486        """Draw lines specified in x[i],y[i] in the current color/symbol.
487        Confidence intervals in x are given by dx[i] or by (xlo[i],xhi[i])
488        if the limits are asymmetric.
489       
490        The title is the name that will show up on the legend.
491        """
492        self.x = x
493        self.y = y
494        self.dy = dy
495        #Alina:added
496        self.view = self.View(self.x, self.y, None, self.dy)
497    def render(self,plot,**kw):
498        #plot.curve(self.x,self.y,dy=self.dy,**kw)
499        plot.curve(self.view.x,self.view.y,dy=self.view.dy,**kw)
500
501    def changed(self):
502        return False
503
504
505
506class Fit1D(Plottable):
507    """Fit plottable: composed of a data line plus a theory line.  This
508    is treated like a single object from the perspective of the graph,
509    except that it will have two legend entries, one for the data and
510    one for the theory.
511
512    The color of the data and theory will be shared."""
513
514    def __init__(self,data=None,theory=None):
515        self.data=data
516        self.theory=theory
517
518    def render(self,plot,**kw):
519        self.data.render(plot,**kw)
520        self.theory.render(plot,**kw)
521
522    def changed(self):
523        return self.data.changed() or self.theory.changed()
524
525######################################################
526
527def sample_graph():
528    import numpy as nx
529   
530    # Construct a simple graph
531    if False:
532        x = nx.array([1,2,3,4,5,6],'d')
533        y = nx.array([4,5,6,5,4,5],'d')
534        dy = nx.array([0.2, 0.3, 0.1, 0.2, 0.9, 0.3])
535    else:
536        x = nx.linspace(0,1.,10000)
537        y = nx.sin(2*nx.pi*x*2.8)
538        dy = nx.sqrt(100*nx.abs(y))/100
539    data = Data1D(x,y,dy=dy)
540    data.xaxis('distance', 'm')
541    data.yaxis('time', 's')
542    graph = Graph()
543    graph.title('Walking Results')
544    graph.add(data)
545    graph.add(Theory1D(x,y,dy=dy))
546
547    return graph
548
549def demo_plotter(graph):
550    import wx
551    #from pylab_plottables import Plotter
552    from mplplotter import Plotter
553
554    # Make a frame to show it
555    app = wx.PySimpleApp()
556    frame = wx.Frame(None,-1,'Plottables')
557    plotter = Plotter(frame)
558    frame.Show()
559
560    # render the graph to the pylab plotter
561    graph.render(plotter)
562   
563    class GraphUpdate:
564        callnum=0
565        def __init__(self,graph,plotter):
566            self.graph,self.plotter = graph,plotter
567        def __call__(self):
568            if self.graph.changed(): 
569                self.graph.render(self.plotter)
570                return True
571            return False
572        def onIdle(self,event):
573            #print "On Idle checker %d"%(self.callnum)
574            self.callnum = self.callnum+1
575            if self.__call__(): 
576                pass # event.RequestMore()
577    update = GraphUpdate(graph,plotter)
578    frame.Bind(wx.EVT_IDLE,update.onIdle)
579    app.MainLoop()
580
581import sys; print sys.version
582if __name__ == "__main__":
583    demo_plotter(sample_graph())
584   
Note: See TracBrowser for help on using the repository browser.