2
This module contains the newfangled transform class which allows the
3
placement of artists (lines, patches, text) in a variety of coordinate
4
systems (display, arbitrary data, relative axes, physical sizes)
6
The default Transform() is identity.
9
x == Transform.positions(x) True
10
x == Transform.scale(x) True
12
A linear Transformation is specified by giving a Bound1D (min, max)
13
instance for the domain and range. The transform below maps the
14
interval [0,1] to [-10,10]
16
t = Transform( Bound1D(0,1), Bound1D(-10,10) )
18
Since all Transforms know their inverse function, you can compute an
19
inverse transformation by calling inverse_positions or inverse_scale
21
t = Transform( Bound1D(0,1), Bound1D(-10,10) )
22
val = t.inverse_positions(5) # maps [-10,10] to [0,1]
24
The difference between 'positions' and 'scale' is that the positions
25
func is appropriate for locations (eg, x,y) and the scale func is
26
appropriate for lengths (eg, width, height).
28
The Bound1D methods provide a number of utility functions: the
29
interval max-min, determining if a point is in the open or closed
30
interval, constraining the bound to be positive (useful for log
31
transforms), and so on. These are useful for storing view, data and
32
display limits of a given axis.
34
The Bound2D is a straight-forward generalization of Bound1D, and
35
stores 2 Bound1D instances 'x' and 'y' to represent a 2D Bound (useful
36
for Axes bounding boxes, clipping etc). All Artists are responsible
37
for returning their extent in display coords as a Bound2D instance,
38
which is useful for determining whether 2 Artists overlap. Some
39
utility functions, eg, bound2d_all, return the Bound2D instance that
40
bounds all the Bound2D instances passed as args. This helps in text
41
layout, eg, in positioning the axis labels to not overlap the tick
44
The Bound1D instances store their max and min values as RWVals
45
(read/write references). These are mutable scalars that can be shared
46
among all the figure components. When a figure clas resizes and thus
47
changes the display limits of an Axes, the Axes and all its components
48
know about the changes because they store a reference to the
49
displaylim, not the scalar value of the display lim. Likewise for
52
Also, it is possible to do simple arithmetic in RRefs via the derived
53
BinOp class, which stores both sides of a binary arithmetic operation,
54
as well as the binary function to return the result of the binop
55
applied to the dereferenced scalars. This allows you to place artists
56
with locations like '3 centimenters below the x axis'
58
Here are some concepts and how to apply them via the transform
61
* Map view limits to display limits via a linear tranformation
63
# viewlim and displaylim are Bound1D instances
64
tx = Transform( axes.xaxis.viewlim, axes.xaxis.displaylim )
65
ty = Transform( axes.yaxis.viewlim, axes.yaxis.displaylim )
66
l = Line2D(dpi, bbox, xdata, ydata, transx=tx, transy=ty)
68
* Map relative axes coords ( 0,0 is lower left and 1,1 is upper
69
right ) to display coords. This example puts text in the middle
70
of the axes (0.5, 0.5)
72
tx = Transform( Bound1D(0,1), axes.xaxis.displaylim )
73
ty = Transform( Bound1D(0,1), axes.yaxis.displaylim )
74
text = AxisText(dpi, bbox, 0.5, 0.5, transx=tx, transy=ty)
76
* Map x view limits to display limits via a log tranformation and y
77
view limits via linear transform. The funcs pair is the
78
transform/inverse pair
80
funcs = logwarn, pow10
81
tx = Transform( axes.xaxis.viewlim, axes.xaxis.displaylim, funcs )
82
ty = Transform( axes.yaxis.viewlim, axes.yaxis.displaylim )
83
l = Line2D(dpi, bbox, xdata, ydata, transx=tx, transy=ty)
85
* You can also do transformation from one physical scale (inches, cm,
86
points, ...) to another. You need to specify an offset in output
90
cm = Centimeter( self.dpi)
91
dots = Dots( self.dpi)
92
t = TransformSize(cm, dots, offset)
94
If you don't know the offset in output coords, you can supply an
95
optional transform to transform the offset to output coords. Eg,
96
if you want to offset by x in data coords, and the output is
97
display coords, you can do
99
offset = 0.2 # x data coords
100
cm = Centimeter( self.dpi)
101
dots = Dots( self.dpi)
102
t = TransformSize(cm, dots, offset, axes.xaxis.transData)
104
* Combining the above, we can specify that a text instance is at an x
105
location in data coords and a y location in points relative to an
106
axis. Eg. the transformation below indicates that the x value of
107
an xticklabel position is in data corrdinates and the y value is 3
108
points below the x axis, top justified
110
# the top of the x ticklabel text is the bottom of the y axis
111
# minus 3 points. Note the code below uses the overloading of
112
# __sub__ and __mul__ to return a BinOp. Changes in the
113
# position of bbox.y or dpi, eg, on a resize event, are
114
# automagically reflected in the tick label position.
115
# dpi*3/72 converts 3 points to dots.
117
top = self.bbox.y.get_refmin() - self.dpi*RRef(3/72.0)
118
text = backends.AxisText(dpi, bbox, x=xdata, y=top,
119
verticalalignment='top',
120
horizontalalignment='center',
121
transx = self.axes.xaxis.transData,
122
# transy is default, identity transform)
124
The unittest code for the transforms module is unit/transforms_unit.py
128
from __future__ import division
130
from Numeric import array, asarray, log10, take, nonzero, arange
131
from cbook import iterable
136
def __init__(self, dpi, val=1):
144
return self._dpi.get() * self.to_inches()
147
return self.to_inches()
150
def __repr__(self): return '%s %s ' % (self._val, self.units())
152
def set_val(self, val):
156
raise NotImplementedError('Derived must override')
159
raise NotImplementedError('Derived must override')
170
return self._val/self._dpi.get()
180
return self._val/72.0
185
class Millimeter(Size):
187
return self._val/25.4
192
class Centimeter(Size):
194
return self._val/2.54
200
def bintimes(x,y): return x*y
201
def binadd(x,y): return x+y
202
def binsub(x,y): return x-y
207
def __init__(self, val):
213
def __add__(self, other):
214
return BinOp(self, other, binadd)
216
def __mul__(self, other):
217
return BinOp(self, other, bintimes)
219
def __sub__(self, other):
220
return BinOp(self, other, binsub)
223
'A read only ref that handles binary ops of refs'
224
def __init__(self, ref1, ref2, func=binadd):
230
return self.func(self.ref1.get(), self.ref2.get())
233
'A readable and writable ref'
241
Store and update information about a 1D bound
243
def __init__(self, minval=None, maxval=None, isPos=False):
244
self._min = RWRef(minval)
245
self._max = RWRef(maxval)
247
if self.defined(): assert(self._max.get()>=self._min.get())
250
'Return the min, max of the bounds'
251
return self._min.get(), self._max.get()
254
'return true if both endpoints defined'
255
return self._min.get() is not None and self._max.get() is not None
257
def get_refmin(self):
260
def get_refmax(self):
263
def in_interval(self, val):
264
'Return true if val is in [min,max]'
265
if not self.defined(): return False
266
smin, smax = self.bounds()
267
if val>=smin and val<=smax: return True
270
def in_open_interval(self, val):
271
'Return true if val is in (min,max)'
272
if not self.defined(): return False
273
smin, smax = self.bounds()
274
if val>smin and val<smax: return True
278
'return max - min if defined, else None'
279
if not self.defined(): return None
280
smin, smax = self.bounds()
284
'return the max of the bounds'
285
return self._max.get()
288
'return the min of the bounds'
289
return self._min.get()
291
def overlap(self, bound):
293
Return true if bound overlaps with self.
295
Return False if either bound undefined
297
if not self.defined() or not bound.defined():
299
smin, smax = self.bounds()
300
bmin, bmax = bound.bounds()
301
return ( ( smin >= bmin and smin <= bmax) or
302
( bmin >= smin and bmin <= smax ) )
306
smin, smax = self.bounds()
307
return 'Bound1D: %s %s' % (smin, smax)
310
def shift(self, val):
311
'Shift min and max by val and return a reference to self'
312
smin, smax = self.bounds()
313
if self._min.get() is not None:
316
if self._max.get() is not None:
322
def set_bounds(self, vmin, vmax):
323
'set the min and max to vmin, vmax'
327
def set_min(self, vmin):
328
if self._isPos and vmin<=0: return
331
def set_max(self, vmax):
332
'set the max to vmax'
333
if self._isPos and vmax<=0: return
338
'scale the min and max by ratio s and return a reference to self'
339
if self._isPos and s<=0: return
340
if not self.defined(): return
342
delta = (s*i-i)/2 # todo; correct for s<1
344
vmin, vmax = self.bounds()
345
self._min.set(vmin - delta)
346
self._max.set(vmax + delta)
351
Update the min and max with values in x. Eg, only update min
352
if min(x)<self.min(). Return a reference to self
354
if iterable(x) and len(x)==0: return
357
if self._isPos and x<=0: return
360
if self._isPos: x = take(x, nonzero(x>0))
361
if not len(x): return
362
minx, maxx = min(x), max(x)
365
if self._max.get() is None: self._max.set(maxx)
366
else: self._max.set(max(self._max.get(), maxx))
368
if self._min.get() is None: self._min.set(minx)
369
else: self._min.set( min(self._min.get(), minx))
372
def is_positive(self, b):
374
If true, bound will only return positive endpoints.
378
if self._min.get()<=0: self._min.set(None)
379
if self._max.get()<=0: self._max.set( None)
382
def bound1d_all(bounds):
384
Return a Bound1D instance that bounds all the Bound1D instances in
387
If the min or max val for any of the bounds is None, the
388
respective value for the returned bbox will also be None
391
if not len(bounds): return Bound1D(None, None)
392
if len(bounds)==1: return bounds[0]
394
# min with a sequence with None is None
395
minval = min([b.min() for b in bounds])
397
# max with a sequence with None is not None, so we use a different
399
maxvals = [b.max() for b in bounds]
400
if None in maxvals: maxval = None
401
else: maxval = max(maxvals)
402
return Bound1D(minval, maxval)
406
Store and update 2D bounding box information
408
Publicly accessible attributes
410
x the x Bound1D instance
411
y the y Bound2D instance
414
def __init__(self, left, bottom, width, height):
417
self.set_bounds(left, bottom, width, height)
420
return 'Bound2D: %s\n\tx: %s\n\ty: %s' % \
421
(list(self.get_bounds()), self.x, self.y)
424
'Return a deep copy of self'
425
return Bound2D(*self.get_bounds())
428
return self.x.defined() and self.y.defined()
430
def set_bounds(self, left, bottom, width, height):
432
assert(left is not None)
433
assert(bottom is not None)
434
assert(width is not None)
435
assert(height is not None)
442
maxy = bottom + height
443
self.x.set_bounds(minx, maxx)
444
self.y.set_bounds(miny, maxy)
446
def get_bounds(self):
448
bottom = self.y.min()
449
width = self.x.interval()
450
height = self.y.interval()
451
return left, bottom, width, height
453
def overlap(self, bound):
455
Return true if bound overlaps with self.
457
Return False if either bound undefined
459
if not self.defined() or not bound.defined():
461
return self.x.overlap(bound.x) and self.y.overlap(bound.y)
463
def bound2d_all(bounds):
465
Return a Bound2D instance that bounds all the Bound2D instances in
468
If the min or max val for any of the bounds is None, the
469
respective value for the returned bbox will also be None
472
bx = bound1d_all([b.x for b in bounds])
473
by = bound1d_all([b.y for b in bounds])
477
width = bx.interval()
478
height = by.interval()
479
return Bound2D(left, bottom, width, height)
481
def iterable_to_array(x):
482
if iterable(x): return asarray(x)
486
'The identity function'
488
except AttributeError: return x
492
'Return log10 for positive x'
496
print >>sys.stderr, 'log scalar warning, non-positive data ignored'
497
return 0 #todo: JDH: what should we return here? None?
503
print >>sys.stderr, 'log iterable warning, non-positive data ignored'
508
'the inverse of log10; 10**x'
509
return 10**asarray(x)
513
Abstract base class for transforming data
515
Publicly accessible attributes are
516
func : the transform func
517
ifunc : the inverse tranform func
519
A tranform from in->out is defined by
521
scale = (maxout-maxin)/( func(maxin)-func(minin) )
522
out = scale * ( func(in)-func(minin) ) + minout
524
funcs are paired with inverses, allowing Transforms to return
529
def __init__(self, boundin=Bound1D(0,1), boundout=Bound1D(0,1),
530
funcs = (identity, identity),
533
The default transform is identity.
535
To do a linear transform, replace the bounds with the
536
coodinate bounds of the input and output spaces
538
To do a log transform, use funcs=(log10, pow10)
541
self._boundin = boundin
542
self._boundout = boundout
543
self.set_funcs(funcs)
546
def inverse_positions(self, x):
547
'Return the inverse transform of x'
548
x = iterable_to_array(x)
549
minin, maxin = self._boundin.bounds()
550
if self.func != identity:
551
minin, maxin = self.func(minin), self.func(maxin)
552
scale = (maxin-minin)/self._boundout.interval()
553
return self.ifunc(scale*(x-self._boundout.min()) + minin)
555
def inverse_scale(self, x):
556
'Return the inverse transform of scale x'
557
x = iterable_to_array(x)
558
minin, maxin = self._boundin.bounds()
559
if self.func != identity:
560
minin, maxin = self.func(minin), self.func(maxin)
561
scale = (maxin-minin)/self._boundout.interval()
562
return self.ifunc(scale*x)
565
def positions(self, x):
566
'Transform the positions in x.'
567
if not self._boundin.defined() or not self._boundout.defined():
568
raise RuntimeError('You must first define the boundaries of the transform')
569
x = iterable_to_array(x)
570
minin, maxin = self._boundin.bounds()
571
if self.func != identity:
572
minin, maxin = self.func(minin), self.func(maxin)
573
scale = self._boundout.interval()/(maxin-minin)
574
return scale*(self.func(x)-minin) + self._boundout.min()
577
'Transform the scale in s'
578
if not self._boundin.defined() or not self._boundout.defined():
579
raise RuntimeError('You must first define the boundaries of the transform')
580
s = iterable_to_array(s)
581
minin, maxin = self._boundin.bounds()
582
if self.func != identity:
583
minin, maxin = self.func(minin), self.func(maxin)
584
scale = self._boundout.interval()/(maxin-minin)
585
return scale*self.func(s)
588
return 'Transform: %s to %s' %(self._boundin, self._boundout)
590
def set_funcs(self, funcs):
591
'Set the func, ifunc to funcs'
592
self.func, self.ifunc = funcs
596
def __init__(self, sin, sout, offset, transOffset=Transform()):
598
transform size in Size instance sin to Size instance sout,
599
offsetting by Bound1D instance RRef instance offset.
600
transOffset is used to transform the offset if not None
604
self._offset = offset
605
self._transOffset = transOffset
607
def positions(self, x):
608
offset = self._get_offset()
609
return offset + self._sin.to_inches()/self._sout.to_inches()*x
611
def inverse_positions(self, x):
612
offset = self._get_offset()
613
return (x - offset)*self._sout.to_inches()/self._sin.to_inches()
615
def inverse_scale(self, x):
616
return self._sout.to_inches()/self._sin.to_inches()*x
619
return self._sin.to_inches()/self._sout.to_inches()*x
621
def _get_offset(self):
622
try: o = self._offset.get()
623
except AttributeError: o = self._offset
624
return self._transOffset.positions(o)
628
def transform_bound1d(bound, trans):
630
Transform a Bound1D instance using transforms trans
633
tmin, tmax = trans.positions(bound.bounds())
634
return Bound1D(tmin, tmax)
636
def inverse_transform_bound1d(bound, trans):
638
Inverse transform a Bound1D instance using trans.inverse()
641
tmin, tmax = trans.inverse_positions(bound.bounds())
642
return Bound1D(tmin, tmax)
645
def transform_bound2d(bbox, transx, transy):
647
Transform a Bound2D instance using transforms transx, transy
649
b1 = transform_bound1d(bbox.x, transx)
650
b2 = transform_bound1d(bbox.y, transy)
655
return Bound2D(l,b,w,h)
657
def inverse_transform_bound2d(bbox, transx, transy):
659
Inverse transform a Bound2D instance using transforms transx, transy
661
b1 = inverse_transform_bound1d(bbox.x, transx)
662
b2 = inverse_transform_bound1d(bbox.y, transy)
667
return Bound2D(l,b,w,h)