source: sasview/src/sans/plottools/plottables.py @ f468791

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 f468791 was a9d5684, checked in by Mathieu Doucet <doucetm@…>, 11 years ago
  • Property mode set to 100644
File size: 41.8 KB
Line 
1"""
2Prototype plottable object support.
3
4The main point of this prototype is to provide a clean separation between
5the style (plotter details: color, grids, widgets, etc.) and substance
6(application details: which information to plot).  Programmers should not be
7dictating line colours and plotting symbols.
8
9Unlike the problem of style in CSS or Word, where most paragraphs look
10the same, each line on a graph has to be distinguishable from its neighbours.
11Our solution is to provide parametric styles, in which a number of
12different classes of object (e.g., reflectometry data, reflectometry
13theory) representing multiple graph primitives cycle through a colour
14palette provided by the underlying plotter.
15
16A full treatment would provide perceptual dimensions of prominence and
17distinctiveness rather than a simple colour number.
18
19"""
20
21# Design question: who owns the color?
22# Is it a property of the plottable?
23# Or of the plottable as it exists on the graph?
24# Or if the graph?
25# If a plottable can appear on multiple graphs, in some case the
26# color should be the same on each graph in which it appears, and
27# in other cases (where multiple plottables from different graphs
28# coexist), the color should be assigned by the graph.  In any case
29# once a plottable is placed on the graph its color should not
30# depend on the other plottables on the graph.  Furthermore, if
31# a plottable is added and removed from a graph and added again,
32# it may be nice, but not necessary, to have the color persist.
33#
34# The safest approach seems to be to give ownership of color
35# to the graph, which will allocate the colors along with the
36# plottable.  The plottable will need to return the number of
37# colors that are needed.
38#
39# The situation is less clear for symbols.  It is less clear
40# how much the application requires that symbols be unique across
41# all plots on the graph.
42
43# Support for ancient python versions
44import copy
45import numpy
46import sys
47
48
49if 'any' not in dir(__builtins__):
50    def any(L):
51        for cond in L:
52            if cond:
53                return True
54        return False
55   
56    def all(L):
57        for cond in L:
58            if not cond:
59                return False
60        return True
61
62
63class Graph:
64    """
65    Generic plottables graph structure.
66   
67    Plot styles are based on color/symbol lists.  The user gets to select
68    the list of colors/symbols/sizes to choose from, not the application
69    developer.  The programmer only gets to add/remove lines from the
70    plot and move to the next symbol/color.
71
72    Another dimension is prominence, which refers to line sizes/point sizes.
73
74    Axis transformations allow the user to select the coordinate view
75    which provides clarity to the data.  There is no way we can provide
76    every possible transformation for every application generically, so
77    the plottable objects themselves will need to provide the transformations.
78    Here are some examples from reflectometry: ::
79   
80       independent: x -> f(x)
81          monitor scaling: y -> M*y
82          log:  y -> log(y if y > min else min)
83          cos:  y -> cos(y*pi/180)
84       dependent:   x -> f(x,y)
85          Q4:      y -> y*x^4
86          fresnel: y -> y*fresnel(x)
87       coordinated: x,y = f(x,y)
88          Q:    x -> 2*pi/L (cos(x*pi/180) - cos(y*pi/180))
89                y -> 2*pi/L (sin(x*pi/180) + sin(y*pi/180))
90       reducing: x,y = f(x1,x2,y1,y2)
91          spin asymmetry: x -> x1, y -> (y1 - y2)/(y1 + y2)
92          vector net: x -> x1, y -> y1*cos(y2*pi/180)
93         
94    Multiple transformations are possible, such as Q4 spin asymmetry
95
96    Axes have further complications in that the units of what are being
97    plotted should correspond to the units on the axes.  Plotting multiple
98    types on the same graph should be handled gracefully, e.g., by creating
99    a separate tab for each available axis type, breaking into subplots,
100    showing multiple axes on the same plot, or generating inset plots.
101    Ultimately the decision should be left to the user.
102
103    Graph properties such as grids/crosshairs should be under user control,
104    as should the sizes of items such as axis fonts, etc.  No direct
105    access will be provided to the application.
106
107    Axis limits are mostly under user control.  If the user has zoomed or
108    panned then those limits are preserved even if new data is plotted.
109    The exception is when, e.g., scanning through a set of related lines
110    in which the user may want to fix the limits so that user can compare
111    the values directly.  Another exception is when creating multiple
112    graphs sharing the same limits, though this case may be important
113    enough that it is handled by the graph widget itself.  Axis limits
114    will of course have to understand the effects of axis transformations.
115
116    High level plottable objects may be composed of low level primitives.
117    Operations such as legend/hide/show copy/paste, etc. need to operate
118    on these primitives as a group.  E.g., allowing the user to have a
119    working canvas where they can drag lines they want to save and annotate
120    them.
121
122    Graphs need to be printable.  A page layout program for entire plots
123    would be nice.
124   
125    """
126    def _xaxis_transformed(self, name, units):
127        """
128        Change the property of the x axis
129        according to an axis transformation
130        (as opposed to changing the basic properties)
131        """
132        if units != "":
133            name = "%s (%s)" % (name, units)
134        self.prop["xlabel"] = name
135        self.prop["xunit"] = units
136       
137    def _yaxis_transformed(self, name, units):
138        """
139        Change the property of the y axis
140        according to an axis transformation
141        (as opposed to changing the basic properties)
142        """
143        if units != "":
144            name = "%s (%s)" % (name, units)
145        self.prop["ylabel"] = name
146        self.prop["yunit"] = units
147       
148    def xaxis(self, name, units):
149        """
150        Properties of the x axis.
151        """
152        if units != "":
153            name = "%s (%s)" % (name, units)
154        self.prop["xlabel"] = name
155        self.prop["xunit"] = units
156        self.prop["xlabel_base"] = name
157        self.prop["xunit_base"] = units
158
159    def yaxis(self, name, units):
160        """
161        Properties of the y axis.
162        """
163        if units != "":
164            name = "%s (%s)" % (name, units)
165        self.prop["ylabel"] = name
166        self.prop["yunit"] = units
167        self.prop["ylabel_base"] = name
168        self.prop["yunit_base"] = units
169       
170    def title(self, name):
171        """
172        Graph title
173        """
174        self.prop["title"] = name
175       
176    def get(self, key):
177        """
178        Get the graph properties
179        """
180        if key == "color":
181            return self.color
182        elif key == "symbol":
183            return self.symbol
184        else:
185            return self.prop[key]
186
187    def set(self, **kw):
188        """
189        Set the graph properties
190        """
191        for key in kw:
192            if key == "color":
193                self.color = kw[key] % len(self.colorlist)
194            elif key == "symbol":
195                self.symbol = kw[key] % len(self.symbollist)
196            else:
197                self.prop[key] = kw[key]
198
199    def isPlotted(self, plottable):
200        """Return True is the plottable is already on the graph"""
201        if plottable in self.plottables:
202            return True
203        return False
204
205    def add(self, plottable, color=None):
206        """Add a new plottable to the graph"""
207        # record the colour associated with the plottable
208        if not plottable in self.plottables:
209            if color is not None:
210                self.plottables[plottable] = color
211            else:
212                self.color += plottable.colors()
213                self.plottables[plottable] = self.color
214
215    def changed(self):
216        """Detect if any graphed plottables have changed"""
217        return any([p.changed() for p in self.plottables])
218   
219    def get_range(self):
220        """
221        Return the range of all displayed plottables
222        """
223        min = None
224        max = None
225        for p in self.plottables:
226            if p.hidden == True:
227                continue
228            if not p.x == None:
229                for x_i in p.x:
230                    if min == None or x_i < min:
231                        min = x_i
232                    if max == None or x_i > max:
233                        max = x_i
234        return min, max
235   
236    def replace(self, plottable):
237        """Replace an existing plottable from the graph"""
238        selected_color = None
239        selected_plottable = None
240        for p in self.plottables.keys():
241            if plottable.id == p.id:
242                selected_plottable = p
243                selected_color = self.plottables[p]
244                break
245        if  selected_plottable is not None and selected_color is not None:
246            del self.plottables[selected_plottable]
247            self.plottables[plottable] = selected_color
248
249    def delete(self, plottable):
250        """Remove an existing plottable from the graph"""
251        if plottable in self.plottables:
252            del self.plottables[plottable]
253            self.color = len(self.plottables)
254           
255    def reset_scale(self):
256        """
257        Resets the scale transformation data to the underlying data
258        """
259        for p in self.plottables:
260            p.reset_view()
261
262    def reset(self):
263        """Reset the graph."""
264        self.color = -1
265        self.symbol = 0
266        self.prop = {"xlabel": "", "xunit": None,
267                     "ylabel": "", "yunit": None,
268                     "title": ""}
269        self.plottables = {}
270   
271    def _make_labels(self):
272        """
273        """
274        # Find groups of related plottables
275        sets = {}
276        for p in self.plottables:
277            if p.__class__ in sets:
278                sets[p.__class__].append(p)
279            else:
280                sets[p.__class__] = [p]
281        # Ask each plottable class for a set of unique labels
282        labels = {}
283        for c in sets:
284            labels.update(c.labels(sets[c]))
285        return labels
286   
287    def get_plottable(self, name):
288        """
289        Return the plottable with the given
290        name if it exists. Otherwise return None
291        """
292        for item in self.plottables:
293            if item.name == name:
294                return item
295        return None
296   
297    def returnPlottable(self):
298        """
299        This method returns a dictionary of plottables contained in graph
300        It is just by Plotpanel to interact with the complete list of plottables
301        inside the graph.
302        """
303        return self.plottables
304   
305    def render(self,plot):
306        """Redraw the graph"""
307        plot.connect.clearall()
308        plot.clear()
309        plot.properties(self.prop)
310        labels = self._make_labels()
311        for p in self.plottables:
312            if p.custom_color is not None:
313                p.render(plot, color=p.custom_color, symbol=0,
314                markersize=p.markersize, label=labels[p])
315            else:
316                p.render(plot, color=self.plottables[p], symbol=0,
317                     markersize=p.markersize, label=labels[p])
318        plot.render()
319   
320    def __init__(self, **kw):
321        self.reset()
322        self.set(**kw)
323        # Name of selected plottable, if any
324        self.selected_plottable = None
325
326
327# Transform interface definition
328# No need to inherit from this class, just need to provide
329# the same methods.
330class Transform:
331    """
332    Define a transform plugin to the plottable architecture.
333   
334    Transforms operate on axes.  The plottable defines the
335    set of transforms available for it, and the axes on which
336    they operate.  These transforms can operate on the x axis
337    only, the y axis only or on the x and y axes together.
338   
339    This infrastructure is not able to support transformations
340    such as log and polar plots as these require full control
341    over the drawing of axes and grids.
342   
343    A transform has a number of attributes.
344   
345    name: user visible name for the transform.  This will
346        appear in the context menu for the axis and the transform
347        menu for the graph.
348       
349    type: operational axis.  This determines whether the
350        transform should appear on x,y or z axis context
351        menus, or if it should appear in the context menu for
352        the graph.
353       
354    inventory: (not implemented)
355        a dictionary of user settable parameter names and
356        their associated types.  These should appear as keyword
357        arguments to the transform call.  For example, Fresnel
358        reflectivity requires the substrate density:
359             { 'rho': type.Value(10e-6/units.angstrom**2) }
360             
361        Supply reasonable defaults in the callback so that
362        limited plotting clients work even though they cannot
363        set the inventory.
364       
365    """
366    def __call__(self, plottable, **kwargs):
367        """
368        Transform the data.  Whenever a plottable is added
369        to the axes, the infrastructure will apply all required
370        transforms.  When the user selects a different representation
371        for the axes (via menu, script, or context menu), all
372        plottables on the axes will be transformed.  The
373        plottable should store the underlying data but set
374        the standard x,dx,y,dy,z,dz attributes appropriately.
375       
376        If the call raises a NotImplemented error the dataline
377        will not be plotted.  The associated string will usually
378        be 'Not a valid transform', though other strings are possible.
379        The application may or may not display the message to the
380        user, along with an indication of which plottable was at fault.
381       
382        """
383        raise NotImplemented, "Not a valid transform"
384
385    # Related issues
386    # ==============
387    #
388    # log scale:
389    #    All axes have implicit log/linear scaling options.
390    #
391    # normalization:
392    #    Want to display raw counts vs detector efficiency correction
393    #    Want to normalize by time/monitor/proton current/intensity.
394    #    Want to display by eg. counts per 3 sec or counts per 10000 monitor.
395    #    Want to divide by footprint (ab initio, fitted or measured).
396    #    Want to scale by attenuator values.
397    #
398    # compare/contrast:
399    #    Want to average all visible lines with the same tag, and
400    #    display difference from one particular line.  Not a transform
401    #    issue?
402    #
403    # multiline graph:
404    #    How do we show/hide data parts.  E.g., data or theory, or
405    #    different polarization cross sections?  One way is with
406    #    tags: each plottable has a set of tags and the tags are
407    #    listed as check boxes above the plotting area.  Click a
408    #    tag and all plottables with that tag are hidden on the
409    #    plot and on the legend.
410    #
411    # nonconformant y-axes:
412    #    What do we do with temperature vs. Q and reflectivity vs. Q
413    #    on the same graph?
414    #
415    # 2D -> 1D:
416    #    Want various slices through the data.  Do transforms apply
417    #    to the sliced data as well?
418
419
420class Plottable(object):
421    """
422    """
423    # Short ascii name to refer to the plottable in a menu
424    short_name = None
425    # Fancy name
426    name = None
427    # Data
428    x  = None
429    y  = None
430    dx = None
431    dy = None
432    # Parameter to allow a plot to be part of the list without being displayed
433    hidden = False
434    # Flag to set whether a plottable has an interactor or not
435    interactive = True
436    custom_color = None
437    markersize = 5  # default marker size is 'size 5'
438   
439    def __init__(self):
440        self.view = View()
441        self._xaxis = ""
442        self._xunit = ""
443        self._yaxis = ""
444        self._yunit = ""
445       
446    def __setattr__(self, name, value):
447        """
448        Take care of changes in View when data is changed.
449        This method is provided for backward compatibility.
450        """
451        object.__setattr__(self, name, value)
452        if name in ['x', 'y', 'dx', 'dy']:
453            self.reset_view()
454            #print "self.%s has been called" % name
455
456    def set_data(self, x, y, dx=None, dy=None):
457        """
458        """
459        self.x = x
460        self.y = y
461        self.dy = dy
462        self.dx = dx
463        self.transformView()
464   
465    def xaxis(self, name, units):
466        """
467        Set the name and unit of x_axis
468       
469        :param name: the name of x-axis
470        :param units: the units of x_axis
471       
472        """
473        self._xaxis = name
474        self._xunit = units
475
476    def yaxis(self, name, units):
477        """
478        Set the name and unit of y_axis
479       
480        :param name: the name of y-axis
481        :param units: the units of y_axis
482       
483        """
484        self._yaxis = name
485        self._yunit = units
486       
487    def get_xaxis(self):
488        """Return the units and name of x-axis"""
489        return self._xaxis, self._xunit
490   
491    def get_yaxis(self):
492        """ Return the units and name of y- axis"""
493        return self._yaxis, self._yunit
494
495    @classmethod
496    def labels(cls, collection):
497        """
498        Construct a set of unique labels for a collection of plottables of
499        the same type.
500       
501        Returns a map from plottable to name.
502       
503        """
504        n = len(collection)
505        map = {}
506        if n > 0:
507            basename = str(cls).split('.')[-1]
508            if n == 1:
509                map[collection[0]] = basename
510            else:
511                for i in xrange(len(collection)):
512                    map[collection[i]] = "%s %d" % (basename, i)
513        return map
514
515    ##Use the following if @classmethod doesn't work
516    # labels = classmethod(labels)
517    def setLabel(self, labelx, labely):
518        """
519        It takes a label of the x and y transformation and set View parameters
520       
521        :param transx: The label of x transformation is sent by Properties Dialog
522        :param transy: The label of y transformation is sent Properties Dialog
523       
524        """
525        self.view.xLabel = labelx
526        self.view.yLabel = labely
527   
528    def set_View(self, x, y):
529        """Load View"""
530        self.x = x
531        self.y = y
532        self.reset_view()
533       
534    def reset_view(self):
535        """Reload view with new value to plot"""
536        self.view = View(self.x, self.y, self.dx, self.dy)
537        self.view.Xreel = self.view.x
538        self.view.Yreel = self.view.y
539        self.view.DXreel = self.view.dx
540        self.view.DYreel = self.view.dy
541       
542    def render(self, plot):
543        """
544        The base class makes sure the correct units are being used for
545        subsequent plottable.
546       
547        For now it is assumed that the graphs are commensurate, and if you
548        put a Qx object on a Temperature graph then you had better hope
549        that it makes sense.
550       
551        """
552        plot.xaxis(self._xaxis, self._xunit)
553        plot.yaxis(self._yaxis, self._yunit)
554       
555    def is_empty(self):
556        """
557        Returns True if there is no data stored in the plottable
558        """
559        if not self.x == None and len(self.x) == 0 \
560            and not self.y == None and len(self.y) == 0:
561            return True
562        return False
563       
564    def colors(self):
565        """Return the number of colors need to render the object"""
566        return 1
567   
568    def transformView(self):
569        """
570        It transforms x, y before displaying
571        """
572        self.view.transform(self.x, self.y, self.dx, self.dy)
573       
574    def returnValuesOfView(self):
575        """
576        Return View parameters and it is used by Fit Dialog
577        """
578        return self.view.returnXview()
579   
580    def check_data_PlottableX(self):
581        """
582        Since no transformation is made for log10(x), check that
583        no negative values is plot in log scale
584        """
585        self.view.check_data_logX()
586       
587    def check_data_PlottableY(self):
588        """
589        Since no transformation is made for log10(y), check that
590        no negative values is plot in log scale
591        """
592        self.view.check_data_logY()
593       
594    def transformX(self, transx, transdx):
595        """
596        Receive pointers to function that transform x and dx
597        and set corresponding View pointers
598       
599        :param transx: pointer to function that transforms x
600        :param transdx: pointer to function that transforms dx
601       
602        """
603        self.view.setTransformX(transx, transdx)
604       
605    def transformY(self, transy, transdy):
606        """
607        Receive pointers to function that transform y and dy
608        and set corresponding View pointers
609       
610        :param transy: pointer to function that transforms y
611        :param transdy: pointer to function that transforms dy
612       
613        """
614        self.view.setTransformY(transy, transdy)
615       
616    def onReset(self):
617        """
618        Reset x, y, dx, dy view with its parameters
619        """
620        self.view.onResetView()
621       
622    def onFitRange(self, xmin=None, xmax=None):
623        """
624        It limits View data range to plot from min to max
625       
626        :param xmin: the minimum value of x to plot.
627        :param xmax: the maximum value of x to plot
628       
629        """
630        self.view.onFitRangeView(xmin, xmax)
631   
632   
633class View:
634    """
635    Representation of the data that might include a transformation
636    """
637    x = None
638    y = None
639    dx = None
640    dy = None
641
642    def __init__(self, x=None, y=None, dx=None, dy=None):
643        """
644        """
645        self.x = x
646        self.y = y
647        self.dx = dx
648        self.dy = dy
649        # To change x range to the reel range
650        self.Xreel = self.x
651        self.Yreel = self.y
652        self.DXreel = self.dx
653        self.DYreel = self.dy
654        # Labels of x and y received from Properties Dialog
655        self.xLabel = ""
656        self.yLabel = ""
657        # Function to transform x, y, dx and dy
658        self.funcx = None
659        self.funcy = None
660        self.funcdx = None
661        self.funcdy = None
662
663    def transform(self, x=None, y=None, dx=None, dy=None):
664        """
665        Transforms the x,y,dx and dy vectors and stores
666         the output in View parameters
667
668        :param x: array of x values
669        :param y: array of y values
670        :param dx: array of  errors values on x
671        :param dy: array of error values on y
672       
673        """
674        # Sanity check
675        # Do the transofrmation only when x and y are empty
676        has_err_x = not (dx == None or len(dx) == 0)
677        has_err_y = not (dy == None or len(dy) == 0)
678       
679        if(x != None) and (y != None):
680            if not dx == None and not len(dx) == 0 and not len(x) == len(dx):
681                msg = "Plottable.View: Given x and dx are not"
682                msg += " of the same length"
683                raise ValueError, msg
684            # Check length of y array
685            if not len(y) == len(x):
686                msg = "Plottable.View: Given y "
687                msg += "and x are not of the same length"
688                raise ValueError, msg
689       
690            if not dy == None and not len(dy) == 0 and not len(y) == len(dy):
691                msg = "Plottable.View: Given y and dy are not of the same "
692                msg += "length: len(y)=%s, len(dy)=%s" % (len(y), len(dy))
693                raise ValueError, msg
694            self.x = []
695            self.y = []
696            if has_err_x:
697                self.dx = []
698            else:
699                self.dx = None
700            if has_err_y:
701                self.dy = []
702            else:
703                self.dy = None
704            tempx = []
705            tempy = []
706            if not has_err_x:
707                dx = numpy.zeros(len(x))
708            if not has_err_y:
709                dy = numpy.zeros(len(y))
710            for i in range(len(x)):
711                try:
712                    tempx = self.funcx(x[i], y[i])
713                    tempy = self.funcy(y[i], x[i])
714                    if has_err_x:
715                        tempdx = self.funcdx(x[i], y[i], dx[i], dy[i])
716                    if has_err_y:
717                        tempdy = self.funcdy(y[i], x[i], dy[i], dx[i])
718                    self.x.append(tempx)
719                    self.y.append(tempy)
720                    if has_err_x:
721                        self.dx.append(tempdx)
722                    if has_err_y:
723                        self.dy.append(tempdy)
724                except:
725                    tempx = x[i]
726                    tempy = y[i]
727                    tempdy = dy[i]
728            # Sanity check
729            if not len(self.x) == len(self.y):
730                msg = "Plottable.View: transformed x "
731                msg += "and y are not of the same length"
732                raise ValueError, msg
733            if has_err_x and not (len(self.x) and len(self.dx)):
734                msg = "Plottable.View: transformed x and dx"
735                msg += " are not of the same length"
736                raise ValueError, msg
737            if has_err_y and not (len(self.y) and len(self.dy)):
738                msg = "Plottable.View: transformed y"
739                msg += " and dy are not of the same length"
740                raise ValueError, msg
741            # Check that negative values are not plot on x and y axis for
742            # log10 transformation
743            self.check_data_logX()
744            self.check_data_logY()
745            # Store x ,y dx,and dy in their full range for reset
746            self.Xreel = self.x
747            self.Yreel = self.y
748            self.DXreel = self.dx
749            self.DYreel = self.dy
750               
751    def onResetView(self):
752        """
753        Reset x,y,dx and y in their full range  and in the initial scale
754        in case their previous range has changed
755        """
756        self.x = self.Xreel
757        self.y = self.Yreel
758        self.dx = self.DXreel
759        self.dy = self.DYreel
760       
761    def setTransformX(self, funcx, funcdx):
762        """
763        Receive pointers to function that transform x and dx
764        and set corresponding View pointers
765       
766        :param transx: pointer to function that transforms x
767        :param transdx: pointer to function that transforms dx
768        """
769        self.funcx = funcx
770        self.funcdx = funcdx
771       
772    def setTransformY(self, funcy, funcdy):
773        """
774        Receive pointers to function that transform y and dy
775        and set corresponding View pointers
776       
777        :param transx: pointer to function that transforms y
778        :param transdx: pointer to function that transforms dy
779        """
780        self.funcy = funcy
781        self.funcdy = funcdy
782   
783    def returnXview(self):
784        """
785        Return View  x,y,dx,dy
786        """
787        return self.x, self.y, self.dx, self.dy
788   
789    def check_data_logX(self):
790        """
791        Remove negative value in x vector to avoid plotting negative
792        value of Log10
793        """
794        tempx = []
795        tempdx = []
796        tempy = []
797        tempdy = []
798        if self.dx == None:
799            self.dx = numpy.zeros(len(self.x))
800        if self.dy == None:
801            self.dy = numpy.zeros(len(self.y))
802        if self.xLabel == "log10(x)":
803            for i in range(len(self.x)):
804                try:
805                    if (self.x[i] > 0):
806                        tempx.append(self.x[i])
807                        tempdx.append(self.dx[i])
808                        tempy.append(self.y[i])
809                        tempdy.append(self.dy[i])
810                except:
811                    print "check_data_logX: skipping point x %g" % self.x[i]
812                    print sys.exc_value
813                    pass 
814            self.x = tempx
815            self.y = tempy
816            self.dx = tempdx
817            self.dy = tempdy
818       
819    def check_data_logY(self):
820        """
821        Remove negative value in y vector
822        to avoid plotting negative value of Log10
823       
824        """
825        tempx = []
826        tempdx = []
827        tempy = []
828        tempdy = []
829        if self.dx == None:
830            self.dx = numpy.zeros(len(self.x))
831        if self.dy == None:
832            self.dy = numpy.zeros(len(self.y))
833        if (self.yLabel == "log10(y)"):
834            for i in range(len(self.x)):
835                try:
836                    if (self.y[i] > 0):
837                        tempx.append(self.x[i])
838                        tempdx.append(self.dx[i])
839                        tempy.append(self.y[i])
840                        tempdy.append(self.dy[i])
841                except:
842                    print "check_data_logY: skipping point %g" % self.y[i]
843                    print sys.exc_value
844                    pass
845            self.x = tempx
846            self.y = tempy
847            self.dx = tempdx
848            self.dy = tempdy
849           
850    def onFitRangeView(self, xmin=None, xmax=None):
851        """
852        It limits View data range to plot from min to max
853       
854        :param xmin: the minimum value of x to plot.
855        :param xmax: the maximum value of x to plot
856       
857        """
858        tempx = []
859        tempdx = []
860        tempy = []
861        tempdy = []
862        if self.dx == None:
863            self.dx = numpy.zeros(len(self.x))
864        if self.dy == None:
865            self.dy = numpy.zeros(len(self.y))
866        if (xmin != None) and (xmax != None):
867            for i in range(len(self.x)):
868                if (self.x[i] >= xmin) and (self.x[i] <= xmax):
869                    tempx.append(self.x[i])
870                    tempdx.append(self.dx[i])
871                    tempy.append(self.y[i])
872                    tempdy.append(self.dy[i])
873            self.x = tempx
874            self.y = tempy
875            self.dx = tempdx
876            self.dy = tempdy
877
878             
879class Data2D(Plottable):
880    """
881    2D data class for image plotting
882    """
883    def __init__(self, image=None, qx_data=None, qy_data=None,
884                  err_image=None, xmin=None, xmax=None, ymin=None,
885                  ymax=None, zmin=None, zmax=None):
886        """
887        Draw image
888        """
889        Plottable.__init__(self)
890        self.name = "Data2D"
891        self.label = None
892        self.data = image
893        self.qx_data = qx_data
894        self.qy_data = qx_data
895        self.err_data = err_image
896        self.source = None
897        self.detector = []
898   
899        ## Units for Q-values
900        self.xy_unit = 'A^{-1}'
901        ## Units for I(Q) values
902        self.z_unit = 'cm^{-1}'
903        self._zaxis = ''
904        # x-axis unit and label
905        self._xaxis = '\\rm{Q_{x}}'
906        self._xunit = 'A^{-1}'
907        # y-axis unit and label
908        self._yaxis = '\\rm{Q_{y}}'
909        self._yunit = 'A^{-1}'
910       
911        ### might remove that later
912        ## Vector of Q-values at the center of each bin in x
913        self.x_bins = []
914        ## Vector of Q-values at the center of each bin in y
915        self.y_bins = []
916       
917        #x and y boundaries
918        self.xmin = xmin
919        self.xmax = xmax
920        self.ymin = ymin
921        self.ymax = ymax
922       
923        self.zmin = zmin
924        self.zmax = zmax
925        self.id = None
926       
927    def xaxis(self, label, unit):
928        """
929        set x-axis
930       
931        :param label: x-axis label
932        :param unit: x-axis unit
933       
934        """
935        self._xaxis = label
936        self._xunit = unit
937       
938    def yaxis(self, label, unit):
939        """
940        set y-axis
941       
942        :param label: y-axis label
943        :param unit: y-axis unit
944       
945        """
946        self._yaxis = label
947        self._yunit = unit
948       
949    def zaxis(self, label, unit):
950        """
951        set z-axis
952       
953        :param label: z-axis label
954        :param unit: z-axis unit
955       
956        """
957        self._zaxis = label
958        self._zunit = unit
959       
960    def setValues(self, datainfo=None):
961        """
962        Use datainfo object to initialize data2D
963       
964        :param datainfo: object
965       
966        """
967        self.image = copy.deepcopy(datainfo.data)
968        self.qx_data = copy.deepcopy(datainfo.qx_data)
969        self.qy_data = copy.deepcopy(datainfo.qy_data)
970        self.err_image = copy.deepcopy(datainfo.err_data)
971       
972        self.xy_unit = datainfo.Q_unit
973        self.z_unit = datainfo.I_unit
974        self._zaxis = datainfo._zaxis
975       
976        self.xaxis(datainfo._xunit, datainfo._xaxis)
977        self.yaxis(datainfo._yunit, datainfo._yaxis)
978        #x and y boundaries
979        self.xmin = datainfo.xmin
980        self.xmax = datainfo.xmax
981        self.ymin = datainfo.ymin
982        self.ymax = datainfo.ymax
983        ## Vector of Q-values at the center of each bin in x
984        self.x_bins = datainfo.x_bins
985        ## Vector of Q-values at the center of each bin in y
986        self.y_bins = datainfo.y_bins
987       
988    def set_zrange(self, zmin=None, zmax=None):
989        """
990        """
991        if zmin < zmax:
992            self.zmin = zmin
993            self.zmax = zmax
994        else:
995            raise "zmin is greater or equal to zmax "
996       
997    def render(self, plot, **kw):
998        """
999        Renders the plottable on the graph
1000       
1001        """
1002        plot.image(self.data, self.qx_data, self.qy_data,
1003                   self.xmin, self.xmax, self.ymin,
1004                   self.ymax, self.zmin, self.zmax, **kw)
1005       
1006    def changed(self):
1007        """
1008        """
1009        return False
1010   
1011    @classmethod
1012    def labels(cls, collection):
1013        """Build a label mostly unique within a collection"""
1014        map = {}
1015        for item in collection:
1016            #map[item] = label(item, collection)
1017            #map[item] = r"$\rm{%s}$" % item.name
1018            if item.label == "Data2D":
1019                item.label = item.name
1020            map[item] = item.label
1021        return map
1022
1023
1024class Data1D(Plottable):
1025    """
1026    Data plottable: scatter plot of x,y with errors in x and y.
1027    """
1028   
1029    def __init__(self, x, y, dx=None, dy=None):
1030        """
1031        Draw points specified by x[i],y[i] in the current color/symbol.
1032        Uncertainty in x is given by dx[i], or by (xlo[i],xhi[i]) if the
1033        uncertainty is asymmetric.  Similarly for y uncertainty.
1034
1035        The title appears on the legend.
1036        The label, if it is different, appears on the status bar.
1037        """
1038        Plottable.__init__(self)
1039        self.name = "data"
1040        self.label = "data"
1041        self.x = x
1042        self.y = y
1043        self.dx = dx
1044        self.dy = dy
1045        self.source = None
1046        self.detector = None
1047        self.xaxis('', '')
1048        self.yaxis('', '')
1049        self.view = View(self.x, self.y, self.dx, self.dy)
1050        self.symbol = 0
1051        self.custom_color = None
1052        self.markersize = 5
1053        self.id = None
1054        self.zorder = 1
1055        self.hide_error = False
1056     
1057    def render(self, plot, **kw):
1058        """
1059        Renders the plottable on the graph
1060        """
1061        if self.interactive == True:
1062            kw['symbol'] = self.symbol
1063            kw['id'] = self.id
1064            kw['hide_error'] = self.hide_error
1065            kw['markersize'] = self.markersize
1066            plot.interactive_points(self.view.x, self.view.y,
1067                                    dx=self.view.dx, dy=self.view.dy,
1068              name=self.name, zorder=self.zorder, **kw)
1069        else:
1070            kw['id'] =  self.id
1071            kw['hide_error'] = self.hide_error
1072            kw['symbol'] = self.symbol
1073            kw['color'] = self.custom_color
1074            kw['markersize'] = self.markersize
1075            plot.points(self.view.x, self.view.y, dx=self.view.dx,
1076                dy=self.view.dy, zorder=self.zorder, 
1077                marker=self.symbollist[self.symbol], **kw)
1078       
1079    def changed(self):
1080        return False
1081
1082    @classmethod
1083    def labels(cls, collection):
1084        """Build a label mostly unique within a collection"""
1085        map = {}
1086        for item in collection:
1087            #map[item] = label(item, collection)
1088            #map[item] = r"$\rm{%s}$" % item.name
1089            if item.label == "data":
1090                item.label = item.name
1091            map[item] = item.label
1092        return map
1093   
1094   
1095class Theory1D(Plottable):
1096    """
1097    Theory plottable: line plot of x,y with confidence interval y.
1098   
1099    """
1100    def __init__(self, x, y, dy=None):
1101        """
1102        Draw lines specified in x[i],y[i] in the current color/symbol.
1103        Confidence intervals in x are given by dx[i] or by (xlo[i],xhi[i])
1104        if the limits are asymmetric.
1105       
1106        The title is the name that will show up on the legend.
1107        """
1108        Plottable.__init__(self)
1109        msg = "Theory1D is no longer supported, please use Data1D and change"
1110        msg += " symbol.\n"
1111        raise DeprecationWarning, msg
1112        self.name = "theory"
1113        self.label = "theory"
1114        self.x = x
1115        self.y = y
1116        self.dy = dy
1117        self.xaxis('', '')
1118        self.yaxis('', '')
1119        self.view = View(self.x, self.y, None, self.dy)
1120        self.symbol = 0
1121        self.id = None
1122        self.zorder = 10
1123       
1124    def render(self, plot, **kw):
1125        """
1126        """
1127        if self.interactive == True:
1128            kw['id'] = self.id
1129            plot.interactive_curve(self.view.x, self.view.y,
1130                                   dy=self.view.dy,
1131                                   name=self.name, zorder=self.zorder, **kw)
1132        else:
1133            kw['id'] = self.id
1134            plot.curve(self.view.x, self.view.y, dy=self.view.dy, 
1135                       zorder=self.zorder, **kw)
1136           
1137    def changed(self):
1138        return False
1139   
1140    @classmethod
1141    def labels(cls, collection):
1142        """Build a label mostly unique within a collection"""
1143        map = {}
1144        for item in collection:
1145            if item.label == "theory":
1146                item.label = item.name
1147            map[item] = item.label
1148        return map
1149   
1150   
1151class Fit1D(Plottable):
1152    """
1153    Fit plottable: composed of a data line plus a theory line.  This
1154    is treated like a single object from the perspective of the graph,
1155    except that it will have two legend entries, one for the data and
1156    one for the theory.
1157
1158    The color of the data and theory will be shared.
1159   
1160    """
1161    def __init__(self, data=None, theory=None):
1162        """
1163        """
1164        Plottable.__init__(self)
1165        self.data = data
1166        self.theory = theory
1167
1168    def render(self, plot, **kw):
1169        """
1170        """
1171        self.data.render(plot, **kw)
1172        self.theory.render(plot, **kw)
1173
1174    def changed(self):
1175        """
1176        """
1177        return self.data.changed() or self.theory.changed()
1178
1179
1180# ---------------------------------------------------------------
1181class Text(Plottable):
1182    """
1183    """
1184    def __init__(self, text=None, xpos=0.5, ypos=0.9, name='text'):
1185        """
1186        Draw the user-defined text in plotter
1187        We can specify the position of text
1188        """
1189        Plottable.__init__(self)
1190        self.name = name
1191        self.text = text
1192        self.xpos = xpos
1193        self.ypos = ypos
1194       
1195    def render(self, plot, **kw):
1196        """
1197        """
1198        from matplotlib import transforms
1199
1200        xcoords = transforms.blended_transform_factory(plot.subplot.transAxes,
1201                                                       plot.subplot.transAxes)
1202        plot.subplot.text(self.xpos,
1203                          self.ypos,
1204                          self.text,
1205                          label=self.name,
1206                          transform=xcoords,
1207                          )
1208           
1209    def setText(self, text):
1210        """Set the text string."""
1211        self.text = text
1212
1213    def getText(self, text):
1214        """Get the text string."""
1215        return self.text
1216
1217    def set_x(self, x):
1218        """
1219        Set the x position of the text
1220        ACCEPTS: float
1221        """
1222        self.xpos = x
1223
1224    def set_y(self, y):
1225        """
1226        Set the y position of the text
1227        ACCEPTS: float
1228        """
1229        self.ypos = y
1230     
1231
1232# ---------------------------------------------------------------
1233class Chisq(Plottable):
1234    """
1235    Chisq plottable plots the chisq
1236    """
1237    def __init__(self, chisq=None):
1238        """
1239        Draw the chisq in plotter
1240        We can specify the position of chisq
1241        """
1242        Plottable.__init__(self)
1243        self.name = "chisq"
1244        #super( Chisq, self).__init__(None, None, None, None)
1245        self._chisq = chisq
1246        self.xpos = 0.5
1247        self.ypos = 0.9
1248       
1249    def render(self, plot, **kw):
1250        """
1251        """
1252        if  self._chisq == None:
1253            chisqTxt = r'$\chi^2=$'
1254        else:
1255            chisqTxt = r'$\chi^2=%g$' % (float(self._chisq))
1256
1257        from matplotlib import transforms
1258
1259        xcoords = transforms.blended_transform_factory(plot.subplot.transAxes,
1260                                                     plot.subplot.transAxes)
1261        plot.subplot.text(self.xpos,
1262                          self.ypos,
1263                          chisqTxt, label='chisq',
1264                          transform=xcoords,)
1265           
1266    def setChisq(self, chisq):
1267        """
1268        Set the chisq value.
1269        """
1270        self._chisq = chisq
1271
1272
1273######################################################
1274
1275def sample_graph():
1276    import numpy as nx
1277   
1278    # Construct a simple graph
1279    if False:
1280        x = nx.array([1,2,3,4,5,6], 'd')
1281        y = nx.array([4,5,6,5,4,5], 'd')
1282        dy = nx.array([0.2, 0.3, 0.1, 0.2, 0.9, 0.3])
1283    else:
1284        x = nx.linspace(0, 1., 10000)
1285        y = nx.sin(2 * nx.pi * x * 2.8)
1286        dy = nx.sqrt(100 * nx.abs(y)) / 100
1287    data = Data1D(x, y, dy=dy)
1288    data.xaxis('distance', 'm')
1289    data.yaxis('time', 's')
1290    graph = Graph()
1291    graph.title('Walking Results')
1292    graph.add(data)
1293    graph.add(Theory1D(x, y, dy=dy))
1294    return graph
1295
1296
1297def demo_plotter(graph):
1298    import wx
1299    from pylab_plottables import Plotter
1300    #from mplplotter import Plotter
1301
1302    # Make a frame to show it
1303    app = wx.PySimpleApp()
1304    frame = wx.Frame(None, -1, 'Plottables')
1305    plotter = Plotter(frame)
1306    frame.Show()
1307
1308    # render the graph to the pylab plotter
1309    graph.render(plotter)
1310   
1311    class GraphUpdate:
1312        callnum = 0
1313       
1314        def __init__(self, graph, plotter):
1315            self.graph, self.plotter = graph, plotter
1316       
1317        def __call__(self):
1318            if self.graph.changed():
1319                self.graph.render(self.plotter)
1320                return True
1321            return False
1322       
1323        def onIdle(self, event):
1324            self.callnum = self.callnum + 1
1325            if self.__call__(): 
1326                pass  # event.RequestMore()
1327    update = GraphUpdate(graph, plotter)
1328    frame.Bind(wx.EVT_IDLE, update.onIdle)
1329    app.MainLoop()
Note: See TracBrowser for help on using the repository browser.