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

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.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 6e6dd4c was 45dffa69, checked in by andyfaff, 8 years ago

MAINT: more 'not x is None' fixes

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