From: <md...@us...> - 2007-09-12 19:48:00
|
Revision: 3842 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=3842&view=rev Author: mdboom Date: 2007-09-12 12:47:56 -0700 (Wed, 12 Sep 2007) Log Message: ----------- More progress. Zooming and panning working thanks to John's patch. Modified Paths: -------------- branches/transforms/lib/matplotlib/affine.py branches/transforms/lib/matplotlib/artist.py branches/transforms/lib/matplotlib/axes.py branches/transforms/lib/matplotlib/backend_bases.py branches/transforms/lib/matplotlib/backends/backend_agg.py branches/transforms/lib/matplotlib/backends/backend_tkagg.py Modified: branches/transforms/lib/matplotlib/affine.py =================================================================== --- branches/transforms/lib/matplotlib/affine.py 2007-09-12 18:22:24 UTC (rev 3841) +++ branches/transforms/lib/matplotlib/affine.py 2007-09-12 19:47:56 UTC (rev 3842) @@ -8,9 +8,16 @@ from numpy.linalg import inv from sets import Set +# MGDTODO: The name of this module is bad, since it deals with +# non-affine transformations as well. It should probably just be +# "transforms", but we already had one of those... ;) + # MGDTODO: This creates a ton of cyclical references. We may want to # consider using weak references +# MGDTODO: deep copying is probably incorrect wrt the parent/child +# relationships + class TransformNode(object): def __init__(self): self._parents = Set() @@ -48,29 +55,31 @@ points = N.array(args, dtype=N.float_).reshape(2, 2) return Bbox(points) from_lbrt = staticmethod(from_lbrt) + + def __copy__(self): + return Bbox(self._points.copy()) + def __deepcopy__(self, memo): + return Bbox(self._points.copy()) + def __cmp__(self, other): # MGDTODO: Totally suboptimal - if isinstance(other, Bbox): - if (self._points == other._points).all(): - return 0 + if isinstance(other, Bbox) and (self._points == other._points).all(): + return 0 return -1 - + + def __repr__(self): + return 'Bbox(%s)' % repr(self._points) + __str__ = __repr__ + # JDH: the update method will update the box limits from the # existing limits and the new data; it appears here you are just # using the new data. We use an "ignore" flag to specify whether # you want to include the existing data or not in the update - def update_from_data(self, x, y): + def update_from_data(self, x, y, ignore=True): self._points = N.array([[x.min(), y.min()], [x.max(), y.max()]], N.float_) self.invalidate() - - def copy(self): - return Bbox(self._points.copy()) - def __repr__(self): - return 'Bbox(%s)' % repr(self._points) - __str__ = __repr__ - # MGDTODO: Probably a more efficient ways to do this... def _get_xmin(self): return self._points[0, 0] @@ -136,19 +145,24 @@ return self.ymax - self.ymin height = property(_get_height) + def _get_bounds(self): + return (self.xmin, self.ymin, + self.xmax - self.xmin, self.ymax - self.ymin) + def _set_bounds(self, bounds): + l,b,w,h = bounds + self._points = N.array([[l, b], [l+w, b+h]], N.float_) + self.invalidate() + bounds = property(_get_bounds, _set_bounds) + def transformed(self, transform): return Bbox(transform(self._points)) def inverse_transformed(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) - def expanded(self, sw, sh): - width = self.width() - height = self.height() + 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]]) @@ -199,6 +213,9 @@ if isinstance(other, Transform): return composite_transform_factory(other, self) raise TypeError("Can not add Transform to object of type '%s'" % type(other)) + + def transform_point(self, point): + return self.__call__([point])[0] def has_inverse(self): raise NotImplementedError() @@ -211,49 +228,27 @@ def is_affine(self): return False - -class Affine2D(Transform): + +# MGDTODO: Separate out Affine2DBase / Affine2DConcrete so BlendedAffine and CompositeAffine don't have translate/scale/rotate members + +class Affine2DBase(Transform): input_dims = 2 output_dims = 2 - - def __init__(self, matrix = None): - """ - Initialize an Affine transform from a 3x3 numpy float array. - a c e - b d f - 0 0 1 - """ + def __init__(self): Transform.__init__(self) - if matrix is None: - matrix = N.identity(3) - else: - assert matrix.shape == (3, 3) - self._mtx = matrix self._inverted = None - def __repr__(self): - return "Affine2D(%s)" % repr(self._mtx) - __str__ = __repr__ - - def __cmp__(self, other): - # MGDTODO: We need to decide if we want deferred transforms - # to be equal to this one - if isinstance(other, Affine2D): - if (self.get_matrix() == other.get_matrix()).all(): - return 0 - return -1 - def _do_invalidation(self): result = self._inverted is None self._inverted = None return result + + #@staticmethod + def _concat(a, b): + return N.dot(b, a) + _concat = staticmethod(_concat) - #@staticmethod - def from_values(a, b, c, d, e, f): - return Affine2D(Affine2D.matrix_from_values(a, b, c, d, e, f)) - from_values = staticmethod(from_values) - def to_values(self): mtx = self.get_matrix() return tuple(mtx[:2].swapaxes(0, 1).flatten()) @@ -268,7 +263,7 @@ matrix_from_values = staticmethod(matrix_from_values) def get_matrix(self): - return self._mtx + raise NotImplementedError() def __call__(self, points): """ @@ -278,26 +273,74 @@ points must be a numpy array of shape (N, 2), where N is the number of points. """ - # MGDTODO: This involves a copy. We may need to do something like - # http://neuroimaging.scipy.org/svn/ni/ni/trunk/neuroimaging/core/reference/mapping.py - # to separate the matrix out into the translation and scale components - # and apply each separately (which is still sub-optimal) - - # 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 + # MGDTODO: The major speed trap here is just converting to + # the points to an array in the first place. If we can use + # more arrays upstream, that should help here. 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(mtx, new_points)[:2] - return result.swapaxes(0, 1) + points = points.transpose() + points = N.dot(mtx[0:2, 0:2], points) + points = points + mtx[0:2, 2:] + return points.transpose() + def inverted(self): + if self._inverted is None: + mtx = self.get_matrix() + self._inverted = Affine2D(inv(mtx)) + return self._inverted + + def is_separable(self): + mtx = self.get_matrix() + return mtx[0, 1] == 0.0 and mtx[1, 0] == 0.0 + + def is_affine(self): + return True + + +class Affine2D(Affine2DBase): + input_dims = 2 + output_dims = 2 + + def __init__(self, matrix = None): + """ + Initialize an Affine transform from a 3x3 numpy float array. + + a c e + b d f + 0 0 1 + """ + Affine2DBase.__init__(self) + if matrix is None: + matrix = N.identity(3) + else: + assert matrix.shape == (3, 3) + self._mtx = matrix + self._inverted = None + + def __repr__(self): + return "Affine2D(%s)" % repr(self._mtx) + __str__ = __repr__ + + def __cmp__(self, other): + if (isinstance(other, Affine2D) and + (self.get_matrix() == other.get_matrix()).all()): + return 0 + return -1 + + def __copy__(self): + return Affine2D(self._mtx.copy()) + + def __deepcopy__(self, memo): + return Affine2D(self._mtx.copy()) + #@staticmethod - def _concat(a, b): - return N.dot(b, a) - _concat = staticmethod(_concat) + def from_values(a, b, c, d, e, f): + return Affine2D(Affine2D.matrix_from_values(a, b, c, d, e, f)) + from_values = staticmethod(from_values) + def get_matrix(self): + return self._mtx + #@staticmethod def concat(a, b): return Affine2D(Affine2D._concat(a._mtx, b._mtx)) @@ -346,19 +389,18 @@ def is_affine(self): return True -class BlendedAffine2D(Affine2D): +class BlendedAffine2D(Affine2DBase): def __init__(self, x_transform, y_transform): assert x_transform.is_affine() assert y_transform.is_affine() assert x_transform.is_separable() assert y_transform.is_separable() - Transform.__init__(self) + Affine2DBase.__init__(self) self.add_children([x_transform, y_transform]) self._x = x_transform self._y = y_transform self._mtx = None - self._inverted = None def __repr__(self): return "BlendedAffine2D(%s,%s)" % (self._x, self._y) @@ -367,7 +409,7 @@ def _do_invalidation(self): if self._mtx is not None: self._mtx = None - Affine2D._do_invalidation(self) + Affine2DBase._do_invalidation(self) return False return True @@ -376,8 +418,9 @@ x_mtx = self._x.get_matrix() y_mtx = self._y.get_matrix() # This works because we already know the transforms are - # separable - self._mtx = N.vstack([x_mtx[0], y_mtx[1], [0.0, 0.0, 1.0]]) + # separable, though normally one would want to set b and + # c to zero. + self._mtx = N.vstack((x_mtx[0], y_mtx[1], [0.0, 0.0, 1.0])) def is_separable(self): return True @@ -397,20 +440,22 @@ self._y = y_transform def __call__(self, points): - # MGDTODO: Implement me - pass + x_points = self._x(points) + y_points = self._y(points) + # This works because we already know the transforms are + # separable + return N.hstack((x_points[:, 0:1], y_points[:, 1:2])) -class CompositeAffine2D(Affine2D): +class CompositeAffine2D(Affine2DBase): def __init__(self, a, b): assert a.is_affine() assert b.is_affine() - Transform.__init__(self) + Affine2DBase.__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) @@ -418,7 +463,7 @@ def _do_invalidation(self): self._mtx = None - Affine2D._do_invalidation(self) + Affine2DBase._do_invalidation(self) def _make__mtx(self): if self._mtx is None: @@ -433,22 +478,23 @@ class CompositeTransform(Transform): def __init__(self, a, b): assert a.output_dims == b.input_dims - + self.input_dims = a.input_dims + self.output_dims = b.output_dims + 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): +class BboxTransform(Affine2DBase): def __init__(self, boxin, boxout): assert isinstance(boxin, Bbox) assert isinstance(boxout, Bbox) - Transform.__init__(self) + Affine2DBase.__init__(self) self.add_children([boxin, boxout]) self._boxin = boxin self._boxout = boxout @@ -462,7 +508,7 @@ def _do_invalidation(self): if self._mtx is not None: self._mtx = None - Affine2D._do_invalidation(self) + Affine2DBase._do_invalidation(self) return False return True @@ -481,14 +527,6 @@ 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 @@ -541,6 +579,9 @@ return interval[0] < val and interval[1] > val if __name__ == '__main__': + from random import random + import timeit + bbox = Bbox.from_lbrt(10., 15., 20., 25.) assert bbox.xmin == 10 assert bbox.ymin == 15 @@ -589,7 +630,9 @@ scale = Affine2D().scale(10, 20) assert scale.to_values() == (10, 0, 0, 20, 0, 0) rotation = Affine2D().rotate_deg(30) - print rotation.to_values() == (0.86602540378443871, 0.49999999999999994, -0.49999999999999994, 0.86602540378443871, 0.0, 0.0) + print rotation.to_values() == (0.86602540378443871, 0.49999999999999994, + -0.49999999999999994, 0.86602540378443871, + 0.0, 0.0) points = N.array([[1,2],[3,4],[5,6],[7,8]], N.float_) translated_points = translation(points) @@ -600,11 +643,20 @@ print rotated_points tpoints1 = rotation(translation(scale(points))) - trans_sum = rotation + translation + scale + trans_sum = scale + translation + rotation tpoints2 = trans_sum(points) print tpoints1, tpoints2 print tpoints1 == tpoints2 # Need to do some sort of fuzzy comparison here? # assert (tpoints1 == tpoints2).all() + + # Here are some timing tests + points = [(random(), random()) for i in xrange(10000)] + t = timeit.Timer("trans_sum(points)", "from __main__ import trans_sum, points") + print "Time to transform 10000 x 10 points as tuples:", t.timeit(10) + + points2 = N.asarray(points) + t = timeit.Timer("trans_sum(points2)", "from __main__ import trans_sum, points2") + print "Time to transform 10000 x 10 points as numpy array:", t.timeit(10) __all__ = ['Transform', 'Affine2D'] Modified: branches/transforms/lib/matplotlib/artist.py =================================================================== --- branches/transforms/lib/matplotlib/artist.py 2007-09-12 18:22:24 UTC (rev 3841) +++ branches/transforms/lib/matplotlib/artist.py 2007-09-12 19:47:56 UTC (rev 3842) @@ -337,7 +337,7 @@ def _set_gc_clip(self, gc): 'set the clip properly for the gc' if self.clipbox is not None: - gc.set_clip_rectangle(self.clipbox.get_bounds()) + gc.set_clip_rectangle(self.clipbox.bounds) gc.set_clip_path(self._clippath) def draw(self, renderer, *args, **kwargs): Modified: branches/transforms/lib/matplotlib/axes.py =================================================================== --- branches/transforms/lib/matplotlib/axes.py 2007-09-12 18:22:24 UTC (rev 3841) +++ branches/transforms/lib/matplotlib/axes.py 2007-09-12 19:47:56 UTC (rev 3842) @@ -1,5 +1,5 @@ from __future__ import division, generators -import math, sys, warnings +import math, sys, warnings, copy import numpy as npy @@ -483,7 +483,7 @@ """ martist.Artist.__init__(self) self._position = maffine.Bbox.from_lbwh(*rect) - self._originalPosition = self._position.copy() + self._originalPosition = copy.deepcopy(self._position) self.set_axes(self) self.set_aspect('auto') self.set_adjustable('box') @@ -613,7 +613,7 @@ """ martist.Artist.set_figure(self, fig) - l, b, w, h = self._position.get_bounds() + l, b, w, h = self._position.bounds xmin = fig.bbox.xmin xmax = fig.bbox.xmax ymin = fig.bbox.ymin @@ -669,9 +669,9 @@ def get_position(self, original=False): 'Return the axes rectangle left, bottom, width, height' if original: - return self._originalPosition[:] + return self._originalPosition.bounds else: - return self._position[:] + return self._position.bounds # return [val.get() for val in self._position] def set_position(self, pos, which='both'): @@ -694,10 +694,10 @@ # # Change values within self._position--don't replace it. # for num,val in zip(pos, self._position): # val.set(num) - self._position = pos + self._position.bounds = pos.bounds # MGDTODO: side-effects if which in ('both', 'original'): - self._originalPosition = pos + self._originalPosition.bounds = pos.bounds def _set_artist_props(self, a): @@ -1547,7 +1547,9 @@ def get_xscale(self): 'return the xaxis scale string: log or linear' - return self.scaled[self.transData.get_funcx().get_type()] + # MGDTODO + # return self.scaled[self.transData.get_funcx().get_type()] + return 'linear' def set_xscale(self, value, basex = 10, subsx=None): """ @@ -1671,7 +1673,8 @@ def get_yscale(self): 'return the yaxis scale string: log or linear' - return self.scaled[self.transData.get_funcy().get_type()] + # return self.scaled[self.transData.get_funcy().get_type()] + return 'linear' def set_yscale(self, value, basey=10, subsy=None): """ Modified: branches/transforms/lib/matplotlib/backend_bases.py =================================================================== --- branches/transforms/lib/matplotlib/backend_bases.py 2007-09-12 18:22:24 UTC (rev 3841) +++ branches/transforms/lib/matplotlib/backend_bases.py 2007-09-12 19:47:56 UTC (rev 3842) @@ -4,7 +4,7 @@ """ from __future__ import division -import os, sys, warnings +import os, sys, warnings, copy import numpy as npy import matplotlib.numerix.npyma as ma @@ -1070,7 +1070,7 @@ def get_width_height(self): """return the figure width and height in points or pixels (depending on the backend), truncated to integers""" - return int(self.figure.bbox.width()), int(self.figure.bbox.height()) + return int(self.figure.bbox.width), int(self.figure.bbox.height) filetypes = { 'emf': 'Enhanced Metafile', @@ -1544,7 +1544,7 @@ xmin, xmax = a.get_xlim() ymin, ymax = a.get_ylim() lim = xmin, xmax, ymin, ymax - self._xypress.append((x, y, a, i, lim,a.transData.deepcopy())) + self._xypress.append((x, y, a, i, lim, copy.deepcopy(a.transData))) self.canvas.mpl_disconnect(self._idDrag) self._idDrag=self.canvas.mpl_connect('motion_notify_event', self.drag_pan) @@ -1571,7 +1571,7 @@ xmin, xmax = a.get_xlim() ymin, ymax = a.get_ylim() lim = xmin, xmax, ymin, ymax - self._xypress.append(( x, y, a, i, lim, a.transData.deepcopy() )) + self._xypress.append(( x, y, a, i, lim, copy.deepcopy(a.transData) )) self.press(event) @@ -1637,8 +1637,9 @@ #safer to use the recorded button at the press than current button: #multiple button can get pressed during motion... if self._button_pressed==1: - lastx, lasty = trans.inverse_xy_tup( (lastx, lasty) ) - x, y = trans.inverse_xy_tup( (event.x, event.y) ) + inverse = trans.inverted() + lastx, lasty = inverse.transform_point((lastx, lasty)) + x, y = inverse.transform_point( (event.x, event.y) ) if a.get_xscale()=='log': dx=1-lastx/x else: @@ -1664,15 +1665,16 @@ ymax -= dy elif self._button_pressed==3: try: - dx=(lastx-event.x)/float(a.bbox.width()) - dy=(lasty-event.y)/float(a.bbox.height()) + dx=(lastx-event.x)/float(a.bbox.width) + dy=(lasty-event.y)/float(a.bbox.height) dx,dy=format_deltas(event,dx,dy) if a.get_aspect() != 'auto': dx = 0.5*(dx + dy) dy = dx alphax = pow(10.0,dx) alphay = pow(10.0,dy)#use logscaling, avoid singularities and smother scaling... - lastx, lasty = trans.inverse_xy_tup( (lastx, lasty) ) + inverse = trans.inverted() + lastx, lasty = inverse.transform_point( (lastx, lasty) ) if a.get_xscale()=='log': xmin = lastx*(xmin/lastx)**alphax xmax = lastx*(xmax/lastx)**alphax @@ -1710,8 +1712,9 @@ xmin, ymin, xmax, ymax = lim # zoom to rect - lastx, lasty = a.transData.inverse_xy_tup( (lastx, lasty) ) - x, y = a.transData.inverse_xy_tup( (x, y) ) + inverse = a.transData.inverted() + lastx, lasty = inverse.transform_point( (lastx, lasty) ) + x, y = inverse.transform_point( (x, y) ) Xmin,Xmax=a.get_xlim() Ymin,Ymax=a.get_ylim() Modified: branches/transforms/lib/matplotlib/backends/backend_agg.py =================================================================== --- branches/transforms/lib/matplotlib/backends/backend_agg.py 2007-09-12 18:22:24 UTC (rev 3841) +++ branches/transforms/lib/matplotlib/backends/backend_agg.py 2007-09-12 19:47:56 UTC (rev 3842) @@ -398,7 +398,7 @@ self.figure.draw(self.renderer) def get_renderer(self): - l,b,w,h = self.figure.bbox.get_bounds() + l,b,w,h = self.figure.bbox.bounds # MGDTODO # key = w, h, self.figure.dpi.get() key = w, h, self.figure.dpi Modified: branches/transforms/lib/matplotlib/backends/backend_tkagg.py =================================================================== --- branches/transforms/lib/matplotlib/backends/backend_tkagg.py 2007-09-12 18:22:24 UTC (rev 3841) +++ branches/transforms/lib/matplotlib/backends/backend_tkagg.py 2007-09-12 19:47:56 UTC (rev 3842) @@ -147,7 +147,7 @@ def __init__(self, figure, master=None, resize_callback=None): FigureCanvasAgg.__init__(self, figure) self._idle = True - t1,t2,w,h = self.figure.bbox.get_bounds() + t1,t2,w,h = self.figure.bbox.bounds w, h = int(w), int(h) self._tkcanvas = Tk.Canvas( master=master, width=w, height=h, borderwidth=4) @@ -288,7 +288,7 @@ self.window.wm_title("Figure %d" % num) self.canvas = canvas self._num = num - t1,t2,w,h = canvas.figure.bbox.get_bounds() + t1,t2,w,h = canvas.figure.bbox.bounds w, h = int(w), int(h) self.window.minsize(int(w*3/4),int(h*3/4)) if matplotlib.rcParams['toolbar']=='classic': @@ -436,7 +436,7 @@ self.canvas = canvas self.window = window - xmin, xmax = canvas.figure.bbox.intervalx().get_bounds() + xmin, xmax = canvas.figure.bbox.intervalx height, width = 50, xmax-xmin Tk.Frame.__init__(self, master=self.window, width=width, height=height, @@ -582,7 +582,7 @@ self.message.set(s) def draw_rubberband(self, event, x0, y0, x1, y1): - height = self.canvas.figure.bbox.height() + height = self.canvas.figure.bbox.height y0 = height-y0 y1 = height-y1 try: self.lastrect This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |