source: sasview/src/sas/sasgui/plottools/plottables.py @ 0765c00

Last change on this file since 0765c00 was 9a5097c, checked in by andyfaff, 8 years ago

MAINT: import numpy as np

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