|
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.
|