From: <jd...@us...> - 2007-10-15 20:00:56
|
Revision: 3949 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=3949&view=rev Author: jdh2358 Date: 2007-10-15 13:00:54 -0700 (Mon, 15 Oct 2007) Log Message: ----------- fixed an aspect=auto problem with bezier ellipse approx Modified Paths: -------------- trunk/matplotlib/CHANGELOG trunk/matplotlib/boilerplate.py trunk/matplotlib/lib/matplotlib/backends/backend_ps.py trunk/matplotlib/lib/matplotlib/cbook.py trunk/matplotlib/lib/matplotlib/mlab.py trunk/matplotlib/lib/matplotlib/patches.py trunk/matplotlib/src/_backend_agg.cpp trunk/matplotlib/src/_transforms.cpp trunk/matplotlib/unit/ellipse_compare.py Modified: trunk/matplotlib/CHANGELOG =================================================================== --- trunk/matplotlib/CHANGELOG 2007-10-15 14:03:19 UTC (rev 3948) +++ trunk/matplotlib/CHANGELOG 2007-10-15 20:00:54 UTC (rev 3949) @@ -1,3 +1,12 @@ +2007-10-15 Fixed a bug in patches.Ellipse that was broken for + aspect='auto'. Scale free ellipses now work properly for + equal and auto on Agg and PS, and they fall back on a + polygonal approximation for nonlinear transformations until + we convince oursleves that the spline approximation holds + for nonlinear transformations. Added + unit/ellipse_compare.py to compare spline with vertex + approx for both aspects. JDH + 2007-10-05 remove generator expressions from texmanager and mpltraits. generator expressions are not supported by python-2.3 - DSD Modified: trunk/matplotlib/boilerplate.py =================================================================== --- trunk/matplotlib/boilerplate.py 2007-10-15 14:03:19 UTC (rev 3948) +++ trunk/matplotlib/boilerplate.py 2007-10-15 20:00:54 UTC (rev 3949) @@ -2,10 +2,9 @@ # file is pasted into pylab.py. We did try to do this the smart way, # with callable functions and new.function, but could never get the # docstrings right for python2.2. See -# http://groups-beta.google.com/group/comp.lang.python/messages/1b14640f3a4ad3dc,b3d7453af21e5f82,17739e70ac6f710c,9d5291fce29cbbb1,c5b578e4ffc6af28,056ff270daa2f414?thread_id=dcd63ec13096a0f6&mode=thread +# http://groups.google.com/group/comp.lang.python/browse_frm/thread/dcd63ec13096a0f6/1b14640f3a4ad3dc?#1b14640f3a4ad3dc - # note we check for __doc__ is not None since py2exe optimize removes # the docstrings Modified: trunk/matplotlib/lib/matplotlib/backends/backend_ps.py =================================================================== --- trunk/matplotlib/lib/matplotlib/backends/backend_ps.py 2007-10-15 14:03:19 UTC (rev 3948) +++ trunk/matplotlib/lib/matplotlib/backends/backend_ps.py 2007-10-15 20:00:54 UTC (rev 3949) @@ -329,7 +329,7 @@ size = prop.get_size_in_points() font.set_size(size, 72.0) return font - + def draw_arc(self, gc, rgbFace, x, y, width, height, angle1, angle2, rotation): """ Draw an arc centered at x,y with width and height and angles @@ -340,7 +340,7 @@ """ ps = '%f %f translate\n%f rotate\n%f %f translate\n%s ellipse' % \ (x, y, rotation, -x, -y, _nums_to_str(angle1, angle2, 0.5*width, 0.5*height, x, y)) - self._draw_ps(ps, gc, rgbFace, "arc") + self._draw_ps(ps, gc, None, "arc") def _rgba(self, im): return im.as_rgba_str() @@ -519,9 +519,42 @@ end += step if cliprect: write('grestore\n') - def draw_path(self,gc,rgbFace,path,trans): - pass + def draw_path(self, gc, rgbFace, path): + ps_cmd = [] + ps_cmd.append('newpath') + + while 1: + code, xp, yp = path.vertex() + + #print code, xp, yp + + if code == agg.path_cmd_stop: + ps_cmd.append('closepath') # Hack, path_cmd_end_poly not found + break + elif code == agg.path_cmd_move_to: + ps_cmd.append('%g %g m' % (xp,yp)) + elif code == agg.path_cmd_line_to: + ps_cmd.append('%g %g l' % (xp,yp)) + elif code == agg.path_cmd_curve3: + pass + elif code == agg.path_cmd_curve4: + verts = [xp, yp] + verts.extend(path.vertex()[1:]) + verts.extend(path.vertex()[1:]) + ps_cmd.append('%g %g %g %g %g %g curveto'%tuple(verts)) + elif code == agg.path_cmd_end_poly: + ps_cmd.append('closepath') + elif code == agg.path_cmd_mask: + pass + else: + pass + #print code + + ps = '\n'.join(ps_cmd) + + self._draw_ps(ps, gc, rgbFace, "custom_path") + def draw_lines(self, gc, x, y, transform): """ x and y are npy.equal length arrays, draw lines connecting each @@ -883,6 +916,7 @@ # local variable eliminates all repeated attribute lookups write = self._pswriter.write write('gsave\n') + if debugPS and command: write("% "+command+"\n") @@ -916,6 +950,7 @@ if cliprect: write("grestore\n") write('grestore\n') + def push_gc(self, gc, store=1): """ Push the current onto stack, with the exception of the clip box, which @@ -1581,5 +1616,15 @@ 0 0 1 5 3 roll arc setmatrix closepath - } bind def""" + } bind def""", + """/unitcircle { + newpath +-1. 0. moveto +-1.0 0.552284749831 -0.552284749831 1.0 0.0 1.0 curveto +0.552284749831 1.0 1.0 0.552284749831 1.0 0.0 curveto +1.0 -0.552284749831 0.552284749831 -1.0 0.0 -1.0 curveto +-0.552284749831 -1.0 -1.0 -0.552284749831 -1.0 0.0 curveto +closepath + } bind def""", + ] Modified: trunk/matplotlib/lib/matplotlib/cbook.py =================================================================== --- trunk/matplotlib/lib/matplotlib/cbook.py 2007-10-15 14:03:19 UTC (rev 3948) +++ trunk/matplotlib/lib/matplotlib/cbook.py 2007-10-15 20:00:54 UTC (rev 3949) @@ -230,17 +230,18 @@ except TypeError: return False else: return True -def to_filehandle(fname): +def to_filehandle(fname, flag='r'): """ fname can be a filename or a file handle. Support for gzipped - files is automatic, if the filename ends in .gz + files is automatic, if the filename ends in .gz. flag is a + read/write flag for file """ if is_string_like(fname): if fname.endswith('.gz'): import gzip - fh = gzip.open(fname) + fh = gzip.open(fname, flag) else: - fh = file(fname) + fh = file(fname, flag) elif hasattr(fname, 'seek'): fh = fname else: Modified: trunk/matplotlib/lib/matplotlib/mlab.py =================================================================== --- trunk/matplotlib/lib/matplotlib/mlab.py 2007-10-15 14:03:19 UTC (rev 3948) +++ trunk/matplotlib/lib/matplotlib/mlab.py 2007-10-15 20:00:54 UTC (rev 3949) @@ -809,6 +809,8 @@ If p is a scalar, the largest value of x less than or equal to the p percentage point in the sequence is returned. """ + + x = npy.ravel(x) x.sort() Nx = len(x) @@ -1282,7 +1284,10 @@ converterd, if not None, is a dictionary mapping column number or munged column name to a converter function - See examples/loadrec.py + names, if not None, is a list of header names. In this case, no + header will be read from the file + + if no rows are found, None is returned See examples/loadrec.py """ if converterd is None: @@ -1291,9 +1296,42 @@ import dateutil.parser parsedate = dateutil.parser.parse + fh = cbook.to_filehandle(fname) - reader = csv.reader(fh, delimiter=delimiter) + + class FH: + """ + for space delimited files, we want different behavior than + comma or tab. Generally, we want multiple spaces to be + treated as a single separator, whereas with comma and tab we + want multiple commas to return multiple (empty) fields. The + join/strip trick below effects this + """ + def __init__(self, fh): + self.fh = fh + + def close(self): + self.fh.close() + + def seek(self, arg): + self.fh.seek(arg) + + def fix(self, s): + return ' '.join(s.split()) + + + def next(self): + return self.fix(self.fh.next()) + + def __iter__(self): + for line in self.fh: + yield self.fix(line) + + if delimiter==' ': + fh = FH(fh) + + reader = csv.reader(fh, delimiter=delimiter) def process_skiprows(reader): if skiprows: for i, row in enumerate(reader): @@ -1388,9 +1426,131 @@ rows.append([func(val) for func, val in zip(converters, row)]) fh.close() + if not len(rows): + return None r = npy.rec.fromrecords(rows, names=names) return r + +def rec2csv(r, fname, delimiter=','): + """ + Save the data from numpy record array r into a comma/space/tab + delimited file. The record array dtype names will be used for + column headers. + + + fname - can be a filename or a file handle. Support for gzipped + files is automatic, if the filename ends in .gz + """ + fh = cbook.to_filehandle(fname, 'w') + writer = csv.writer(fh, delimiter=delimiter) + header = r.dtype.names + writer.writerow(header) + for row in r: + writer.writerow(map(str, row)) + fh.close() + +# some record array helpers +def rec_append_field(rec, name, arr, dtype=None): + 'return a new record array with field name populated with data from array arr' + arr = npy.asarray(arr) + if dtype is None: + dtype = arr.dtype + newdtype = npy.dtype(rec.dtype.descr + [(name, dtype)]) + newrec = npy.empty(rec.shape, dtype=newdtype) + for field in rec.dtype.fields: + newrec[field] = rec[field] + newrec[name] = arr + return newrec.view(npy.recarray) + + +def rec_drop_fields(rec, names): + 'return a new numpy record array with fields in names dropped' + + names = set(names) + Nr = len(rec) + + newdtype = npy.dtype([(name, rec.dtype[name]) for name in rec.dtype.names + if name not in names]) + + newrec = npy.empty(Nr, dtype=newdtype) + for field in newdtype.names: + newrec[field] = rec[field] + + return newrec.view(npy.recarray) + + +def rec_join(key, r1, r2): + """ + join record arrays r1 and r2 on key; key is a tuple of field + names. if r1 and r2 have equal values on all the keys in the key + tuple, then their fields will be merged into a new record array + containing the union of the fields of r1 and r2 + """ + + for name in key: + if name not in r1.dtype.names: + raise ValueError('r1 does not have key field %s'%name) + if name not in r2.dtype.names: + raise ValueError('r2 does not have key field %s'%name) + + def makekey(row): + return tuple([row[name] for name in key]) + + + names = list(r1.dtype.names) + [name for name in r2.dtype.names if name not in set(r1.dtype.names)] + + + + r1d = dict([(makekey(row),i) for i,row in enumerate(r1)]) + r2d = dict([(makekey(row),i) for i,row in enumerate(r2)]) + + r1keys = set(r1d.keys()) + r2keys = set(r2d.keys()) + + keys = r1keys & r2keys + + r1ind = [r1d[k] for k in keys] + r2ind = [r2d[k] for k in keys] + + + r1 = r1[r1ind] + r2 = r2[r2ind] + + r2 = rec_drop_fields(r2, r1.dtype.names) + + + def key_desc(name): + 'if name is a string key, use the larger size of r1 or r2 before merging' + dt1 = r1.dtype[name] + if dt1.type != npy.string_: + return (name, dt1.descr[0][1]) + + dt2 = r1.dtype[name] + assert dt2==dt1 + if dt1.num>dt2.num: + return (name, dt1.descr[0][1]) + else: + return (name, dt2.descr[0][1]) + + + + keydesc = [key_desc(name) for name in key] + + newdtype = npy.dtype(keydesc + + [desc for desc in r1.dtype.descr if desc[0] not in key ] + + [desc for desc in r2.dtype.descr if desc[0] not in key ] ) + + + newrec = npy.empty(len(r1), dtype=newdtype) + for field in r1.dtype.names: + newrec[field] = r1[field] + + for field in r2.dtype.names: + newrec[field] = r2[field] + + return newrec.view(npy.recarray) + def slopes(x,y): """ SLOPES calculate the slope y'(x) Given data vectors X and Y SLOPES Modified: trunk/matplotlib/lib/matplotlib/patches.py =================================================================== --- trunk/matplotlib/lib/matplotlib/patches.py 2007-10-15 14:03:19 UTC (rev 3948) +++ trunk/matplotlib/lib/matplotlib/patches.py 2007-10-15 20:00:54 UTC (rev 3949) @@ -11,8 +11,9 @@ import matplotlib.nxutils as nxutils import matplotlib.mlab as mlab import matplotlib.artist as artist +from matplotlib import transforms as mtrans +import agg - # these are not available for the object inspector until after the # class is build so we define an initial set here for the init # function and they will be overridden after object defn @@ -749,6 +750,32 @@ """ A scale-free ellipse """ + offset = 4.0 * (npy.sqrt(2) - 1) / 3.0 + + circle = npy.array([ + [-1.0, 0.0], + + [-1.0, offset], + [-offset, 1.0], + [0.0, 1.0], + + [offset, 1.0], + [1.0, offset], + [1.0, 0.0], + + [1.0, -offset], + [offset, -1.0], + [0.0, -1.0], + + [-offset, -1.0], + [-1.0, -offset], + [-1.0, 0.0], + + [-1.0, 0.0] + ], + npy.float_) + + def __str__(self): return "Ellipse(%d,%d;%dx%d)"%(self.center[0],self.center[1],self.width,self.height) @@ -777,14 +804,29 @@ return inside,{} def get_verts(self): - x,y = self.center - l,r = x-self.width/2.0, x+self.width/2.0 - b,t = y-self.height/2.0, y+self.height/2.0 - x,l,r = self.convert_xunits((x,l,r)) - y,b,t = self.convert_yunits((y,b,t)) - verts = ((x,y), (l,y), (x,t), (r,y), (x,b)) - return npy.array(verts, npy.float) + xcenter, ycenter = self.center + + width, height = self.width, self.height + angle = self.angle + + theta = npy.arange(0.0, 360.0, 1.0)*npy.pi/180.0 + x = width/2. * npy.cos(theta) + y = height/2. * npy.sin(theta) + + rtheta = angle*npy.pi/180. + R = npy.array([ + [npy.cos(rtheta), -npy.sin(rtheta)], + [npy.sin(rtheta), npy.cos(rtheta)], + ]) + + + x, y = npy.dot(R, npy.array([x, y])) + x += xcenter + y += ycenter + + return zip(x, y) + def draw(self, renderer): if not self.get_visible(): return #renderer.open_group('patch') @@ -803,35 +845,72 @@ if self._hatch: gc.set_hatch(self._hatch ) - tverts = self.get_transform().seq_xy_tups(self.get_verts()) - # center is first vert - # take the abs since we do not want a negative width or height as this - # will cause the renderer to misbehave. - width = abs(tverts[3,0] - tverts[1,0]) - height = abs(tverts[2,1] - tverts[4,1]) + offset = self.offset - # we also have to transform the angle, do this using polar coordinates - # convert to radians - angle = self.angle * math.pi / 180.0 + - # convert the angle to polar coordinates (Assume r = 1.0) - anglex = math.cos(angle) - angley = math.sin(angle) + if not hasattr(renderer, 'draw_path'): + verbose.report('patches.Ellipse renderer does not support path drawing; falling back on vertex approximation for nonlinear transformation') + renderer.draw_polygon(gc, rgbFace, self.get_verts()) + return + - # transform the angle vertex and the origin - angle_verts = npy.array(((anglex, angley), (0.0, 0.0)), npy.float) - angle_verts = self.get_transform().seq_xy_tups(angle_verts) + x, y = self.center + theta = self.angle * npy.pi/180. + T = npy.array([ + [1, 0, x], + [0, 1, y], + [0, 0, 1]]) - # get the new x and y coords (from the origin) - anglex = angle_verts[0, 0] - angle_verts[1, 0] - angley = angle_verts[0, 1] - angle_verts[1, 1] + S = npy.array([ + [self.width/2., 0, 0], + [0, self.height/2., 0], + [0, 0, 1]]) - # convert back to an angle (in degrees) - angle = math.atan2(angley, anglex) * 180.0 / math.pi - renderer.draw_arc(gc, rgbFace, tverts[0,0], tverts[0,1], - width, height, 0.0, 360.0, angle) + + # rotate by theta + R = npy.array([ + [npy.cos(theta), -npy.sin(theta), 0], + [npy.sin(theta), npy.cos(theta), 0], + [0, 0, 1]]) + # transform unit circle into ellipse + E = npy.dot(T, npy.dot(R, S)) + + + # Apply the display affine + sx, b, c, sy, tx, ty = self.get_transform().as_vec6_val() + + # display coords + D = npy.array([ + [sx, b, tx], + [c, sy, ty], + [0, 0, 1]], npy.float_) + + M = npy.dot(D,E) + + C = npy.ones((3, len(self.circle))) + C[0:2,:] = self.circle.T + + ellipse = npy.dot(M, C).T[:,:2] + + path = agg.path_storage() + path.move_to(*ellipse[0]) + verts = ellipse[1:4].flat + path.curve4(*verts) + verts = ellipse[4:7].flat + path.curve4(*verts) + verts = ellipse[7:10].flat + path.curve4(*verts) + verts = ellipse[10:13].flat + path.curve4(*verts) + path.close_polygon() + + renderer.draw_path(gc, rgbFace, path) + + + class Circle(Ellipse): """ A circle patch Modified: trunk/matplotlib/src/_backend_agg.cpp =================================================================== --- trunk/matplotlib/src/_backend_agg.cpp 2007-10-15 14:03:19 UTC (rev 3948) +++ trunk/matplotlib/src/_backend_agg.cpp 2007-10-15 20:00:54 UTC (rev 3949) @@ -2052,7 +2052,7 @@ theRasterizer->reset_clipping(); _VERBOSE("RendererAgg::draw_path"); - args.verify_length(4); + args.verify_length(3); GCAgg gc = GCAgg(args[0], dpi); facepair_t face = _get_rgba_face(args[1], gc.alpha); @@ -2064,43 +2064,20 @@ throw Py::TypeError("Could not convert path_storage"); - Transformation* mpltransform = static_cast<Transformation*>(args[3].ptr()); - double a, b, c, d, tx, ty; - try { - mpltransform->affine_params_api(&a, &b, &c, &d, &tx, &ty); - } - catch(...) { - throw Py::ValueError("Domain error on affine_params_api in RendererAgg::draw_path"); - } - - agg::trans_affine xytrans = agg::trans_affine(a,b,c,d,tx,ty); - double heightd = double(height); - agg::path_storage tpath; // the mpl transformed path - bool needNonlinear = mpltransform->need_nonlinear_api(); + agg::path_storage tpath; // the flipped path size_t Nx = path->total_vertices(); double x, y; unsigned cmd; bool curvy = false; for (size_t i=0; i<Nx; i++) { + + if (cmd==agg::path_cmd_curve3 || cmd==agg::path_cmd_curve4) curvy=true; cmd = path->vertex(i, &x, &y); - if (cmd==agg::path_cmd_curve3 || cmd==agg::path_cmd_curve4) curvy=true; - if (needNonlinear) - try { - mpltransform->nonlinear_only_api(&x, &y); - } - catch (...) { - throw Py::ValueError("Domain error on nonlinear_only_api in RendererAgg::draw_path"); - - } - - //use agg's transformer? - xytrans.transform(&x, &y); - y = heightd - y; //flipy - tpath.add_vertex(x,y,cmd); + tpath.add_vertex(x, heightd-y, cmd); } - + set_clipbox_rasterizer(gc.cliprect); _fill_and_stroke(tpath, gc, face, curvy); return Py::Object(); Modified: trunk/matplotlib/src/_transforms.cpp =================================================================== --- trunk/matplotlib/src/_transforms.cpp 2007-10-15 14:03:19 UTC (rev 3948) +++ trunk/matplotlib/src/_transforms.cpp 2007-10-15 20:00:54 UTC (rev 3949) @@ -1974,6 +1974,7 @@ *ty = _ty->val(); } + Py::Object Affine::as_vec6(const Py::Tuple &args) { _VERBOSE("Affine::as_vec6"); Modified: trunk/matplotlib/unit/ellipse_compare.py =================================================================== --- trunk/matplotlib/unit/ellipse_compare.py 2007-10-15 14:03:19 UTC (rev 3948) +++ trunk/matplotlib/unit/ellipse_compare.py 2007-10-15 20:00:54 UTC (rev 3949) @@ -27,27 +27,22 @@ fig = figure() ax = fig.add_subplot(211, aspect='auto') -ax.fill(x, y, alpha=0.2, facecolor='yellow') +ax.fill(x, y, alpha=0.2, facecolor='yellow', edgecolor='yellow', linewidth=1, zorder=1) e1 = patches.Ellipse((xcenter, ycenter), width, height, - angle=angle, linewidth=2, fill=False) + angle=angle, linewidth=2, fill=False, zorder=2) -ax.add_artist(e1) +ax.add_patch(e1) ax = fig.add_subplot(212, aspect='equal') -ax.fill(x, y, alpha=0.2, facecolor='yellow') +ax.fill(x, y, alpha=0.2, facecolor='green', edgecolor='green', zorder=1) e2 = patches.Ellipse((xcenter, ycenter), width, height, - angle=angle, linewidth=2, fill=False) + angle=angle, linewidth=2, fill=False, zorder=2) -ax.add_artist(e2) -ax.autoscale_view() +ax.add_patch(e2) - -ax.set_xlim(0.2, .5) -ax.set_ylim(0.3, 0.7) - #fig.savefig('ellipse_compare.png') -#fig.savefig('ellipse_compare.ps') +fig.savefig('ellipse_compare') show() This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |