source: sasview/src/sas/plottools/plottables.py @ 5f0be1f

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 5f0be1f was 3477478, checked in by Mathieu Doucet <doucetm@…>, 10 years ago

pylint fixes

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