~ubuntu-branches/ubuntu/karmic/python-scipy/karmic

« back to all changes in this revision

Viewing changes to scipy/sandbox/plt/plot_utility.py

  • Committer: Bazaar Package Importer
  • Author(s): Ondrej Certik
  • Date: 2008-06-16 22:58:01 UTC
  • mfrom: (2.1.24 intrepid)
  • Revision ID: james.westby@ubuntu.com-20080616225801-irdhrpcwiocfbcmt
Tags: 0.6.0-12
* The description updated to match the current SciPy (Closes: #489149).
* Standards-Version bumped to 3.8.0 (no action needed)
* Build-Depends: netcdf-dev changed to libnetcdf-dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
""" General purpose classes for plotting utility.
 
2
 
 
3
    Many of these classes and function are helpful for laying out the
 
4
    location of element of a graph (titles, tick labels, ticks, etc).
 
5
    A simple class for handling properties of graph elements is also
 
6
    included.  Everything here is independent of any specific GUI or
 
7
    drawing platform
 
8
    .
 
9
    The classes and functions break out into 3 main sections:
 
10
 
 
11
    General layout:
 
12
        class box_object   -- Helps align rectangular graph elements
 
13
                              relative to one another.  Currently only
 
14
                              Int values are allowed for box dimensions.
 
15
        class point_object -- Alignment of points with one-another or
 
16
                              box_objects.
 
17
    Property Management:
 
18
        class poperty_object -- base class for any graph object that
 
19
                                manages a set of properties.  Pretty
 
20
                                low-tech compared to graphite and Zope.
 
21
                                It could use additions for error checking.
 
22
 
 
23
    Axis layout functions:
 
24
        auto_ticks          -- find set of tick locations for an axis.
 
25
        format_tick_labels  -- convert the numerical tick values to strings
 
26
                               for labels
 
27
        auto_interval       -- determine an appropriate tick interval for axis
 
28
        auto_bounds         -- determine appropriate end points for axis
 
29
        and a few others...
 
30
"""
 
31
 
 
32
 
 
33
#----------------------------------------------------------
 
34
#               General layout classes
 
35
#----------------------------------------------------------
 
36
from Numeric import *
 
37
from numpy.core.umath import *
 
38
import numpy.limits as limits
 
39
import numpy as misc
 
40
 
 
41
LEFT,RIGHT,TOP,BOTTOM = 0,1,2,3 # used by same_as() method
 
42
 
 
43
TYPE = Int # all values in a box_object are forced to be integers
 
44
           # might change this later when we've looked at round off
 
45
           # issues more thoroughly.
 
46
 
 
47
class box_object:
 
48
    """ Helpful for laying out rectangles.
 
49
 
 
50
        This class has three general areas of functionality:
 
51
            1. Retreive information about a box such as it's size,
 
52
               the x-coordinate of its left side, and others.
 
53
            2. Specify the location of one box (rectangle) in relation to
 
54
               another using methods such as left_of(), above(),
 
55
               or center_on().
 
56
            3. Change the size (and location) of a box by either choping off
 
57
               a portion of the box using trim_right(), trim_left(), and
 
58
               others or inflating the box by a percentage.
 
59
    """
 
60
    def __init__(self,topleft,size):
 
61
        self.topleft = asarray(topleft,TYPE)
 
62
        self._size = asarray(size,TYPE)
 
63
 
 
64
    #--------------- interrogation functions --------------------#
 
65
    def left(self):
 
66
        "Return the x-coordinate of the left edge."
 
67
        return self.topleft[0]
 
68
    def right(self):
 
69
        "Return the x-coordinate of the right edge."
 
70
        return self.topleft[0] + self.width()
 
71
    def top(self):
 
72
        "Return the y-coordinate of the top edge."
 
73
        return self.topleft[1]
 
74
    def bottom(self):
 
75
        "Return the y-coordinate of the bottom edge."
 
76
        return self.topleft[1] + self.height()
 
77
    def center_x(self):
 
78
        "Return the x-coordinate of the boxes center."
 
79
        return self.topleft[0] + .5 * self.width()
 
80
    def center_y(self):
 
81
        "Return the y-coordinate of the left edge."
 
82
        return self.topleft[1] + .5 * self.height()
 
83
    def size(self):
 
84
        "Return the size of the box as array((width,height))."
 
85
        return self._size
 
86
    def width(self):
 
87
        "Return the width of the box (length along x-axis)"
 
88
        return self.size()[0]
 
89
    def height(self):
 
90
        "Return the width of the box (length along y-axis)"
 
91
        return self.size()[1]
 
92
    def center(self):
 
93
        "Return the coordinates of the boxes center as an array"
 
94
        return self.topleft + .5 * self.size()
 
95
    def radial(self,angle):
 
96
        """ Length of a ray extending from the center of the box at the
 
97
            specified polar angle (in radians) to the edge of the box"""
 
98
        # Could have some div_by_zero errors if h or w = 0
 
99
        #   --> haven't thought about this much
 
100
        w,h = self.size()
 
101
        slope = abs(tan(angle))
 
102
        if slope > float(h)/w:
 
103
            s1 = h*.5
 
104
            s2 = s1/slope
 
105
        else:
 
106
            s1 = w*.5
 
107
            s2 = s1*slope
 
108
        return sqrt(s1**2+s2**2)
 
109
 
 
110
    #------------- Moving and resizing methods ----------------#
 
111
    def move(self,location):
 
112
        """ Move box so that its top left corner is located at
 
113
            the specified location.  Location is an (x,y) pair."""
 
114
        self.topleft = asarray(location,TYPE)
 
115
    def translate(self,offset):
 
116
        """ Shift the box's location by the amount specifed in
 
117
            offset.  Offset is an (x,y) pair."""
 
118
        self.topleft = self.topleft + asarray(offset,TYPE)
 
119
    def set_size(self,sz):
 
120
        """ Set the size of the box. sz is an (x,y) pair."""
 
121
        self._size = asarray(sz,TYPE)
 
122
    def set_width(self,width):
 
123
        """ Set the width of the box. sz is a numeric value"""
 
124
        self.set_size((width,self.height()))
 
125
    def set_height(self,height):
 
126
        """ Set the height of the box. sz is a numeric value."""
 
127
        self.set_size((self.width(),height))
 
128
 
 
129
    #-------------------- Relational Positioning ----------------#
 
130
    def above(self,other_box,margin=0):
 
131
        """Move box so that its bottom edge has same y-coord of the
 
132
           top edge of other_box.  The size does not change.  Optionally,
 
133
           margin specifies the gap between the boxes."""
 
134
        self.topleft[1] = other_box.top() - self.height() - margin
 
135
    def below(self,other_box,margin=0):
 
136
        """Move box so that its top edge has same y-coord of the
 
137
           bottom edge of other_box.  The size does not change.  Optionally,
 
138
           margin specifies the gap between the boxes."""
 
139
        self.topleft[1] = other_box.bottom() + margin
 
140
    def left_of(self,other_box,margin=0):
 
141
        """Move box so that its right edge has same x-coord of the
 
142
           left edge of other_box.  The size does not change.  Optionally,
 
143
           margin specifies the gap between the boxes."""
 
144
        self.topleft[0] = other_box.left() - self.width() - margin
 
145
    def right_of(self,other_box,margin=0):
 
146
        """Move box so that its left edge has same x-coord of the
 
147
           right edge of other_box.  The size does not change.  Optionally,
 
148
           margin specifies the gap between the boxes."""
 
149
        self.topleft[0] = other_box.right() + margin
 
150
    def center_on_x_of(self,other_box):
 
151
        """Move box so that x-coord of its center equals the x-coord of
 
152
           other_box's center. The size does not change."""
 
153
        self.topleft[0] = other_box.center_x() - .5 * self.width()
 
154
    def center_on_y_of(self,other_box):
 
155
        """Move box so that y-coord of its center equals the y-coord of
 
156
           other_box's center. The size does not change."""
 
157
        self.topleft[1] = other_box.center_y() - .5 * self.height()
 
158
    def center_on(self,other_box):
 
159
        """Move box so that its center is the same as other_box's center.
 
160
           The size does not change."""
 
161
        self.center_on_x_of(other_box)
 
162
        self.center_on_y_of(other_box)
 
163
    def same_as(self,other_box,edge,margin=0):
 
164
        """Set the specified edge of this box to be aligned with the
 
165
           same edge of other_box.  The size does not change.
 
166
        """
 
167
        if edge == LEFT:
 
168
            self.topleft[0] = other_box.left() + margin
 
169
        elif edge == RIGHT:
 
170
            self.topleft[0] = other_box.right() - margin
 
171
        elif edge == TOP:
 
172
            self.topleft[1] = other_box.top() + margin
 
173
        elif edge == BOTTOM:
 
174
            self.topleft[1] = other_box.bottom() - margin
 
175
        else:
 
176
            raise ValueError, "edge should only be plt.xxx where xxx is" \
 
177
                              " LEFT,RIGHT,TOP, or BOTTOM"
 
178
    def radial_offset_from(self,other_box,angle,margin=0):
 
179
        """ Move this box so that its center is aligned along a radial ray
 
180
            out of other_box's center at the specified angle (in radians),
 
181
            and its edge touches the appropriate edge of other_box.
 
182
            Optionally, margin specifies the size of the gap (along the ray)
 
183
            between the two boxes.
 
184
        """
 
185
        dist = other_box.radial(angle) + margin + self.radial(angle)
 
186
        new_center = other_box.center() + rotate((dist,0),(0,0),angle)
 
187
        self.center_on(point_object(new_center.astype(TYPE)))
 
188
 
 
189
    #------------------ Trim the edges of the box ----------------#
 
190
    def trim_top(self,amount,margin=0):
 
191
        """ Remove amount from the top edge of the box.  The size and
 
192
            topleft corner of the box are altered appropriately.  Margin
 
193
            specifies an additional amount to be removed.
 
194
        """
 
195
        self.topleft[1] = self.topleft[1] + amount
 
196
        self.set_height( self.height() - amount - margin)
 
197
    def trim_left(self,amount,margin=0):
 
198
        """ Remove amount from the left edge of the box.  The size and
 
199
            topleft corner of the box are altered appropriately.  Margin
 
200
            specifies an additional amount to be removed.
 
201
        """
 
202
        self.topleft[0] = self.topleft[0] + amount
 
203
        self.set_width( self.width() - amount - margin)
 
204
    def trim_right(self,amount,margin=0):
 
205
        """ Remove amount from the right edge of the box.  The size and
 
206
            is altered appropriately.  Margin specifies an additional amount
 
207
            to be removed.
 
208
        """
 
209
        self.set_width(self.width() - amount - margin)
 
210
    def trim_bottom(self,amount,margin=0):
 
211
        """ Remove amount from the bottom edge of the box.  The size
 
212
            is altered appropriately.  Margin specifies an additional
 
213
            amount to be removed.
 
214
        """
 
215
        self.set_height(self.height() - amount - margin)
 
216
    def trim_all(self,amount,margin=0):
 
217
        """ Remove amount from the all edges of the box.  The size and
 
218
            topleft corner of the bow are altered appropriately.  Margin
 
219
            specifies an additional amount to be removed.
 
220
        """
 
221
        self.trim_top(amount,margin)
 
222
        self.trim_bottom(amount,margin)
 
223
        self.trim_left(amount,margin)
 
224
        self.trim_bottom(amount,margin)
 
225
 
 
226
    def inflate(self,percent):
 
227
        """ Expand the box in all directions by the specified percentage.
 
228
            Values < 1 shrink the box.  The center of the box remains the same.
 
229
        """
 
230
        old_size = self.size()
 
231
        inflated = old_size * percent
 
232
        shift = .5 * (old_size - inflated)
 
233
        self.topleft = floor(self.topleft + shift).astype(TYPE)
 
234
        self.set_size(floor(inflated).astype(TYPE))
 
235
 
 
236
    #------------------ test point relationships ----------------#
 
237
    def contains(self,pos):
 
238
        l,r,t,b = self.left(),self.right(),self.top(),self.bottom()
 
239
        #print self.text, self.size(), l,r,t,b
 
240
        #print pos[0],pos[1]
 
241
        if ((l <= pos[0] <= r) and (t <= pos[1] <= b)):
 
242
            return 1
 
243
        return 0
 
244
 
 
245
class point_object(box_object):
 
246
    """ Useful for laying points (and boxes) in relation to one another.
 
247
 
 
248
        point_object is a degenerate case of a box_object that has
 
249
        zero size.  The set_size() method is not allowed for points. As
 
250
        a result, size altering methods such as set_height and trimming
 
251
        functions are not valid.  Also, the radial() method always returns
 
252
        zero.  Other than that, the standard box_object methods work.
 
253
        point_objects can also be used as arguments to box_object methods.
 
254
    """
 
255
    def __init__(self,pt):
 
256
        box_object.__init__(self,pt,(0,0))
 
257
    def radial(self,angle):
 
258
        return 0
 
259
    def set_size(self,sz):
 
260
        # this'll catch all set_size, trim_xxx and other inappropriate methods
 
261
        raise TypeError, "can't set the size of a point_object"
 
262
 
 
263
def bounding_points(objects):
 
264
    l = min(map(lambda x: x.left(),objects))
 
265
    t = min(map(lambda x: x.top(),objects))
 
266
    r = min(map(lambda x: x.right(),objects))
 
267
    b = min(map(lambda x: x.bottom(),objects))
 
268
    return (l,t),(r,b)
 
269
 
 
270
def bounding_box(objects):
 
271
    l = min(map(lambda x: x.left(),objects))
 
272
    t = min(map(lambda x: x.top(),objects))
 
273
    r = min(map(lambda x: x.right(),objects))
 
274
    b = min(map(lambda x: x.bottom(),objects))
 
275
    return box_object((l,t),(r-l,b-t))
 
276
 
 
277
#----------------------------------------------------------
 
278
#               Simple Property class
 
279
#----------------------------------------------------------
 
280
 
 
281
class property_object:
 
282
    """ Base class for graph object with properties like 'color', 'font', etc.
 
283
 
 
284
        Many object have a standard set of properties that describe the
 
285
        object.  Text objects, for example, have a color, font, and style.
 
286
        These attirbutes often have default values and perhaps only a range
 
287
        of acceptable values.  Every class derived from this one must have
 
288
        the variable "_attributes" defined at the class level.  The keys of
 
289
        this dictionary are the names of the class attributes.  The value
 
290
        for each key is a list with three entries.  The first is the default
 
291
        value for the attribute, the second is a list of acceptable values
 
292
        and the third is a short text description of the attribute.  FOr
 
293
        example:
 
294
          class text_object(property_class):
 
295
            _attributes = { 'color': ['black',['black','red','blue'],
 
296
                                      "The color of the text" ],
 
297
                            'style': ['normal',['normal','bold'],
 
298
                                      "The style of the text" ],}
 
299
 
 
300
        Currently only the first entry is used.  The second is rather limited
 
301
        in functionality, but might be useful for automatically generating
 
302
        dialog boxes.
 
303
 
 
304
        Graphite has a more flexible property system, but it is somewhat
 
305
        complex.  Zope also has a nice property structure.  If we run into
 
306
        major limitations with this 15 line implementation, we might look
 
307
        here for inspiration (or adoption...).
 
308
 
 
309
        There is no type safety enforced here which could cause so problems
 
310
        for unsuspecting users.  Maybe the optional typing of future Python
 
311
        versions will mitigate this.
 
312
    """
 
313
    def __init__(self, kw, **attr):
 
314
        attrs = {};attrs.update(kw);
 
315
        if attr: attrs.update(attr)
 
316
        for name, value in self._attributes.items():
 
317
            val = value[0]
 
318
            try:
 
319
                val = attrs[name]
 
320
            except KeyError: pass
 
321
            self.__dict__[name] = val
 
322
    def reset_default(self):
 
323
        """ Reset the objects attributes to their default values.
 
324
        """
 
325
        for name, value in self._attributes.items():
 
326
            self.__dict__[name] = value[0]
 
327
 
 
328
    def clone_properties(self,other):
 
329
        """ Reset the objects attributes to their default values.
 
330
        """
 
331
        for name in other._attributes.keys():
 
332
            self.__dict__[name] = other.__dict__[name]
 
333
 
 
334
#----------------------------------------------------------#
 
335
#-------------- Axis and Tick mark utilities --------------#
 
336
#----------------------------------------------------------#
 
337
 
 
338
def rotate(pts,center,angle):
 
339
    """ pts is a Nx2 array of N (x,y) point pairs.  The points
 
340
        are rotated counter-clockwise around a center (x,y) point
 
341
        the specified angle (in radians).
 
342
    """
 
343
    t_pts = asarray(pts) - asarray(center)
 
344
    transform = array( ((cos(angle),-sin(angle)),
 
345
                        (sin(angle), cos(angle))) )
 
346
    rotated = transpose(dot(transform, transpose(t_pts)))
 
347
    t_pts = around(rotated + center)
 
348
    return t_pts.astype(TYPE)
 
349
 
 
350
def log2(num):
 
351
    """ Log base 2 of a number ( or array)
 
352
 
 
353
        !! 1e-16 is here to prevent errors when log is 0
 
354
    """
 
355
    if is_number(num) and num == 0:
 
356
        num = num + 1e-16
 
357
    elif type(num) == type(array([])):
 
358
        putmask(num,equal(num,0.),1e-16)
 
359
    return log10(num)/log10(2)
 
360
 
 
361
def is_base2(range):
 
362
    " True if value is base 2 (2, 4, 8, 16, ...)"
 
363
    l = log2(range)
 
364
    return (l == floor(l) and l > 0.0)
 
365
 
 
366
def is_number(val):
 
367
    " Returns 1 if value is an integer or double, 0 otherwise."
 
368
    return (type(val) in [type(0.0),type(0)])
 
369
 
 
370
default_bounds = ['auto','auto','auto']
 
371
def auto_ticks(data_bounds, bounds_info = default_bounds):
 
372
    """ Find locations for axis tick marks.
 
373
 
 
374
        Calculate the location for tick marks on an axis. data_bounds is a
 
375
        sequence of 2 numbers specifying the maximum and minimum values of
 
376
        the data along this axis. bounds_info is a sequence of 3 values that
 
377
        specify how the axis end points and tick interval are calculated. An
 
378
        array of tick mark locations is returned from the function.  The
 
379
        first and last tick entries are the axis end points.
 
380
 
 
381
        data_bounds -- (lower,upper).  The maximum and minimum values of the
 
382
                       data long this axis.  If any of the settings in
 
383
                       bounds_info are 'auto' or 'fit', the axis properties
 
384
                       are calculated automatically from these settings.
 
385
        bounds_info -- (lower,upper,interval).  Each entry can either be
 
386
                       a numerical value or a string.  If a number,the axis
 
387
                       property is set to that value.  If the entry
 
388
                       is 'auto', the property is calculated automatically.
 
389
                       lower and upper can also be 'fit' in which case
 
390
                       the axis end points are set equal to the values
 
391
                       in data_bounds.
 
392
    """
 
393
    # pretty ugly code...
 
394
    # man, this needs some testing.
 
395
 
 
396
    # hmmm, all the nan stuff should have been handled before this
 
397
    # point...
 
398
    #from misc import nan_to_num, isinf
 
399
    #if is_number(bounds_info[0]): lower = nan_to_num(bounds_info[0])
 
400
    #else:                         lower = nan_to_num(data_bounds[0])
 
401
    #if is_number(bounds_info[1]): upper = nan_to_num(bounds_info[1])
 
402
    #else:                         upper = nan_to_num(data_bounds[1])
 
403
    #if is_number(bounds_info[2]): interval = nan_to_num(bounds_info[2])
 
404
    #else:                         interval = bounds_info[2]
 
405
 
 
406
    if is_number(bounds_info[0]): lower = bounds_info[0]
 
407
    else:                         lower = data_bounds[0]
 
408
    if is_number(bounds_info[1]): upper = bounds_info[1]
 
409
    else:                         upper = data_bounds[1]
 
410
    interval = bounds_info[2]
 
411
 
 
412
    #print 'raw input:', lower,upper,interval
 
413
    #print 'raw interval:', interval
 
414
    if interval in ['linear','auto']:
 
415
        rng = abs(upper - lower)
 
416
        if rng == 0.:
 
417
        # anything more intelligent to do here?
 
418
            interval = .5
 
419
            lower,upper = data_bounds + array((-.5,.5))
 
420
        if is_base2(rng) and is_base2(upper) and rng > 4:
 
421
            if rng == 2:
 
422
                interval = 1
 
423
            elif rng == 4:
 
424
                interval = 4
 
425
            else:
 
426
                interval = rng / 4 # maybe we want it 8
 
427
        else:
 
428
            interval = auto_interval((lower,upper))
 
429
    elif type(interval) in [type(0.0),type(0)]:
 
430
        pass
 
431
    else:
 
432
        #print 'interval: ', interval
 
433
        raise ValueError, interval + " is an unknown value for interval: " \
 
434
                          "  expects 'auto' or 'linear', or a number"
 
435
 
 
436
    # If the lower or upper bound are set to 'auto',
 
437
    # calculate them based on the newly chosen interval.
 
438
    #print 'interval:', interval
 
439
    auto_lower,auto_upper = auto_bounds(data_bounds,interval)
 
440
    if bounds_info[0] == 'auto':
 
441
        lower = auto_lower
 
442
    if bounds_info[1] == 'auto':
 
443
        upper = auto_upper
 
444
 
 
445
    # again, we shouldn't need this
 
446
    # cluge to handle inf values
 
447
    #if isinf(lower):
 
448
    #    lower = nan_to_num(lower) / 10
 
449
    #if isinf(upper):
 
450
    #    upper = nan_to_num(upper) / 10
 
451
    #if isinf(interval):
 
452
    #    interval = nan_to_num(interval) / 10
 
453
        # if the lower and upper bound span 0, make sure ticks
 
454
    # will hit exactly on zero.
 
455
 
 
456
    if lower < 0 and upper > 0:
 
457
        #print 'arrrgh',0,upper+interval,interval
 
458
        hi_ticks = arange(0,upper+interval,interval)
 
459
        low_ticks = - arange(interval,-lower+interval,interval)
 
460
        ticks = concatenate((low_ticks[::-1],hi_ticks))
 
461
    else:
 
462
        # othersize the ticks start and end on the lower and
 
463
        # upper values.
 
464
        ticks = arange(lower,upper+interval,interval)
 
465
    #cluge
 
466
    if len(ticks) < 2:
 
467
        ticks = array(((lower-lower*1e-7),lower))
 
468
    #print 'ticks:',ticks
 
469
    if bounds_info[0] == 'fit': ticks[0] = lower
 
470
    if bounds_info[1] == 'fit': ticks[-1] = upper
 
471
    return ticks
 
472
 
 
473
 
 
474
def format_tick_labels(ticks):
 
475
    """ Convert tick values to formatted strings.
 
476
        Definitely needs some work
 
477
    """
 
478
    return map(str,ticks)
 
479
    #print ticks
 
480
    #if equal(ticks,ticks.astype('l')):
 
481
    #    return map(str,ticks.astype('l'))
 
482
    #else:
 
483
    #    return map(str,ticks)
 
484
 
 
485
 
 
486
 
 
487
def calc_bound(end_point,interval,end):
 
488
    """ Find an axis end point that includes the the value end_point.  If the
 
489
        tick mark interval results in a tick mark hitting directly on the
 
490
        end_point, end_point is returned.  Otherwise, the location of the tick
 
491
        mark just past the end_point is returned. end is 'lower' or 'upper' to
 
492
        specify whether end_point is at the lower or upper end of the axis.
 
493
    """
 
494
    quotient,remainder = divmod(end_point,interval)
 
495
    if not remainder: return end_point
 
496
 
 
497
    c1 = axis_bound = (quotient + 1) * interval
 
498
    c2 = axis_bound = (quotient) * interval
 
499
    if end == 'upper': return max(c1,c2)
 
500
    if end == 'lower': return min(c1,c2)
 
501
 
 
502
 
 
503
def auto_bounds(data_bounds,interval):
 
504
    """ Calculate an appropriate upper and lower bounds for the axis from
 
505
        the the data_bounds (lower, upper) and the given axis interval.  The
 
506
        boundaries will either hit exactly on the lower and upper values
 
507
        or on the tick mark just beyond the lower and upper values.
 
508
    """
 
509
    data_lower,data_upper = data_bounds
 
510
    lower = calc_bound(data_lower,interval,'lower')
 
511
    upper = calc_bound(data_upper,interval,'upper')
 
512
    return array((lower,upper))
 
513
 
 
514
def auto_interval(data_bounds):
 
515
    """ Calculate the tick intervals for a graph axis.
 
516
 
 
517
        Description:
 
518
        The boundaries for the data to be plotted on the axis are:
 
519
            data_bounds = (lower,upper)
 
520
 
 
521
        A choice is made between 3 to 9 ticks marks (including end points)
 
522
        and tick intervals at 1, 2, 2.5, 5, 10, 20, ...
 
523
 
 
524
        Returns:
 
525
        interval -- float. tick mark interval for axis
 
526
    """
 
527
    range = float(data_bounds[1]) - float(data_bounds[0])
 
528
    # We'll choose from between 2 and 8 tick marks
 
529
    # Favortism is given to more ticks:
 
530
    #   Note reverse order and see CLUGE below...
 
531
    divisions = arange(8,2.,-1.) #(7,6,...,3)
 
532
    # Calculate the intervals for the divisions
 
533
    candidate_intervals = range / divisions
 
534
    # Get magnitudes and mantissas for each candidate
 
535
    magnitudes = 10.**floor(log10(candidate_intervals))
 
536
    mantissas = candidate_intervals / magnitudes
 
537
 
 
538
    # list of "pleasing" intervals between ticks on graph
 
539
    # only first magnitude listed, higher mags others inferred
 
540
    magic_intervals = array((1.,2.,2.5,5.,10.))
 
541
    # calculate the absolute differences between the candidates
 
542
    # (with mag removed) and the magic intervals
 
543
    differences = abs(magic_intervals[:,newaxis] - mantissas)
 
544
 
 
545
    # Find the division and magic interval combo
 
546
    # that produce the smallest differences.
 
547
    # also:
 
548
    # CLUGE: argsort doesn't preserve the order of
 
549
    # equal values, so we subtract a small , index
 
550
    # dependent amount from each difference to
 
551
    # force correct ordering
 
552
    sh = shape(differences)
 
553
    small = 2.2e-16 *arange(sh[1]) * arange(sh[0])[:,newaxis]
 
554
    small = small[::-1,::-1] #reverse order
 
555
    differences = differences - small
 
556
    # ? Numeric should allow keyword "axis" ? comment out for now
 
557
    #best_mantissa = minimum.reduce(differences,axis=0)
 
558
    #best_magic = minimum.reduce(differences,axis=-1)
 
559
    best_mantissa = minimum.reduce(differences,0)
 
560
    best_magic = minimum.reduce(differences,-1)
 
561
    magic_index = argsort(best_magic)[0]
 
562
    mantissa_index = argsort(best_mantissa)[0]
 
563
    # The best interval is the magic_interval
 
564
    # multiplied by the magnitude of the best mantissa
 
565
    interval = magic_intervals[magic_index]
 
566
    magnitude = magnitudes[mantissa_index]
 
567
    #print differences
 
568
    #print 'results:', magic_index, mantissa_index,interval, magnitude
 
569
    #print 'returned:',interval*magnitude
 
570
    result = interval*magnitude
 
571
    if result == 0.0:
 
572
        result = limits.float_epsilon
 
573
    return result