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

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 f2f6af9 was f2f6af9, checked in by lewis, 8 years ago

Set ylim when scale changed to Porod

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