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

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

Ensure plot colours are set correctly

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