From: <md...@us...> - 2007-09-12 13:36:31
|
Revision: 3835 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=3835&view=rev Author: mdboom Date: 2007-09-12 06:36:25 -0700 (Wed, 12 Sep 2007) Log Message: ----------- Second pass, using a stateful transform tree. Modified Paths: -------------- branches/transforms/lib/matplotlib/affine.py branches/transforms/lib/matplotlib/artist.py branches/transforms/lib/matplotlib/axes.py branches/transforms/lib/matplotlib/axis.py branches/transforms/lib/matplotlib/backends/backend_agg.py branches/transforms/lib/matplotlib/backends/backend_tkagg.py branches/transforms/lib/matplotlib/figure.py branches/transforms/lib/matplotlib/legend.py branches/transforms/lib/matplotlib/lines.py branches/transforms/lib/matplotlib/table.py branches/transforms/lib/matplotlib/text.py branches/transforms/lib/matplotlib/ticker.py Removed Paths: ------------- branches/transforms/lib/matplotlib/bbox.py Modified: branches/transforms/lib/matplotlib/affine.py =================================================================== --- branches/transforms/lib/matplotlib/affine.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/affine.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -6,19 +6,203 @@ import numpy as N from numpy.linalg import inv +from sets import Set -class Transform(object): +# MGDTODO: This creates a ton of cyclical references. We may want to +# consider using weak references + +class TransformNode(object): + def __init__(self): + self._parents = Set() + + def invalidate(self): + if not self._do_invalidation(): + for parent in self._parents: + parent.invalidate() + + def _do_invalidation(self): + return False + + def add_children(self, children): + for child in children: + child._parents.add(self) + +class Bbox(TransformNode): + def __init__(self, points): + TransformNode.__init__(self) + self._points = N.asarray(points, N.float_) + self.track = False + + def __del__(self): + if self.track: + print "Bbox::__del__" + + #@staticmethod + def unit(): + return Bbox([[0,0], [1,1]]) + unit = staticmethod(unit) + + #@staticmethod + def from_lbwh(left, bottom, width, height): + return Bbox([[left, bottom], [left + width, bottom + height]]) + from_lbwh = staticmethod(from_lbwh) + + #@staticmethod + def from_lbrt(left, bottom, right, top): + return Bbox([[left, bottom], [right, top]]) + from_lbrt = staticmethod(from_lbrt) + + def update_from_data(self, x, y): + self._points = N.array([[x.min(), y.min()], [x.max(), y.max()]], N.float_) + self.invalidate() + if self.track: + print "Bbox::update_from_data", self._points + + def copy(self): + if self.track: + print "Bbox::copy" + return Bbox(self._points.copy()) + + def __repr__(self): + return 'Bbox(%s)' % repr(self._points) + __str__ = __repr__ + + def __cmp__(self, other): + # MGDTODO: Totally suboptimal + if isinstance(other, Bbox): + return (self._points == other._points).all() + return -1 + + # MGDTODO: Probably a more efficient ways to do this... + def _get_xmin(self): + if self.track: + print "Bbox::_get_xmin" + return self._points[0, 0] + def _set_xmin(self, val): + print "Bbox::_set_xmin" + self._points[0, 0] = val + self.invalidate() + xmin = property(_get_xmin, _set_xmin) + + def _get_ymin(self): + return self._points[0, 1] + def _set_ymin(self, val): + self._points[0, 1] = val + self.invalidate() + ymin = property(_get_ymin, _set_ymin) + + def _get_xmax(self): + return self._points[1, 0] + def _set_xmax(self, val): + self._points[1, 0] = val + self.invalidate() + xmax = property(_get_xmax, _set_xmax) + + def _get_ymax(self): + return self._points[1, 1] + def _set_ymax(self, val): + self._points[1, 1] = val + self.invalidate() + ymax = property(_get_ymax, _set_ymax) + + def _get_min(self): + return self._points[0] + def _set_min(self, val): + self._points[0] = val + self.invalidate() + min = property(_get_min, _set_min) + + def _get_max(self): + return self._points[1] + def _set_max(self, val): + self._points[1] = val + self.invalidate() + max = property(_get_max, _set_max) + + def _get_intervalx(self): + return self._points[:,0] + def _set_intervalx(self, interval): + self._points[:,0] = interval + self.invalidate() + intervalx = property(_get_intervalx, _set_intervalx) + + def _get_intervaly(self): + return self._points[:,1] + def _set_intervaly(self, interval): + self._points[:,1] = interval + self.invalidate() + intervaly = property(_get_intervaly, _set_intervaly) + + def _get_width(self): + return self.xmax - self.xmin + width = property(_get_width) + + def _get_height(self): + return self.ymax - self.ymin + height = property(_get_height) + + def transformed(self, transform): + return Bbox(self.transform(self._points)) + + def inverse_transformed(self, transform): + return Bbox(self.transform.inverted()(self._points)) + + def get_bounds(self): + return (self.xmin, self.ymin, + self.xmax - self.xmin, self.ymax - self.ymin) + + def expanded(self, sw, sh): + width = self.width() + height = self.height() + deltaw = (sw * width - width) / 2.0 + deltah = (sh * height - height) / 2.0 + a = N.array([[-deltaw, -deltah], [deltaw, deltah]]) + return Bbox(self._points + a) + + def contains(self, x, y): + return (x >= self.xmin and x <= self.xmax and + y >= self.ymin and y <= self.ymax) + + #@staticmethod + def union(bboxes): + """ + Return the Bbox that bounds all bboxes + """ + assert(len(bboxes)) + + if len(bboxes) == 1: + return bboxes[0] + + bbox = bboxes[0] + xmin = bbox.xmin + ymin = bbox.ymin + xmax = bbox.xmax + ymax = bbox.ymax + + for bbox in bboxes[1:]: + xmin = min(xmin, bbox.xmin) + ymin = min(ymin, bbox.ymin) + xmax = max(xmax, bbox.xmax) + ymax = max(ymax, bbox.ymax) + + return Bbox.from_lbrt(xmin, ymin, xmax, ymax) + union = staticmethod(union) + +class Transform(TransformNode): + def __init__(self): + TransformNode.__init__(self) + def __call__(self, points): raise NotImplementedError() def __add__(self, other): if isinstance(other, Transform): - return CompositeTransform(self, other) + return composite_transform_factory(self, other) raise TypeError("Can not add Transform to object of type '%s'" % type(other)) def __radd__(self, other): if isinstance(other, Transform): - return CompositeTransform(other, self) + return composite_transform_factory(other, self) raise TypeError("Can not add Transform to object of type '%s'" % type(other)) def has_inverse(self): @@ -30,15 +214,9 @@ def is_separable(self): return False -class CompositeTransform(Transform): - def __init__(self, a, b): - assert a.output_dims == b.input_dims - self.a = a - self.b = b - - def __call__(self, points): - return self.b(self.a(points)) - + def is_affine(self): + return False + class Affine2D(Transform): input_dims = 2 output_dims = 2 @@ -51,15 +229,22 @@ b d f 0 0 1 """ + Transform.__init__(self) if matrix is None: matrix = N.identity(3) else: assert matrix.shape == (3, 3) - self.mtx = matrix + self._mtx = matrix + self._inverted = None def __repr__(self): - return "Affine2D(%s)" % repr(self.mtx) + return "Affine2D(%s)" % repr(self._mtx) __str__ = __repr__ + + def _do_invalidation(self): + result = self._inverted is None + self._inverted = None + return result #@staticmethod def from_values(a, b, c, d, e, f): @@ -67,7 +252,8 @@ from_values = staticmethod(from_values) def to_values(self): - return tuple(self.mtx[:2].swapaxes(0, 1).flatten()) + mtx = self.get_matrix() + return tuple(mtx[:2].swapaxes(0, 1).flatten()) #@staticmethod def matrix_from_values(a, b, c, d, e, f): @@ -78,6 +264,9 @@ return affine matrix_from_values = staticmethod(matrix_from_values) + def get_matrix(self): + return self._mtx + def __call__(self, points): """ Applies the transformation to a set of 2D points and @@ -91,13 +280,14 @@ # to separate the matrix out into the translation and scale components # and apply each separately (which is still sub-optimal) - # This is nicer for now, however, since we can just keep a + # This is easier for now, however, since we can just keep a # regular affine matrix around # MGDTODO: Trap cases where this isn't an array and fix there - points = N.array(points, N.float_) + mtx = self.get_matrix() + points = N.asarray(points, N.float_) new_points = points.swapaxes(0, 1) new_points = N.vstack((new_points, N.ones((1, points.shape[0])))) - result = N.dot(self.mtx, new_points)[:2] + result = N.dot(mtx, new_points)[:2] return result.swapaxes(0, 1) #@staticmethod @@ -105,8 +295,9 @@ return N.dot(b, a) _concat = staticmethod(_concat) + #@staticmethod def concat(a, b): - return Affine2D(Affine2D._concat(a.mtx, b.mtx)) + return Affine2D(Affine2D._concat(a._mtx, b._mtx)) concat = staticmethod(concat) #@staticmethod @@ -114,100 +305,246 @@ return Affine2D(N.identity(3)) identity = staticmethod(identity) - def __add__(self, other): - if isinstance(other, Affine2D): - return Affine2D.concat(self, other) - return Transform.__add__(self, other) - - def __radd__(self, other): - if isinstance(other, Affine2D): - return Affine2D.concat(other, self) - return Transform.__radd__(self, other) - - def rotated(self, theta): + def rotate(self, theta): a = N.cos(theta) b = N.sin(theta) rotate_mtx = self.matrix_from_values(a, b, -b, a, 0, 0) - return Affine2D(self._concat(self.mtx, rotate_mtx)) + self._mtx = self._concat(self._mtx, rotate_mtx) + self.invalidate() + return self - def rotated_deg(self, degrees): - return self.rotated(degrees*N.pi/180.) + def rotate_deg(self, degrees): + return self.rotate(degrees*N.pi/180.) - def translated(self, tx, ty): + def translate(self, tx, ty): translate_mtx = self.matrix_from_values(1., 0., 0., 1., tx, ty) - return Affine2D(self._concat(self.mtx, translate_mtx)) + self._mtx = self._concat(self._mtx, translate_mtx) + self.invalidate() + return self - def scaled(self, sx, sy=None): + def scale(self, sx, sy=None): if sy is None: sy = sx scale_mtx = self.matrix_from_values(sx, 0., 0., sy, 0., 0.) - return Affine2D(self._concat(self.mtx, scale_mtx)) + self._mtx = self._concat(self._mtx, scale_mtx) + self.invalidate() + return self def inverted(self): - # MGDTODO: We may want to optimize by storing the inverse - # of the transform with every transform - return Affine2D(inv(self.mtx)) + if self._inverted is None: + mtx = self.get_matrix() + self._inverted = Affine2D(inv(mtx)) + return self._inverted def is_separable(self): - mtx = self.mtx + mtx = self.get_matrix() return mtx[0, 1] == 0.0 and mtx[1, 0] == 0.0 + def is_affine(self): + return True + class BlendedAffine2D(Affine2D): def __init__(self, x_transform, y_transform): -# assert isinstance(x_transform, Affine2D) -# assert isinstance(y_transform, Affine2D) + assert x_transform.is_affine() + assert y_transform.is_affine() assert x_transform.is_separable() assert y_transform.is_separable() - x_mtx = x_transform.mtx - y_mtx = y_transform.mtx - self.mtx = self.matrix_from_values( - x_mtx[0,0], 0.0, 0.0, y_mtx[1,1], x_mtx[0,2], y_mtx[1,2]) -# This is a placeholder since eventually we may need to handle the -# more general case of two transforms that aren't affines -BlendedTransform = BlendedAffine2D + Transform.__init__(self) + self.add_children([x_transform, y_transform]) + self._x = x_transform + self._y = y_transform + self._mtx = None + self._inverted = None -def blend_xy_sep_transform(x_transform, y_transform): - return BlendedAffine2D(x_transform, y_transform) + def __repr__(self): + return "BlendedAffine2D(%s,%s)" % (self._x, self._y) + __str__ = __repr__ + + def _do_invalidation(self): + if self._mtx is not None: + self._mtx = None + Affine2D._do_invalidation(self) + return False + return True -def get_bbox_transform(boxin, boxout): - x_scale = boxout.width() / boxin.width() - y_scale = boxout.height() / boxin.height() + def _make__mtx(self): + if self._mtx is None: + x_mtx = self._x.get_matrix() + y_mtx = self._y.get_matrix() + self._mtx = N.vstack([x_mtx[0], y_mtx[1], [0.0, 0.0, 1.0]]) +# self._mtx = self.matrix_from_values( +# x_mtx[0,0], 0.0, 0.0, y_mtx[1,1], x_mtx[0,2], y_mtx[1,2]) + print "Blended", x_mtx, y_mtx, self._mtx + + def is_separable(self): + return True + + def get_matrix(self): + self._make__mtx() + return self._mtx - # MGDTODO: Optimize - return Affine2D() \ - .translated(-boxin.xmin(), -boxin.ymin()) \ - .scaled(x_scale, y_scale) \ - .translated(boxout.xmin(), boxout.ymin()) +class BlendedTransform(Transform): + def __init__(self, x_transform, y_transform): + assert x_transform.is_separable() + assert y_transform.is_separable() + + Transform.__init__(self) + self.add_children([x_transform, y_transform]) + self._x = x_transform + self._y = y_transform + + def __call__(self, points): + # MGDTODO: Implement me + pass + +class CompositeAffine2D(Affine2D): + def __init__(self, a, b): + assert a.is_affine() + assert b.is_affine() + + Transform.__init__(self) + self.add_children([a, b]) + self._a = a + self._b = b + self._mtx = None + self._inverted = None + + def __repr__(self): + return "CompositeAffine2D(%s, %s)" % (self._a, self._b) + __str__ = __repr__ + + def _do_invalidation(self): + self._mtx = None + Affine2D._do_invalidation(self) -if __name__ == '__main__': - print Affine2D.from_values(1., 0, 0, 1, 0, 0) - - print "translated", Affine2D.identity().translated(5, 4) - print "rotated", Affine2D.identity().rotated_deg(30) - print "scaled", Affine2D.identity().scaled(5, 4) - - transform = Affine2D.identity().rotated_deg(30).translated(5, 4) + def _make__mtx(self): + if self._mtx is None: + self._mtx = self._concat( + self._b.get_matrix(), + self._a.get_matrix()) - points = N.array([[1, 2], [3, 4], [5, 6]]) + def get_matrix(self): + self._make__mtx() + return self._mtx + +class CompositeTransform(Transform): + def __init__(self, a, b): + assert a.output_dims == b.input_dims - print inv(transform.mtx) + Transform.__init__(self) + self.add_children([a, b]) + self._a = a + self._b = b + + def __call__(self, points): + # MGDTODO: Optimize here by concatenating affines if possible + return self._b(self._a(points)) + +class BboxTransform(Affine2D): + def __init__(self, boxin, boxout): + assert isinstance(boxin, Bbox) + assert isinstance(boxout, Bbox) + + Transform.__init__(self) + self.add_children([boxin, boxout]) + self._boxin = boxin + self._boxout = boxout + self._mtx = None + self._inverted = None + + def __repr__(self): + return "BboxTransform(%s, %s)" % (self._boxin, self._boxout) + __str__ = __repr__ + + def _do_invalidation(self): + if self._mtx is not None: + self._mtx = None + Affine2D._do_invalidation(self) + return False + return True + + def _make__mtx(self): + if self._mtx is None: + boxin = self._boxin + boxout = self._boxout + x_scale = boxout.width / boxin.width + y_scale = boxout.height / boxin.height + + # MGDTODO: Optimize + affine = Affine2D() \ + .translate(-boxin.xmin, -boxin.ymin) \ + .scale(x_scale, y_scale) \ + .translate(boxout.xmin, boxout.ymin) + + self._mtx = affine._mtx + + def __call__(self, points): + self._make__mtx() + return Affine2D.__call__(self, points) + + def inverted(self): + self._make__mtx() + return Affine2D.inverted(self) + + def is_separable(self): + return True + + def get_matrix(self): + self._make__mtx() + return self._mtx - print transform(points) +def blend_xy_sep_transform(x_transform, y_transform): + if x_transform.is_affine() and y_transform.is_affine(): + return BlendedAffine2D(x_transform, y_transform) + return BlendedTransform(x_transform, y_transform) - transform = Affine2D.identity().scaled(5., 1.).translated(10, 0) - print transform - print transform.inverted() +def composite_transform_factory(a, b): + if a.is_affine() and b.is_affine(): + return CompositeAffine2D(a, b) + return CompositeTransform(a, b) - from bbox import Bbox - print "BBOX" - boxin = Bbox([[10, 10], [320, 240]]) - boxout = Bbox([[25, 25], [640, 400]]) - print boxin._points, boxin.xmin(), boxin.ymin(), boxin.xmax(), boxin.ymax() - print boxout._points, boxout.xmin(), boxout.ymin(), boxout.xmax(), boxout.ymax() - trans = get_bbox_transform(boxin, boxout) - print trans - print trans(N.array([[10, 10], [320, 240]])) - print trans([[10, 10]]) +# MGDTODO: There's probably a better place for this +def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): + ''' + Ensure the endpoints of a range are not too close together. + + "too close" means the interval is smaller than 'tiny' times + the maximum absolute value. + + If they are too close, each will be moved by the 'expander'. + If 'increasing' is True and vmin > vmax, they will be swapped, + regardless of whether they are too close. + ''' + swapped = False + if vmax < vmin: + vmin, vmax = vmax, vmin + swapped = True + if vmax - vmin <= max(abs(vmin), abs(vmax)) * tiny: + if vmin==0.0: + vmin = -expander + vmax = expander + else: + vmin -= expander*abs(vmin) + vmax += expander*abs(vmax) + if swapped and not increasing: + vmin, vmax = vmax, vmin + return vmin, vmax + +# MGDTODO: Optimize +def interval_contains(interval, val): + return interval[0] <= val and interval[1] >= val + +def interval_contains_open(interval, val): + return interval[0] < val and interval[1] > val +if __name__ == '__main__': + bbox1 = Bbox([[10., 15.], [20., 25.]]) + bbox2 = Bbox([[30., 35.], [40., 45.]]) + trans = BboxTransform(bbox1, bbox2) + print trans(bbox1._points) + + bbox2.intervalx = 50, 55 + print trans(bbox1._points) + __all__ = ['Transform', 'Affine2D'] Modified: branches/transforms/lib/matplotlib/artist.py =================================================================== --- branches/transforms/lib/matplotlib/artist.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/artist.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -138,7 +138,6 @@ ACCEPTS: a matplotlib.transform transformation instance """ - print "set_transform", t self._transform = t self._transformSet = True self.pchanged() Modified: branches/transforms/lib/matplotlib/axes.py =================================================================== --- branches/transforms/lib/matplotlib/axes.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/axes.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -12,7 +12,6 @@ from matplotlib import affine as maffine from matplotlib import agg from matplotlib import axis as maxis -from matplotlib import bbox as mbbox from matplotlib import cbook from matplotlib import collections as mcoll from matplotlib import colors as mcolors @@ -33,7 +32,6 @@ iterable = cbook.iterable is_string_like = cbook.is_string_like -Wrapper = cbook.Wrapper def delete_masked_points(*args): @@ -484,8 +482,8 @@ """ martist.Artist.__init__(self) - self._position = rect - self._originalPosition = rect + self._position = maffine.Bbox.from_lbwh(*rect) + self._originalPosition = self._position.copy() self.set_axes(self) self.set_aspect('auto') self.set_adjustable('box') @@ -615,11 +613,11 @@ """ martist.Artist.set_figure(self, fig) - l, b, w, h = self._position - xmin = fig.bbox.xmin() - xmax = fig.bbox.xmax() - ymin = fig.bbox.ymin() - ymax = fig.bbox.ymax() + l, b, w, h = self._position.get_bounds() + xmin = fig.bbox.xmin + xmax = fig.bbox.xmax + ymin = fig.bbox.ymin + ymax = fig.bbox.ymax figw = xmax-xmin figh = ymax-ymin self.left = l*figw @@ -627,7 +625,7 @@ self.right = (l+w)*figw self.top = (b+h)*figh - self.bbox = mbbox.Bbox.from_lbrt( + self.bbox = maffine.Bbox.from_lbrt( self.left, self.bottom, self.right, self.top, ) @@ -639,7 +637,7 @@ set the dataLim and viewLim BBox attributes and the transData and transAxes Transformation attributes """ - Bbox = mbbox.Bbox + Bbox = maffine.Bbox if self._sharex is not None: left = self._sharex.viewLim.xmin() right = self._sharex.viewLim.xmax() @@ -655,11 +653,12 @@ self.viewLim = Bbox.from_lbrt(left, bottom, right, top) self.dataLim = Bbox.unit() + self.dataLim.track = True - self.transData = maffine.get_bbox_transform( + self.transData = maffine.BboxTransform( self.viewLim, self.bbox) - self.transAxes = maffine.get_bbox_transform( - self.dataLim, self.bbox) + self.transAxes = maffine.BboxTransform( + Bbox.unit(), self.bbox) print "_set_lim_and_transforms", self.viewLim, self.transData, self.dataLim, self.transAxes, self.bbox @@ -1184,9 +1183,8 @@ # MGDTODO ## self.dataLim.update_numerix(x, y, -1) print "update_datalim_numerix", self.dataLim, - self.dataLim = mbbox.Bbox.from_data(x, y) + self.dataLim.update_from_data(x, y) print self.dataLim - # MGDTODO side-effects def _get_verts_in_data_coords(self, trans, xys): if trans == self.transData: @@ -1247,7 +1245,7 @@ axis direction reversal that has already been done. """ # if image data only just use the datalim - print "autoscale_view" + print "autoscale_view", self._autoscaleon, scalex, scaley if not self._autoscaleon: return if (tight or (len(self.images)>0 and @@ -1504,7 +1502,7 @@ def get_xlim(self): 'Get the x axis range [xmin, xmax]' - return self.viewLim.intervalx().get_bounds() + return self.viewLim.intervalx def set_xlim(self, xmin=None, xmax=None, emit=True, **kwargs): @@ -1549,12 +1547,10 @@ # and min(xmin, xmax)<=0): # raise ValueError('Cannot set nonpositive limits with log transform') - xmin, xmax = mbbox.nonsingular(xmin, xmax, increasing=False) + xmin, xmax = maffine.nonsingular(xmin, xmax, increasing=False) - # MGDTODO: This is fairly cumbersome - # MGDTODO: side-effects on x-bounds should propagate - self.viewLim = mbbox.Bbox.from_lbrt(xmin, self.viewLim.ymin(), xmax, self.viewLim.ymax()) - print 'set_xlim', self.viewLim + self.viewLim.intervalx = (xmin, xmax) + print 'set_xlim', self.viewLim, xmin, xmax return xmin, xmax @@ -1634,7 +1630,7 @@ def get_ylim(self): 'Get the y axis range [ymin, ymax]' - return self.viewLim.intervaly().get_bounds() + return self.viewLim.intervaly def set_ylim(self, ymin=None, ymax=None, emit=True, **kwargs): """ @@ -1677,9 +1673,8 @@ # and min(ymin, ymax)<=0): # raise ValueError('Cannot set nonpositive limits with log transform') - ymin, ymax = mbbox.nonsingular(ymin, ymax, increasing=False) - # MGDTODO: side-effects on y-bounds should propagate - self.viewLim = mbbox.Bbox.from_lbrt(self.viewLim.xmin(), ymin, self.viewLim.xmax(), ymax) + ymin, ymax = maffine.nonsingular(ymin, ymax, increasing=False) + self.viewLim.intervaly = (ymin, ymax) if emit: self.callbacks.process('ylim_changed', self) print "set_ylim", self.viewLim Modified: branches/transforms/lib/matplotlib/axis.py =================================================================== --- branches/transforms/lib/matplotlib/axis.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/axis.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -15,8 +15,8 @@ from ticker import NullFormatter, FixedFormatter, ScalarFormatter, LogFormatter from ticker import NullLocator, FixedLocator, LinearLocator, LogLocator, AutoLocator -from affine import Affine2D, blend_xy_sep_transform -from bbox import bbox_union +from affine import Affine2D, Bbox, blend_xy_sep_transform, interval_contains, \ + interval_contains_open from font_manager import FontProperties from text import Text, TextWithDash, _process_text_args from patches import bbox_artist @@ -160,7 +160,7 @@ def draw(self, renderer): if not self.get_visible(): return renderer.open_group(self.__name__) - midPoint = self.get_view_interval().contains_open( self.get_loc() ) + midPoint = interval_contains_open(self.get_view_interval(), self.get_loc() ) if midPoint: if self.gridOn: self.gridline.draw(renderer) @@ -239,7 +239,7 @@ trans = blend_xy_sep_transform( self.axes.transData, self.axes.transAxes) #offset the text downward with a post transformation - trans = trans + Affine2D().translated(0, -1 * self._padPixels) + trans = trans + Affine2D().translate(0, -1 * self._padPixels) t.set_transform(trans) self._set_artist_props(t) @@ -264,7 +264,7 @@ trans = blend_xy_sep_transform( self.axes.transData, self.axes.transAxes) # offset the text upward with a post transformation - trans = trans + Affine2D().translated(0, self._padPixels) + trans = trans + Affine2D().translate(0, self._padPixels) t.set_transform( trans ) self._set_artist_props(t) return t @@ -331,11 +331,11 @@ def get_view_interval(self): 'return the Interval instance for this axis view limits' - return self.axes.viewLim.intervalx() + return self.axes.viewLim.intervalx def get_data_interval(self): 'return the Interval instance for this axis data limits' - return self.axes.dataLim.intervalx() + return self.axes.dataLim.intervalx class YTick(Tick): @@ -362,7 +362,7 @@ trans = blend_xy_sep_transform( self.axes.transAxes, self.axes.transData) # offset the text leftward with a post transformation - trans = trans + Affine2D().translated(-1 * self._padPixels, 0) + trans = trans + Affine2D().translate(-1 * self._padPixels, 0) t.set_transform( trans ) #t.set_transform( self.axes.transData ) @@ -385,7 +385,7 @@ trans = blend_xy_sep_transform( self.axes.transAxes, self.axes.transData) # offset the text rightward with a post transformation - trans = trans + Affine2D().translated(self._padPixels, 0) + trans = trans + Affine2D().translate(self._padPixels, 0) t.set_transform( trans ) self._set_artist_props(t) return t @@ -455,11 +455,11 @@ def get_view_interval(self): 'return the Interval instance for this axis view limits' - return self.axes.viewLim.intervaly() + return self.axes.viewLim.intervaly def get_data_interval(self): 'return the Interval instance for this axis data limits' - return self.axes.dataLim.intervaly() + return self.axes.dataLim.intervaly class Ticker: @@ -575,7 +575,7 @@ interval = self.get_view_interval() for tick, loc, label in zip(majorTicks, majorLocs, majorLabels): if tick is None: continue - if not interval.contains(loc): continue + if not interval_contains(interval, loc): continue seen[loc] = 1 tick.update_position(loc) tick.set_label1(label) @@ -1034,19 +1034,19 @@ bottom = self.axes.bbox.ymin() else: - bbox = bbox_union(bboxes) - bottom = bbox.ymin() + bbox = Bbox.union(bboxes) + bottom = bbox.ymin self.label.set_position( (x, bottom-self.LABELPAD*self.figure.dpi/72.0)) # self.label.set_position( (x, bottom-self.LABELPAD*self.figure.dpi.get()/72.0)) MGDTODO else: if not len(bboxes2): - top = self.axes.bbox.ymax() + top = self.axes.bbox.ymax else: bbox = bbox_union(bboxes2) - top = bbox.ymax() + top = bbox.ymax self.label.set_position( (x, top+self.LABELPAD*self.figure.dpi.get()/72.0)) @@ -1059,8 +1059,8 @@ if not len(bboxes): bottom = self.axes.bbox.ymin() else: - bbox = bbox_union(bboxes) - bottom = bbox.ymin() + bbox = Bbox.union(bboxes) + bottom = bbox.ymin self.offsetText.set_position((x, bottom-self.OFFSETTEXTPAD*self.figure.dpi/72.0)) # self.offsetText.set_position((x, bottom-self.OFFSETTEXTPAD*self.figure.dpi.get()/72.0)) MGDTODO @@ -1133,11 +1133,11 @@ def get_view_interval(self): 'return the Interval instance for this axis view limits' - return self.axes.viewLim.intervalx() + return self.axes.viewLim.intervalx def get_data_interval(self): 'return the Interval instance for this axis data limits' - return self.axes.dataLim.intervalx() + return self.axes.dataLim.intervalx class YAxis(Axis): @@ -1223,11 +1223,11 @@ x,y = self.label.get_position() if self.label_position == 'left': if not len(bboxes): - left = self.axes.bbox.xmin() + left = self.axes.bbox.xmin else: - bbox = bbox_union(bboxes) - left = bbox.xmin() + bbox = Bbox.union(bboxes) + left = bbox.xmin self.label.set_position( (left-self.LABELPAD*self.figure.dpi/72.0, y)) # self.label.set_position( (left-self.LABELPAD*self.figure.dpi.get()/72.0, y)) MGDTODO @@ -1248,7 +1248,7 @@ boxes of all the ticklabels """ x,y = self.offsetText.get_position() - top = self.axes.bbox.ymax() + top = self.axes.bbox.ymax self.offsetText.set_position((x, top+self.OFFSETTEXTPAD*self.figure.dpi/72.0)) # self.offsetText.set_position((x, top+self.OFFSETTEXTPAD*self.figure.dpi.get()/72.0)) MGDTODO @@ -1335,11 +1335,11 @@ def get_view_interval(self): 'return the Interval instance for this axis view limits' - return self.axes.viewLim.intervaly() + return self.axes.viewLim.intervaly def get_data_interval(self): 'return the Interval instance for this axis data limits' - return self.axes.dataLim.intervaly() + return self.axes.dataLim.intervaly Modified: branches/transforms/lib/matplotlib/backends/backend_agg.py =================================================================== --- branches/transforms/lib/matplotlib/backends/backend_agg.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/backends/backend_agg.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -84,7 +84,7 @@ from matplotlib.font_manager import findfont from matplotlib.ft2font import FT2Font, LOAD_DEFAULT from matplotlib.mathtext import MathTextParser -from matplotlib.bbox import Bbox +from matplotlib.affine import Bbox from _backend_agg import RendererAgg as _RendererAgg Modified: branches/transforms/lib/matplotlib/backends/backend_tkagg.py =================================================================== --- branches/transforms/lib/matplotlib/backends/backend_tkagg.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/backends/backend_tkagg.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -219,13 +219,13 @@ def motion_notify_event(self, event): x = event.x # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height() - event.y + y = self.figure.bbox.height - event.y FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) def button_press_event(self, event): x = event.x # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height() - event.y + y = self.figure.bbox.height - event.y num = getattr(event, 'num', None) if sys.platform=='darwin': @@ -239,7 +239,7 @@ def button_release_event(self, event): x = event.x # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height() - event.y + y = self.figure.bbox.height - event.y num = getattr(event, 'num', None) @@ -609,7 +609,7 @@ return b def _init_toolbar(self): - xmin, xmax = self.canvas.figure.bbox.intervalx().get_bounds() + xmin, xmax = self.canvas.figure.bbox.intervalx height, width = 50, xmax-xmin Tk.Frame.__init__(self, master=self.window, width=width, height=height, Deleted: branches/transforms/lib/matplotlib/bbox.py =================================================================== --- branches/transforms/lib/matplotlib/bbox.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/bbox.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -1,161 +0,0 @@ -""" -A convenience class for handling bounding boxes - -2007 Michael Droettboom -""" - -import numpy as N - -class Interval: - def __init__(self, bounds): - self._bounds = N.array(bounds, N.float_) - - def contains(self, value): - bounds = self._bounds - return value >= bounds[0] and value <= bounds[1] - - def contains_open(self, value): - bounds = self._bounds - return value > bounds[0] and value < bounds[1] - - def get_bounds(self): - return self._bounds - - def span(self): - bounds = self._bounds - return bounds[1] - bounds[0] - -class Bbox: - def __init__(self, points): - self._points = N.array(points, N.float_) - - #@staticmethod - def unit(): - return Bbox([[0,0], [1,1]]) - unit = staticmethod(unit) - - #@staticmethod - def from_lbwh(left, bottom, width, height): - return Bbox([[left, bottom], [left + width, bottom + height]]) - from_lbwh = staticmethod(from_lbwh) - - #@staticmethod - def from_lbrt(left, bottom, right, top): - return Bbox([[left, bottom], [right, top]]) - from_lbrt = staticmethod(from_lbrt) - - #@staticmethod - def from_data(x, y): - return Bbox([[x.min(), y.min()], [x.max(), y.max()]]) - from_data = staticmethod(from_data) - - def copy(self): - return Bbox(self._points.copy()) - - def __repr__(self): - return 'Bbox(%s)' % repr(self._points) - __str__ = __repr__ - - def __cmp__(self, other): - # MGDTODO: Totally suboptimal - if isinstance(other, Bbox): - return (self._points == other._points).all() - return -1 - - # MGDTODO: Probably a more efficient ways to do this... - def xmin(self): - return self._points[0,0] - - def ymin(self): - return self._points[0,1] - - def xmax(self): - return self._points[1,0] - - def ymax(self): - return self._points[1,1] - - def width(self): - return self.xmax() - self.xmin() - - def height(self): - return self.ymax() - self.ymin() - - def transform(self, transform): - return Bbox(transform(self._points)) - - def inverse_transform(self, transform): - return Bbox(transform.inverted()(self._points)) - - def get_bounds(self): - return (self.xmin(), self.ymin(), - self.xmax() - self.xmin(), self.ymax() - self.ymin()) - - # MGDTODO: This is an inefficient way to do this - def intervalx(self): - return Interval(self._points[0]) - - def intervaly(self): - return Interval(self._points[1]) - - def scaled(self, sw, sh): - width = self.width() - height = self.height() - deltaw = (sw * width - width) / 2.0 - deltah = (sh * height - height) / 2.0 - a = N.array([[-deltaw, -deltah], [deltaw, deltah]]) - return Bbox(self._points + a) - - def contains(self, x, y): - return (x >= self.xmin() and x <= self.xmax() and - y >= self.ymin() and y <= self.ymax()) - -def bbox_union(bboxes): - """ - Return the Bbox that bounds all bboxes - """ - assert(len(bboxes)) - - if len(bboxes) == 1: - return bboxes[0] - - bbox = bboxes[0] - xmin = bbox.xmin() - ymin = bbox.ymin() - xmax = bbox.xmax() - ymax = bbox.ymax() - - for bbox in bboxes[1:]: - xmin = min(xmin, bbox.xmin()) - ymin = min(ymin, bbox.ymin()) - xmax = max(xmax, bbox.xmax()) - ymax = max(ymax, bbox.ymax()) - - return Bbox.from_lbrt(xmin, ymin, xmax, ymax) - -# MGDTODO: There's probably a better place for this -def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): - ''' - Ensure the endpoints of a range are not too close together. - - "too close" means the interval is smaller than 'tiny' times - the maximum absolute value. - - If they are too close, each will be moved by the 'expander'. - If 'increasing' is True and vmin > vmax, they will be swapped, - regardless of whether they are too close. - ''' - swapped = False - if vmax < vmin: - vmin, vmax = vmax, vmin - swapped = True - if vmax - vmin <= max(abs(vmin), abs(vmax)) * tiny: - if vmin==0.0: - vmin = -expander - vmax = expander - else: - vmin -= expander*abs(vmin) - vmax += expander*abs(vmax) - if swapped and not increasing: - vmin, vmax = vmax, vmin - return vmin, vmax Modified: branches/transforms/lib/matplotlib/figure.py =================================================================== --- branches/transforms/lib/matplotlib/figure.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/figure.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -18,8 +18,7 @@ from text import Text, _process_text_args from legend import Legend -from affine import get_bbox_transform -from bbox import Bbox +from affine import Bbox, BboxTransform from ticker import FormatStrFormatter from cm import ScalarMappable from contour import ContourSet @@ -105,7 +104,7 @@ class Figure(Artist): def __str__(self): - return "Figure(%gx%g)"%(self.figwidth, self.figheight) + return "Figure(%gx%g)"%(self.bbox.max) # return "Figure(%gx%g)"%(self.figwidth.get(),self.figheight.get()) def __init__(self, @@ -130,13 +129,13 @@ if edgecolor is None: edgecolor = rcParams['figure.edgecolor'] self.dpi = dpi - self.figwidth = figsize[0] * dpi - self.figheight = figsize[1] * dpi - self.bbox = Bbox.from_lbwh(0, 0, self.figwidth, self.figheight) + figwidth = figsize[0] * dpi + figheight = figsize[1] * dpi + self.bbox = Bbox.from_lbwh(0, 0, figwidth, figheight) self.frameon = frameon - self.transFigure = get_bbox_transform( Bbox.unit(), self.bbox) + self.transFigure = BboxTransform( Bbox.unit(), self.bbox) @@ -325,9 +324,9 @@ else: w,h = args - - self.figwidth = w - self.figheight = h + dpival = self.dpi + self.bbox.max = w * dpival, h * dpival + print self.bbox # self.figwidth.set(w) MGDTODO # self.figheight.set(h) @@ -341,7 +340,7 @@ manager.resize(int(canvasw), int(canvash)) def get_size_inches(self): - return self.figwidth, self.figheight + return self.bbox.max # return self.figwidth.get(), self.figheight.get() MGDTODO def get_edgecolor(self): @@ -354,12 +353,12 @@ def get_figwidth(self): 'Return the figwidth as a float' - return self.figwidth + return self.bbox.xmax # return self.figwidth.get() MGDTODO def get_figheight(self): 'Return the figheight as a float' - return self.figheight.get() + return self.bbox.ymax def get_dpi(self): 'Return the dpi as a float' @@ -402,7 +401,7 @@ ACCEPTS: float """ # self.figwidth.set(val) MGDTODO - self.figwidth = val + self.bbox.xmax = val def set_figheight(self, val): """ @@ -411,7 +410,7 @@ ACCEPTS: float """ # MGDTODO (set()) - self.figheight = val + self.bbox.ymax = val def set_frameon(self, b): """ Modified: branches/transforms/lib/matplotlib/legend.py =================================================================== --- branches/transforms/lib/matplotlib/legend.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/legend.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -34,8 +34,7 @@ from patches import Patch, Rectangle, RegularPolygon, Shadow, bbox_artist, draw_bbox from collections import LineCollection, RegularPolyCollection, PatchCollection from text import Text -from affine import get_bbox_transform -from bbox import Bbox, bbox_union +from affine import Bbox, BboxTransform def line_cuts_bbox(line, bbox): """ Return True if and only if line cuts bbox. """ @@ -165,7 +164,7 @@ else: raise TypeError("Legend needs either Axes or Figure as parent") self.parent = parent - self.set_transform( get_bbox_transform( Bbox.unit(), parent.bbox) ) + self.set_transform( BboxTransform( Bbox.unit(), parent.bbox) ) if loc is None: loc = rcParams["legend.loc"] @@ -260,10 +259,10 @@ bboxesAll = bboxesText bboxesAll.extend(bboxesHandles) - bbox = bbox_union(bboxesAll) + bbox = Bbox.union(bboxesAll) self.save = bbox - ibox = bbox.inverse_transform(self.get_transform()) + ibox = bbox.inverse_transformed(self.get_transform()) self.ibox = ibox return ibox @@ -531,7 +530,7 @@ if not len(self.legendHandles) and not len(self.texts): return def get_tbounds(text): #get text bounds in axes coords bbox = text.get_window_extent(renderer) - bboxa = bbox.inverse_transform(self.get_transform()) + bboxa = bbox.inverse_transformed(self.get_transform()) return bboxa.get_bounds() hpos = [] Modified: branches/transforms/lib/matplotlib/lines.py =================================================================== --- branches/transforms/lib/matplotlib/lines.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/lines.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -14,11 +14,11 @@ import numerix.ma as ma from matplotlib import verbose import artist +from affine import Bbox from artist import Artist, setp from cbook import iterable, is_string_like, is_numlike from colors import colorConverter -from bbox import Bbox from matplotlib import rcParams # special-purpose marker identifiers: Modified: branches/transforms/lib/matplotlib/table.py =================================================================== --- branches/transforms/lib/matplotlib/table.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/table.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -29,7 +29,7 @@ from patches import Rectangle from cbook import enumerate, is_string_like, flatten from text import Text -from bbox import Bbox, bbox_union +from affine import Bbox @@ -130,7 +130,7 @@ def get_text_bounds(self, renderer): """ Get text bounds in axes co-ordinates. """ bbox = self._text.get_window_extent(renderer) - bbox.inverse_transform(self.get_transform()) + bboxa = bbox.inverse_transformed(self.get_transform()) return bboxa.get_bounds() def get_required_width(self, renderer): Modified: branches/transforms/lib/matplotlib/text.py =================================================================== --- branches/transforms/lib/matplotlib/text.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/text.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -15,7 +15,7 @@ from cbook import enumerate, is_string_like, maxdict, is_numlike from font_manager import FontProperties from patches import bbox_artist, YAArrow -from bbox import Bbox, bbox_union +from affine import Bbox from lines import Line2D import matplotlib.nxutils as nxutils Modified: branches/transforms/lib/matplotlib/ticker.py =================================================================== --- branches/transforms/lib/matplotlib/ticker.py 2007-09-12 07:04:38 UTC (rev 3834) +++ branches/transforms/lib/matplotlib/ticker.py 2007-09-12 13:36:25 UTC (rev 3835) @@ -115,7 +115,7 @@ import matplotlib as mpl from matplotlib import verbose, rcParams from matplotlib import cbook -from matplotlib import bbox as mbbox +from matplotlib import affine as maffine @@ -148,8 +148,9 @@ cases where the Intervals do not need to be updated automatically. ''' - self.dataInterval = mbbox.Interval([vmin, vmax]) - self.viewInterval = mbbox.Interval([vmin, vmax]) + # MGDTODO: Interval no longer exists + self.dataInterval = maffine.Interval([vmin, vmax]) + self.viewInterval = maffine.Interval([vmin, vmax]) class Formatter(TickHelper): """ @@ -341,7 +342,8 @@ self.locs = locs if len(self.locs) > 0: self.verify_intervals() - d = abs(self.viewInterval.span()) + vmin, vmax = self.viewInterval + d = abs(vmax - vmin) if self._useOffset: self._set_offset(d) self._set_orderOfMagnitude(d) self._set_format() @@ -571,7 +573,7 @@ def autoscale(self): 'autoscale the view limits' self.verify_intervals() - return mbbox.nonsingular(*self.dataInterval.get_bounds()) + return maffine.nonsingular(*self.dataInterval.get_bounds()) def pan(self, numsteps): 'Pan numticks (can be positive or negative)' @@ -713,7 +715,7 @@ vmin = math.floor(scale*vmin)/scale vmax = math.ceil(scale*vmax)/scale - return mbbox.nonsingular(vmin, vmax) + return maffine.nonsingular(vmin, vmax) def closeto(x,y): @@ -797,7 +799,7 @@ vmin -=1 vmax +=1 - return mbbox.nonsingular(vmin, vmax) + return maffine.nonsingular(vmin, vmax) def scale_range(vmin, vmax, n = 1, threshold=100): dv = abs(vmax - vmin) @@ -864,14 +866,14 @@ def __call__(self): self.verify_intervals() - vmin, vmax = self.viewInterval.get_bounds() - vmin, vmax = mbbox.nonsingular(vmin, vmax, expander = 0.05) + vmin, vmax = self.viewInterval + vmin, vmax = maffine.nonsingular(vmin, vmax, expander = 0.05) return self.bin_boundaries(vmin, vmax) def autoscale(self): self.verify_intervals() - dmin, dmax = self.dataInterval.get_bounds() - dmin, dmax = mbbox.nonsingular(dmin, dmax, expander = 0.05) + dmin, dmax = self.dataInterval + dmin, dmax = maffine.nonsingular(dmin, dmax, expander = 0.05) return npy.take(self.bin_boundaries(dmin, dmax), [0,-1]) @@ -972,7 +974,7 @@ if vmin==vmax: vmin = decade_down(vmin,self._base) vmax = decade_up(vmax,self._base) - return mbbox.nonsingular(vmin, vmax) + return maffine.nonsingular(vmin, vmax) class AutoLocator(MaxNLocator): def __init__(self): This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |