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

magnetic_scattrelease-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249unittest-saveload
Last change on this file since 3d72901 was 2469df7, checked in by Paul Kienzle <pkienzle@…>, 7 years ago

lint: update 'if x==True/False?' to 'if x/not x:'

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