From: <md...@us...> - 2007-09-21 16:52:54
|
Revision: 3872 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=3872&view=rev Author: mdboom Date: 2007-09-21 09:52:50 -0700 (Fri, 21 Sep 2007) Log Message: ----------- Further progress on arbitrary transformations -- zooming and panning now works without any log-scale-specific hacks. (Though the underlying model is slightly wrong.) Added graphviz output support for debugging transformation trees. Masked array handling much more robust. Modified Paths: -------------- branches/transforms/lib/matplotlib/axes.py branches/transforms/lib/matplotlib/backend_bases.py branches/transforms/lib/matplotlib/lines.py branches/transforms/lib/matplotlib/path.py branches/transforms/lib/matplotlib/transforms.py Modified: branches/transforms/lib/matplotlib/axes.py =================================================================== --- branches/transforms/lib/matplotlib/axes.py 2007-09-21 15:33:18 UTC (rev 3871) +++ branches/transforms/lib/matplotlib/axes.py 2007-09-21 16:52:50 UTC (rev 3872) @@ -637,10 +637,14 @@ # self.viewLim, self.bbox) self.preDataTransform = mtransforms.BboxTransform( self.viewLim, mtransforms.Bbox.unit()) - self.dataTransform = mtransforms.TestLogTransform() - # self.dataTransform = mtransforms.Affine2D().scale(1.5) +# self.dataTransform = mtransforms.TestPolarTransform() +# self.dataTransform = mtransforms.blended_transform_factory( +# mtransforms.TestLogTransform(), +# mtransforms.Affine2D()) + self.dataTransform = mtransforms.Affine2D() self.transData = self.preDataTransform + self.dataTransform + mtransforms.BboxTransform( mtransforms.Bbox.unit(), self.bbox) + self.transData.make_graphviz(open("trans.dot", "w")) def get_position(self, original=False): @@ -1523,7 +1527,7 @@ 'return the xaxis scale string: log or linear' # MGDTODO # return self.scaled[self.transData.get_funcx().get_type()] - return 'linear' + return 'log' def set_xscale(self, value, basex = 10, subsx=None): """ Modified: branches/transforms/lib/matplotlib/backend_bases.py =================================================================== --- branches/transforms/lib/matplotlib/backend_bases.py 2007-09-21 15:33:18 UTC (rev 3871) +++ branches/transforms/lib/matplotlib/backend_bases.py 2007-09-21 16:52:50 UTC (rev 3872) @@ -1655,60 +1655,30 @@ #multiple button can get pressed during motion... if self._button_pressed==1: 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: - dx=x-lastx - if a.get_yscale()=='log': - dy=1-lasty/y - else: - dy=y-lasty - - dx,dy=format_deltas(event,dx,dy) - - if a.get_xscale()=='log': - xmin *= 1-dx - xmax *= 1-dx - else: - xmin -= dx - xmax -= dx - if a.get_yscale()=='log': - ymin *= 1-dy - ymax *= 1-dy - else: - ymin -= dy - ymax -= dy + dx, dy = event.x - lastx, event.y - lasty + dx, dy = format_deltas(event, dx, dy) + delta = npy.array([[dx, dy], [dx, dy]], npy.float_) + bbox = transforms.Bbox(a.bbox.get_points() - delta) + result = bbox.transformed(inverse) elif self._button_pressed==3: try: + inverse = trans.inverted() 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... - 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 - else: - xmin = lastx+alphax*(xmin-lastx) - xmax = lastx+alphax*(xmax-lastx) - if a.get_yscale()=='log': - ymin = lasty*(ymin/lasty)**alphay - ymax = lasty*(ymax/lasty)**alphay - else: - ymin = lasty+alphay*(ymin-lasty) - ymax = lasty+alphay*(ymax-lasty) + alphax = pow(10.0, dx) + alphay = pow(10.0, dy) + # MGDTODO: Make better use of numpy + lastx, lasty = inverse.transform_point((lastx, lasty)) + xmin = (lastx + alphax * (xmin - lastx)) + xmax = (lastx + alphax * (xmax - lastx)) + ymin = (lasty + alphay * (ymin - lasty)) + ymax = (lasty + alphay * (ymax - lasty)) + result = transforms.Bbox.from_lbrt(xmin, ymin, xmax, ymax) except OverflowError: warnings.warn('Overflow while panning') return - a.set_xlim(xmin, xmax) - a.set_ylim(ymin, ymax) + a.set_xlim(*result.intervalx) + a.set_ylim(*result.intervaly) self.dynamic_update() Modified: branches/transforms/lib/matplotlib/lines.py =================================================================== --- branches/transforms/lib/matplotlib/lines.py 2007-09-21 15:33:18 UTC (rev 3871) +++ branches/transforms/lib/matplotlib/lines.py 2007-09-21 16:52:50 UTC (rev 3872) @@ -25,6 +25,53 @@ (TICKLEFT, TICKRIGHT, TICKUP, TICKDOWN, CARETLEFT, CARETRIGHT, CARETUP, CARETDOWN) = range(8) +def unmasked_index_ranges(mask, compressed = True): + ''' + Calculate the good data ranges in a masked 1-D npy.array, based on mask. + + Returns Nx2 npy.array with each row the start and stop indices + for slices of the compressed npy.array corresponding to each of N + uninterrupted runs of unmasked values. + If optional argument compressed is False, it returns the + start and stop indices into the original npy.array, not the + compressed npy.array. + Returns None if there are no unmasked values. + + Example: + + y = ma.array(npy.arange(5), mask = [0,0,1,0,0]) + #ii = unmasked_index_ranges(y.mask()) + ii = unmasked_index_ranges(ma.getmask(y)) + # returns [[0,2,] [2,4,]] + + y.compressed().filled()[ii[1,0]:ii[1,1]] + # returns npy.array [3,4,] + # (The 'filled()' method converts the masked npy.array to a numerix npy.array.) + + #i0, i1 = unmasked_index_ranges(y.mask(), compressed=False) + i0, i1 = unmasked_index_ranges(ma.getmask(y), compressed=False) + # returns [[0,3,] [2,5,]] + + y.filled()[ii[1,0]:ii[1,1]] + # returns npy.array [3,4,] + + ''' + m = npy.concatenate(((1,), mask, (1,))) + indices = npy.arange(len(mask) + 1) + mdif = m[1:] - m[:-1] + i0 = npy.compress(mdif == -1, indices) + i1 = npy.compress(mdif == 1, indices) + assert len(i0) == len(i1) + if len(i1) == 0: + return None + if not compressed: + return npy.concatenate((i0[:, npy.newaxis], i1[:, npy.newaxis]), axis=1) + seglengths = i1 - i0 + breakpoints = npy.cumsum(seglengths) + ic0 = npy.concatenate(((0,), breakpoints[:-1])) + ic1 = breakpoints + return npy.concatenate((ic0[:, npy.newaxis], ic1[:, npy.newaxis]), axis=1) + def segment_hits(cx,cy,x,y,radius): """Determine if any line segments are within radius of a point. Returns the list of line segments that are within that radius. @@ -302,7 +349,7 @@ self._picker = p def get_window_extent(self, renderer): - xy = self.get_transform()(self._xy) + xy = self.get_transform().transform(self._xy) x = xy[:, 0] y = xy[:, 1] @@ -343,9 +390,6 @@ self._yorig = y self.recache() - # MGDTODO: Masked data arrays are broken - _masked_array_to_path_code_mapping = npy.array( - [Path.LINETO, Path.MOVETO, Path.MOVETO], Path.code_type) def recache(self): #if self.axes is None: print 'recache no axes' #else: print 'recache units', self.axes.xaxis.units, self.axes.yaxis.units @@ -363,24 +407,15 @@ if len(x) != len(y): raise RuntimeError('xdata and ydata must be the same length') - self._xy = npy.vstack((npy.asarray(x, npy.float_), - npy.asarray(y, npy.float_))).transpose() + x = x.reshape((len(x), 1)) + y = y.reshape((len(y), 1)) + + self._xy = ma.concatenate((x, y), 1) self._x = self._xy[:, 0] # just a view self._y = self._xy[:, 1] # just a view self._logcache = None - - mx = ma.getmask(x) - my = ma.getmask(y) - mask = ma.mask_or(mx, my) - codes = None - if mask is not ma.nomask: - m = npy.concatenate(((1,), mask, (1,))) - mdif = m[1:] - m[:-1] - mdif = npy.maximum((mdif[:-1] * -2), mask) - codes = npy.take( - self._masked_array_to_path_code_mapping, - mdif) - self._path = Path(self._xy, codes, closed=False) + # Masked arrays are now handled by the Path class itself + self._path = Path(self._xy, closed=False) # MGDTODO: If _draw_steps is removed, remove the following line also self._step_path = None Modified: branches/transforms/lib/matplotlib/path.py =================================================================== --- branches/transforms/lib/matplotlib/path.py 2007-09-21 15:33:18 UTC (rev 3871) +++ branches/transforms/lib/matplotlib/path.py 2007-09-21 16:52:50 UTC (rev 3872) @@ -1,4 +1,5 @@ import numpy as npy +from numpy import ma as ma class Path(object): # Path codes @@ -21,10 +22,8 @@ code_type = npy.uint8 def __init__(self, vertices, codes=None, closed=True): - vertices = npy.asarray(vertices, npy.float_) - assert vertices.ndim == 2 - assert vertices.shape[1] == 2 - + vertices = ma.asarray(vertices, npy.float_) + if codes is None: if closed: codes = self.LINETO * npy.ones( @@ -41,10 +40,27 @@ assert codes.ndim == 1 assert len(codes) == len(vertices) + # The path being passed in may have masked values. However, + # the backends are not expected to deal with masked arrays, so + # we must remove them from the array (using compressed), and + # add MOVETO commands to the codes array accordingly. + mask = ma.getmask(vertices) + if mask is not ma.nomask: + mask1d = ma.mask_or(mask[:, 0], mask[:, 1]) + vertices = ma.compress(npy.invert(mask1d), vertices, 0) + codes = npy.where(npy.concatenate((mask1d[-1:], mask1d[:-1])), + self.MOVETO, codes) + codes = ma.masked_array(codes, mask=mask1d).compressed() + codes = npy.asarray(codes, self.code_type) + + vertices = npy.asarray(vertices, npy.float_) + + assert vertices.ndim == 2 + assert vertices.shape[1] == 2 + assert codes.ndim == 1 + self._codes = codes self._vertices = vertices - - assert self._codes.ndim == 1 def __repr__(self): return "Path(%s, %s)" % (self.vertices, self.codes) @@ -91,10 +107,11 @@ def unit_regular_polygon(cls, numVertices): path = cls._unit_regular_polygons.get(numVertices) if path is None: - theta = 2*npy.pi/numVertices * npy.arange(numVertices) - # This is to make sure the polygon always "points-up" + theta = 2*npy.pi/numVertices * npy.arange(numVertices).reshape((numVertices, 1)) + # This initial rotation is to make sure the polygon always + # "points-up" theta += npy.pi / 2.0 - verts = npy.vstack((npy.cos(theta), npy.sin(theta))).transpose() + verts = npy.concatenate((npy.cos(theta), npy.sin(theta))) path = Path(verts) cls._unit_regular_polygons[numVertices] = path return path Modified: branches/transforms/lib/matplotlib/transforms.py =================================================================== --- branches/transforms/lib/matplotlib/transforms.py 2007-09-21 15:33:18 UTC (rev 3871) +++ branches/transforms/lib/matplotlib/transforms.py 2007-09-21 16:52:50 UTC (rev 3872) @@ -5,6 +5,7 @@ """ import numpy as npy +from numpy import ma as ma from numpy.linalg import inv from sets import Set @@ -19,6 +20,7 @@ class TransformNode(object): def __init__(self): self._parents = Set() + self._children = [] def invalidate(self): self._do_invalidation() @@ -33,7 +35,34 @@ getattr(self, child)._parents.add(self) self._children = children + def make_graphviz(self, fobj): + def recurse(root): + fobj.write('%s [label="%s"];\n' % + (hash(root), root.__class__.__name__)) + if isinstance(root, Affine2DBase): + fobj.write('%s [style=filled, color=".7 .7 .9"];\n' % + hash(root)) + elif isinstance(root, BboxBase): + fobj.write('%s [style=filled, color=".9 .9 .7"];\n' % + hash(root)) + for child_name in root._children: + child = getattr(root, child_name) + fobj.write("%s -> %s;\n" % ( + hash(root), + hash(child))) + recurse(child) + fobj.write("digraph G {\n") + recurse(self) + fobj.write("}\n") + + def is_affine(self): + return isinstance(self, Affine2DBase) + + def is_bbox(self): + return isinstance(self, BboxBase) + + class BboxBase(TransformNode): ''' This is the read-only part of a bounding-box @@ -169,12 +198,6 @@ 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) and (self._points == other._points).all(): @@ -274,6 +297,8 @@ """ Return the Bbox that bounds all bboxes """ + # MGDTODO: There's got to be a way to utilize numpy here + # to make this faster... assert(len(bboxes)) if len(bboxes) == 1: @@ -297,7 +322,7 @@ class TransformedBbox(BboxBase): def __init__(self, bbox, transform): - assert isinstance(bbox, Bbox) + assert bbox.is_bbox() assert isinstance(transform, Transform) BboxBase.__init__(self) @@ -353,9 +378,6 @@ def is_separable(self): return False - def is_affine(self): - return False - class Affine2DBase(Transform): input_dims = 2 @@ -416,9 +438,9 @@ # print "".join(traceback.format_stack()) # print points mtx = self.get_matrix() - points = npy.asarray(points, npy.float_) + points = ma.asarray(points, npy.float_) points = points.transpose() - points = npy.dot(mtx[0:2, 0:2], points) + points = ma.dot(mtx[0:2, 0:2], points) points = points + mtx[0:2, 2:] return points.transpose() @@ -437,9 +459,6 @@ 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): def __init__(self, matrix = None): @@ -469,12 +488,6 @@ return 0 return -1 - def __copy__(self): - return Affine2D(self._mtx.copy()) - - def __deepcopy__(self, memo): - return Affine2D(self._mtx.copy()) - #@staticmethod def from_values(a, b, c, d, e, f): return Affine2D(Affine2D.matrix_from_values(a, b, c, d, e, f)) @@ -542,9 +555,31 @@ mtx = self.get_matrix() return mtx[0, 1] == 0.0 and mtx[1, 0] == 0.0 - def is_affine(self): - return True +class IdentityTransform(Affine2DBase): + """ + A special class that does the identity transform quickly. + """ + _mtx = npy.identity(3) + + def __cmp__(self, other): + if (isinstance(other, Affine2D) and + (other == IDENTITY)): + return 0 + return -1 + + def get_matrix(self): + return _mtx + + def transform(self, points): + return points + + def transform_without_affine(self, points): + return points, self + + def inverted(self): + return self + IDENTITY = Affine2D() class BlendedGenericTransform(Transform): @@ -553,10 +588,9 @@ def __init__(self, x_transform, y_transform): # Here we ask: "Does it blend?" - assert x_transform.is_separable() - assert y_transform.is_separable() - assert x_transform.input_dims == x_transform.output_dims == 2 - assert y_transform.input_dims == y_transform.output_dims == 2 + # MGDTODO: Turn these checks back on + # assert x_transform.is_separable() + # assert y_transform.is_separable() Transform.__init__(self) self._x = x_transform @@ -576,16 +610,18 @@ return self._x(points) if x.input_dims == 2: - x_points = x.transform(points)[:, 0] + x_points = x.transform(points)[:, 0:1] else: x_points = x.transform(points[:, 0]) - + x_points = x_points.reshape((len(x_points), 1)) + if y.input_dims == 2: - y_points = y.transform(points)[:, 1] + y_points = y.transform(points)[:, 1:] else: y_points = y.transform(points[:, 1]) + y_points = y_points.reshape((len(y_points), 1)) - return npy.vstack((x_points, y_points)).transpose() + return ma.concatenate((x_points, y_points), 1) def inverted(self): return BlendedGenericTransform(self._x.inverted(), self._y.inverted()) @@ -598,6 +634,9 @@ def __init__(self, x_transform, y_transform): assert x_transform.is_affine() assert y_transform.is_affine() + # MGDTODO: Turn these checks back on + # assert x_transform.is_separable() + # assert y_transform.is_separable() Transform.__init__(self) self._x = x_transform self._y = y_transform @@ -649,6 +688,8 @@ self._b = b self.set_children(['_a', '_b']) + self.take_shortcut = b.is_affine() + def __repr__(self): return "CompositeGenericTransform(%s, %s)" % (self._a, self._b) __str__ = __repr__ @@ -656,11 +697,15 @@ def transform(self, points): return self._b.transform(self._a.transform(points)) + def transform_without_affine(self, points): + if self.take_shortcut: + return self._a.transform(points), self._b + return self.transform(points), IDENTITY + def inverted(self): return CompositeGenericTransform(self._b.inverted(), self._a.inverted()) def is_separable(self): - return True return self._a.is_separable() and self._b.is_separable() @@ -702,35 +747,81 @@ output_dims = 1 def transform(self, a): - m = npy.ma.masked_where(a < 0, a) + m = ma.masked_where(a < 0, a) return npy.log10(m) class TestLogTransform(Transform): - input_dims = 2 - output_dims = 2 + input_dims = 1 + output_dims = 1 def transform(self, xy): - marray = npy.ma.masked_where(xy <= 0.0, xy * 10.0) - return npy.log10(marray) + marray = ma.masked_where(xy <= 0.0, xy * 10.0) + return (npy.log10(marray) * 0.5) + 0.5 def inverted(self): return TestInvertLogTransform() + def is_separable(self): + return True + class TestInvertLogTransform(Transform): - input_dims = 2 - output_dims = 2 + input_dims = 1 + output_dims = 1 def transform(self, xy): - return npy.power(10, xy) / 10.0 + return ma.power(10, (xy - 0.5) * 2.0) / 10.0 def inverted(self): return TestLogTransform() + def is_separable(self): + return True + + +class TestPolarTransform(Transform): + input_dims = 2 + output_dims = 2 + + def transform(self, xy): + debug = len(xy) > 4 + x = xy[:, 0:1] + y = xy[:, 1:] + x, y = ((y * npy.cos(x)) + 1.0) * 0.5, ((y * npy.sin(x)) + 1.0) * 0.5 + if debug: + print npy.min(xy[:, 0:1]), npy.max(xy[:, 0:1]), npy.min(xy[:, 1:]), npy.max(xy[:, 1:]) + print x.min(), x.max(), y.min(), y.max() + return ma.concatenate((x, y), 1) + + def inverted(self): + return TestInvertPolarTransform() + def is_separable(self): + return False + + +class TestInvertPolarTransform(Transform): + input_dims = 2 + output_dims = 2 + + def transform(self, xy): + x = xy[:, 0:1] + y = xy[:, 1:] + r = ma.sqrt(ma.power(x, 2) + ma.power(y, 2)) + theta = ma.arccos(x / r) + theta = ma.where(y < 0, 2 * npy.pi - theta, theta) + return ma.concatenate((theta / (npy.pi * 2), r), 1) + + def inverted(self): + return TestInvertPolarTransform() + + def is_separable(self): + return False + + class BboxTransform(Affine2DBase): def __init__(self, boxin, boxout): - assert isinstance(boxin, BboxBase) - assert isinstance(boxout, BboxBase) + assert boxin.is_bbox() + assert boxout.is_bbox() Affine2DBase.__init__(self) self._boxin = boxin This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |