""" Install instructions for traits 2.0 # blow away old enthought rm -rf ~/dev/lib/python2.4/site-packages/enthought.* # get easy_install, if necessary wget http://peak.telecommunity.com/dist/ez_setup.py sudo python sez_setup.py sudo rm -rf /usr/local/lib/python2.5/site-packages/enthought* sudo easy_install \ -f http://code.enthought.com/enstaller/eggs/source/unstable \ "enthought.resource <3.0a" "enthought.traits < 3.0a" """ # see install instructions for enthrought traits2 in mtraits import enthought.traits.api as traits from enthought.traits.api import HasTraits, Instance, Trait, Float, Int, \ Array, Tuple from enthought.traits.trait_numeric import TraitArray from matplotlib import agg from matplotlib import colors as mcolors from matplotlib import cbook import numpy as npy is_string_like = cbook.is_string_like ## begin core infrastructure class Affine(HasTraits): """ An affine 3x3 matrix that supports matrix multiplication with other Affine instances or numpy arrays. a = Affine() a.translate = 10,20 a.scale = 20, 40 Be careful not to do *inplace* operations on the array components or the update callbacks will not be triggered, eg DO NOT a.translate += 10, 20 rather DO a.translate_delta(10, 20) Multiplication works as expected: a1 = Affine() a1.scale = 10, 20 a2 = Affine() a2.scale = 4, 5 print a1*a2 x = numpy.random(3, 10) print a1*x All of the translate, scale, xlim, ylim and vec6 properties are simply views into the data matrix, and are updated by reference """ # connect to the data_modified event if you want a callback data = Array('d', (3,3)) translate = traits.Property(Array('d', (2,))) scale = traits.Property(Array('d', (2,))) vec6 = traits.Property(Array('d', (6,))) xlim = traits.Property(Array('d', (2,))) ylim = traits.Property(Array('d', (2,))) #data_modified = traits.Event def _data_default(self): return npy.array([[1,0,0],[0,1,0],[0,0,1]], npy.float_) def _get_xlim(self): sx, b, tx = self.data[0] return self._get_lim(sx, tx) def _set_xlim(self, xlim): xmin, xmax = xlim oldsx, oldb, oldtx = self.data[0] sx = 1./(xmax-xmin) tx = -xmin*sx forward = oldsx!=sx or oldtx!=tx if forward: old = self.data.copy() self.data[0][0] = sx self.data[0][-1] = tx self._data_changed(old, self.data) def _get_ylim(self): c, sy, ty = self.data[1] return self._get_lim(sy, ty) def _set_ylim(self, ylim): ymin, ymax = ylim oldc, oldsy, oldty = self.data[1] sy = 1./(ymax-ymin) ty = -ymin*sy forward = oldsy!=sy or oldty!=ty if forward: old = self.data.copy() self.data[1][1] = sy self.data[1][-1] = ty self._data_changed(old, self.data) def _get_translate(self): return [self.data[0][-1], self.data[1][-1]] def _set_translate(self, s): oldtx = self.data[0][-1] oldty = self.data[1][-1] tx, ty = s forward = tx!=oldtx or ty!=oldty if forward: old = self.data.copy() self.data[0][-1] = tx self.data[1][-1] = ty self._data_changed(old, self.data) def _get_scale(self): return [self.data[0][0], self.data[1][1]] def _set_scale(self, s): oldsx = self.data[0][0] oldsy = self.data[1][1] sx, sy = s forward = sx!=oldsx or sy!=oldsy if forward: old = self.data.copy() self.data[0][0] = sx self.data[1][1] = sy self._data_changed(old, self.data) def _get_vec6(self): a,b,tx = self.data[0] c,d,ty = self.data[1] return [a,b,c,d,tx,ty] def _set_vec6(self, v): a,b,c,d,tx,ty = v olda, oldb, oldtx = self.data[0] oldc, oldd, oldty = self.data[1] forward = a!=olda or b!=oldb or c!=oldc or d!=oldd or tx!=oldtx or ty!=oldty if forward: old = self.data.copy() self.data[0] = a,b,tx self.data[1] = c,d,ty self._data_changed(old, self.data) def _get_lim(self, s, t): lmin = -t/s lmax = 1./s + lmin return lmin, lmax def _data_changed(self, old, new): # Make it known if the translate changed oldtx, oldty = old[0][-1], old[1][-1] tx, ty = new[0][-1], new[1][-1] oldsx, oldsy = old[0][0], old[1][1] sx, sy = new[0][0], new[1][1] oldb, oldc = old[0][1], old[1][0] b, c = new[0][1], new[1][0] tchanged = False schanged = False vchanged = False tchanged = oldtx!=tx or oldty!=ty schanged = oldsx!=sx or oldsy!=sy vchanged = tchanged or schanged or b!=oldb or c!=oldc xchanged = oldtx!=tx or oldsx!=sx ychanged = oldty!=ty or oldsy!=sy if tchanged: self.trait_property_changed('translate', [oldtx, oldty], [tx, ty]) if schanged: self.trait_property_changed('scale', [oldsx, oldsy], [sx, sy]) if xchanged: oldxmin, oldxmax = self._get_lim(oldsx, oldtx) xmin, xmax = self._get_lim(sx, tx) self.trait_property_changed('xlim', [oldxmin, oldxmax], [xmin, xmax]) if ychanged: oldymin, oldymax = self._get_lim(oldsy, oldty) ymin, ymax = self._get_lim(sy, ty) self.trait_property_changed('ylim', [oldymin, oldymax], [ymin, ymax]) if vchanged: self.trait_property_changed( 'vec6', [oldsx, oldb, oldc, oldsy, oldtx, oldty], [sx, b, c, sy, tx, ty]) if tchanged or schanged or vchanged: #self._data_modified = True self.trait_property_changed('data_modified', old, new) def follow(self, othervec6): self.vec6 = othervec6 def __mul__(self, other): if isinstance(other, Affine): new = Affine() new.data = npy.dot(self.data, other.data) return new elif isinstance(other, npy.ndarray): return npy.dot(self.data, other) raise TypeError('Do not know how to multiply Affine by %s'%type(other)) def __repr__(self): return 'AFFINE: %s'%', '.join([str(val) for val in self.vec6]) #return 'AFFINE:\n%s'%self.data class Box(HasTraits): # left, bottom, width, height bounds = traits.Array('d', (4,)) left = traits.Property(Float) bottom = traits.Property(Float) width = traits.Property(Float) height = traits.Property(Float) right = traits.Property(Float) # read only top = traits.Property(Float) # read only def _bounds_default(self): return [0.0, 0.0, 1.0, 1.0] def _get_left(self): return self.bounds[0] def _set_left(self, left): oldbounds = self.bounds[:] self.bounds[0] = left self.trait_property_changed('bounds', oldbounds, self.bounds) def _get_bottom(self): return self.bounds[1] def _set_bottom(self, bottom): oldbounds = self.bounds[:] self.bounds[1] = bottom self.trait_property_changed('bounds', oldbounds, self.bounds) def _get_width(self): return self.bounds[2] def _set_width(self, width): oldbounds = self.bounds[:] self.bounds[2] = width self.trait_property_changed('bounds', oldbounds, self.bounds) def _get_height(self): return self.bounds[2] def _set_height(self, height): oldbounds = self.bounds[:] self.bounds[2] = height self.trait_property_changed('bounds', oldbounds, self.bounds) def _get_right(self): return self.left + self.width def _get_top(self): return self.bottom + self.height def _bounds_changed(self, old, new): pass ## begin custom trait handlers class TraitVertexArray(TraitArray): def __init__ ( self, typecode = None, shape = None, coerce = False ): TraitArray.__init__(self, typecode, shape, coerce) def validate(self, object, name, value): orig = value value = TraitArray.validate(self, object, name, value) if len(value.shape)!=2 or value.shape[1]!=2: return self.error(object, name, orig) return value def info(self): return 'an Nx2 array of doubles which are x,y vertices' VertexArray = Trait(npy.array([[0,0], [1,1]], npy.float_), TraitVertexArray('d')) class ColorHandler(traits.TraitHandler): """ This is a clever little traits mechanism -- users can specify the color as any mpl color, and the traited object will keep the original color, but will add a new attribute with a '_' postfix which is the color rgba tuple. Eg class C(HasTraits): facecolor = Trait('black', ColorHandler()) c = C() c.facecolor = 'red' print c.facecolor # prints red print c.facecolor_ # print (1,0,0,1) """ is_mapped = True def post_setattr(self, object, name, value): object.__dict__[ name + '_' ] = self.mapped_value( value ) def mapped_value(self, value ): if value is None: return None if is_string_like(value): value = value.lower() return mcolors.colorConverter.to_rgba(value) def validate(self, object, name, value): try: self.mapped_value(value) except ValueError: return self.error(object, name, value) else: return value def info(self): return """\ any valid matplotlib color, eg an abbreviation like 'r' for red, a full name like 'orange', a hex color like '#efefef', a grayscale intensity like '0.5', or an RGBA tuple (1,0,0,1)""" class MTraitsNamespace: DPI = Float(72.) Alpha = traits.Range(0., 1., 0.) Affine = Trait(Affine()) AntiAliased = traits.true Color = Trait('black', ColorHandler()) DPI = Float(72.) Interval = Array('d', (2,), npy.array([0.0, 1.0], npy.float_)) LineStyle = Trait('-', '--', '-.', ':', 'steps', None) LineWidth = Float(1.0) Marker = Trait(None, '.', ',', 'o', '^', 'v', '<', '>', 's', '+', 'x', 'd', 'D', '|', '_', 'h', 'H', 'p', '1', '2', '3', '4') MarkerSize = Float(6) Visible = traits.true mtraits = MTraitsNamespace() def Alias(name): return Property(lambda obj: getattr(obj, name), lambda obj, val: setattr(obj, name, val)) class IDGenerator: def __init__(self): self._id = 0 def __call__(self): _id = self._id self._id += 1 return _id ## begin backend API # PATH CODES STOP = 0 MOVETO = 1 LINETO = 2 CURVE3 = 3 CURVE4 = 4 CURVEN = 5 CATROM = 6 UBSPLINE = 7 CLOSEPOLY = 0x0F class PathPrimitive(HasTraits): """ The path is an object that talks to the backends, and is an intermediary between the high level path artists like Line and Polygon, and the backend renderer """ color = mtraits.Color('black') facecolor = mtraits.Color('blue') alpha = mtraits.Alpha(1.0) linewidth = mtraits.LineWidth(1.0) antialiased = mtraits.AntiAliased pathdata =Tuple(Array('b'), VertexArray) affine = Instance(Affine, ()) def _pathdata_default(self): return (npy.array([0,0], dtype=npy.uint8), npy.array([[0,0],[0,0]], npy.float_)) class MarkerPrimitive(HasTraits): locs = Array('d') path = Instance(PathPrimitive, ()) # marker path in points affine = Instance(Affine, ()) # transformation for the verts def _locs_default(self): return npy.array([[0,0],[0,0]], npy.float_) class Renderer(HasTraits): dpi = mtraits.DPI size = traits.Tuple(Int(600), Int(400)) adisplay = Instance(Affine, ()) pathd = traits.Dict(Int, PathPrimitive) markerd = traits.Dict(Int, MarkerPrimitive) def __init__(self, size=(600,400)): self.pathd = dict() self.markerd = dict() self._size_changed(None, size) def _size_changed(self, old, new): width, height = new # almost all renderers assume 0,0 is left, upper, so we'll flip y here by default self.adisplay.translate = 0, height self.adisplay.scale = width, -height def render_path(self, pathid): pass def new_path_primitive(self): """ return a PathPrimitive (or derived); these instances will be added and removed later through add_path and remove path """ return PathPrimitive() def new_marker_primitive(self): """ return a MarkerPrimitive (or derived); these instances will be added and removed later through add_maker and remove_marker """ return MarkerPrimitive() ## begin backend agg class PathPrimitiveAgg(PathPrimitive): def __init__(self): self._pathdata_changed(None, self.pathdata) self._facecolor_changed(None, self.facecolor) self._color_changed(None, self.color) @staticmethod def make_agg_path(pathdata): agg_path = agg.path_storage() codes, xy = pathdata Ncodes = len(codes) for i in range(Ncodes): x, y = xy[i] code = codes[i] #XXX handle other path codes here if code==MOVETO: agg_path.move_to(x, y) elif code==LINETO: agg_path.line_to(x, y) elif code==CLOSEPOLY: agg_path.close_polygon() return agg_path def _pathdata_changed(self, olddata, newdata): self.agg_path = PathPrimitiveAgg.make_agg_path(newdata) def _facecolor_changed(self, oldcolor, newcolor): self.agg_facecolor = self.color_to_rgba8(self.facecolor_) def _color_changed(self, oldcolor, newcolor): #print 'stroke color changed', newcolor c = self.color_to_rgba8(self.color_) self.agg_color = c def color_to_rgba8(self, color): if color is None: return None rgba = [int(255*c) for c in color] return agg.rgba8(*rgba) class MarkerPrimitiveAgg(MarkerPrimitive): path = Instance(PathPrimitiveAgg, ()) class RendererAgg(Renderer): gray = agg.rgba8(128,128,128,255) white = agg.rgba8(255,255,255,255) blue = agg.rgba8(0,0,255,255) black = agg.rgba8(0,0,0,0) pathd = traits.Dict(Int, PathPrimitiveAgg) markerd = traits.Dict(Int, MarkerPrimitiveAgg) def _size_changed(self, old, new): Renderer._size_changed(self, old, new) width, height = self.size stride = width*4 self.buf = buf = agg.buffer(width, height, stride) self.rbuf = rbuf = agg.rendering_buffer() rbuf.attachb(buf) self.pf = pf = agg.pixel_format_rgba(rbuf) self.rbase = rbase = agg.renderer_base_rgba(pf) rbase.clear_rgba8(self.white) # the antialiased renderers self.renderer = agg.renderer_scanline_aa_solid_rgba(rbase); self.rasterizer = agg.rasterizer_scanline_aa() self.scanline = agg.scanline_p8() self.trans = None # the aliased renderers self.rendererbin = agg.renderer_scanline_bin_solid_rgba(rbase); self.scanlinebin = agg.scanline_bin() def new_path_primitive(self): 'return a PathPrimitive (or derived)' return PathPrimitiveAgg() def new_marker_primitive(self): 'return a MarkerPrimitive (or derived)' return MarkerPrimitiveAgg() def render_path(self, pathid): path = self.pathd[pathid] if path.antialiased: renderer = self.renderer scanline = self.scanline render_scanlines = agg.render_scanlines_rgba else: renderer = self.rendererbin scanline = self.scanlinebin render_scanlines = agg.render_scanlines_bin_rgba affine = self.adisplay * path.affine #print 'display affine:\n', self.adisplay #print 'path affine:\n', path.affine #print 'product affine:\n', affine aggaffine = agg.trans_affine(*affine.vec6) transpath = agg.conv_transform_path(path.agg_path, aggaffine) if path.facecolor is not None: #print 'render path', path.facecolor, path.agg_facecolor self.rasterizer.add_path(transpath) renderer.color_rgba8( path.agg_facecolor ) render_scanlines(self.rasterizer, scanline, renderer); if path.color is not None: stroke = agg.conv_stroke_transpath(transpath) stroke.width(path.linewidth) self.rasterizer.add_path(stroke) renderer.color_rgba8( path.agg_color ) render_scanlines(self.rasterizer, scanline, renderer); def render_marker(self, markerid): marker = self.markerd[markerid] path = marker.path if path.antialiased: renderer = self.renderer scanline = self.scanline render_scanlines = agg.render_scanlines_rgba else: renderer = self.rendererbin scanline = self.scanlinebin render_scanlines = agg.render_scanlines_bin_rgba affinelocs = self.adisplay * marker.affine Nmarkers = marker.locs.shape[0] Locs = npy.ones((3, Nmarkers)) Locs[0] = marker.locs[:,0] Locs[1] = marker.locs[:,1] Locs = affinelocs * Locs dpiscale = self.dpi/72. # for some reason this is broken # this will need to be highly optimized and hooked into some # extension code using cached marker rasters as we now do in # _backend_agg pathcodes, pathxy = marker.path.pathdata pathx = dpiscale*pathxy[:,0] pathy = dpiscale*pathxy[:,1] Npath = len(pathcodes) XY = npy.ones((Npath, 2)) for xv,yv,tmp in Locs.T: XY[:,0] = (pathx + xv).astype(int) + 0.5 XY[:,1] = (pathy + yv).astype(int) + 0.5 pathdata = pathcodes, XY aggpath = PathPrimitiveAgg.make_agg_path(pathdata) if path.facecolor is not None: self.rasterizer.add_path(aggpath) renderer.color_rgba8( path.agg_facecolor ) render_scanlines(self.rasterizer, scanline, renderer); if path.color is not None: stroke = agg.conv_stroke_path(aggpath) stroke.width(path.linewidth) self.rasterizer.add_path(stroke) renderer.color_rgba8( path.agg_color ) render_scanlines(self.rasterizer, scanline, renderer); def show(self): # we'll cheat a little and use pylab for display X = npy.fromstring(self.buf.to_string(), npy.uint8) width, height = self.size X.shape = height, width, 4 if 1: import pylab fig = pylab.figure() ax = fig.add_axes([0,0,1,1], xticks=[], yticks=[], frameon=False, aspect='auto') ax.imshow(X, aspect='auto') pylab.show() class Func(HasTraits): def __call__(self, X): 'transform the numpy array with shape N,2' return X def invert(self, x, y): 'invert the point x, y' return x, y def point(self, x, y): 'transform the point x, y' return x, y class Identity(Func): def __call__(self, X): 'transform the numpy array with shape N,2' return X def invert(self, x, y): 'invert the point x, y' return x, y def point(self, x, y): 'transform the point x, y' return x, y class Polar(Func): def __call__(self, X): 'transform the numpy array with shape N,2' r = X[:,0] theta = X[:,1] x = r*npy.cos(theta) y = r*npy.sin(theta) return npy.array([x,y]).T def invert(self, x, y): 'invert the point x, y' raise NotImplementedError def point(self, x, y): 'transform the point x, y' raise NotImplementedError mtraits.Model = Instance(Func, ()) ## begin Artist layer # coordinates: # # artist model : a possibly nonlinear transformation (Func instance) # to a separable cartesian coordinate, eg for polar is takes r, # theta -> r*cos(theta), r*sin(theta) # # AxesCoords.adata : an affine 3x3 matrix that takes model output and # transforms it to axes 0,1. We are kind of stuck with the # mpl/matlab convention that 0,0 is the bottom left of the axes, # even though it contradicts pretty much every GUI layout in the # world # # AxesCoords.aview: an affine 3x3 that transforms an axesview into figure # 0,1 # # Renderer.adisplay : takes an affine 3x3 and puts figure view into display. 0, # 0 is left, top, which is the typical coordinate system of most # graphics formats primitiveID = IDGenerator() artistID = IDGenerator() class Artist(HasTraits): zorder = Float(1.0) alpha = mtraits.Alpha() visible = mtraits.Visible() adata = Instance(Affine, ()) # the data affine aview = Instance(Affine, ()) # the view affine affine = Instance(Affine, ()) # the product of the data and view affine renderer = Trait(None, Renderer) # every artist defines a string which is the name of the attr that # containers should put it into when added. Eg, an Axes is an # Aritst container, and when you place a Line in to an Axes, the # Axes will store a reference to it in the sequence ax.lines where # Line.sequence = 'lines' sequence = 'artists' def __init__(self): self.artistid = artistID() # track affine as the product of the view and the data affines # -- this should be a property, but I had trouble making a # property on my custom class affine so this is a workaround def product(ignore): # modify in place self.affine.follow((self.aview * self.adata).vec6) product(None) # force an affine product updated self.adata.on_trait_change(product, 'vec6') self.aview.on_trait_change(product, 'vec6') def _get_affine(self): return self.aview * self.adata def draw(self): pass class ArtistContainer(Artist): artistd = traits.Dict(Int, Artist) sequence = 'containers' def __init__(self): Artist.__init__(self) self.artistd = dict() def add_artist(self, artist, followdata=True, followview=True): # this is a very interesting change from matplotlib -- every # artist acts as a container that can hold other artists, and # respects zorder drawing internally. This makes zordering # much more flexibel self.artistd[artist.artistid] = artist self.__dict__.setdefault(artist.sequence, []).append(artist) artist.renderer = self.renderer self.sync_trait('renderer', artist, mutual=False) artist.followdata = followdata artist.followview = followview if followdata: # set the data affines to be the same artist.adata.follow(self.adata.vec6) self.adata.on_trait_change(artist.adata.follow, 'vec6') if followview: # set the view affines to be the same artist.aview.follow(self.aview.vec6) self.aview.on_trait_change(artist.aview.follow, 'vec6') def remove_artist(self, artist): if artist.followview: self.aview.on_trait_change(artist.aview.follow, 'vec6', remove=True) del artist.followview if artist.followdata: self.adata.on_trait_change(artist.adata.follow, 'vec6', remove=True) del artist.followdata self.sync_trait('renderer', artist, remove=True) del self.artistd[artist.artistid] self.__dict__[artist.sequence].remove(artist) def draw(self): if self.renderer is None or not self.visible: return dsu = [(artist.zorder, artist.artistid, artist) for artist in self.artistd.values()] dsu.sort() for zorder, artistid, artist in dsu: #print 'artist draw', self, artist, zorder artist.draw() class Path(Artist): """ An interface class between the higher level artists and the path primitive that needs to talk to the renderers """ _path = traits.Instance(PathPrimitive, ()) antialiased = mtraits.AntiAliased() color = mtraits.Color('blue') facecolor = mtraits.Color('yellow') linestyle = mtraits.LineStyle('-') linewidth = mtraits.LineWidth(1.0) model = mtraits.Model pathdata = traits.Tuple(Array('b'), VertexArray) sequence = 'paths' zorder = Float(1.0) # why have an extra layer separating the PathPrimitive from the # Path artist? The reasons are severalfold, but it is still not # clear if this is the better solution. Doing it this way enables # the backends to create their own derived primitves (eg # RendererAgg creates PathPrimitiveAgg, and in that class sets up # trait listeners to create agg colors and agg paths when the # PathPrimitive traits change. Another reason is that it allows # us to handle nonlinear transformation (the "model") at the top # layer w/o making the backends understand them. The current # design is create a mapping between backend primitives and # primitive artists (Path, Text, Image, etc...) and all of the # higher level Artists (Line, Polygon, Axis) will use the # primitive artitsts. So only a few artists will need to know how # to talk to the backend. The alternative is to make the backends # track and understand the primitive artists themselves. def __init__(self): """ The model is a function taking Nx2->Nx2. This is where the nonlinear transformation can be used """ Artist.__init__(self) self._pathid = primitiveID() def _pathdata_default(self): return (npy.array([0,0], dtype=npy.uint8), npy.array([[0,0],[0,0]], npy.float_)) def _update_path(self): 'sync the Path traits with the path primitive' self.sync_trait('linewidth', self._path, mutual=False) self.sync_trait('color', self._path, mutual=False) self.sync_trait('facecolor', self._path, mutual=False) self.sync_trait('antialiased', self._path, mutual=False) # sync up the path affine self._path.affine.follow(self.affine.vec6) self.affine.on_trait_change(self._path.affine.follow, 'vec6') self._update_pathdata() def _update_pathdata(self): #print 'PATH: update pathdata' codes, xy = self.pathdata #print ' PATH: shapes', codes.shape, xy.shape if self.model is not None: xy = self.model(xy) pathdata = codes, xy self._path.pathdata = pathdata def draw(self): if self.renderer is None or not self.visible: return Artist.draw(self) self.renderer.render_path(self._pathid) def _renderer_changed(self, old, new): if old is not None: del old.pathd[self._pathid] if new is None: return #print 'PATH renderer_changed; updating' self._path = renderer.new_path_primitive() new.pathd[self._pathid] = self._path self._update_path() def _model_changed(self, old, new): self._update_pathdata() def _pathdata_changed(self, old, new): #print 'PATH: pathdata changed' self._update_pathdata() class Marker(Artist): """ An interface class between the higher level artists and the marker primitive that needs to talk to the renderers """ _marker = traits.Instance(MarkerPrimitive, ()) locs = Array('d') path = Instance(Path, ()) model = mtraits.Model sequence = 'markers' size = Float(1.0) # size of the marker in points def __init__(self): """ The model is a function taking Nx2->Nx2. This is where the nonlinear transformation can be used """ Artist.__init__(self) self._markerid = primitiveID() def _locs_default(self): return npy.array([[0,1],[0,1]], npy.float_) def _path_default(self): bounds = npy.array([-0.5, -0.5, 1, 1])*self.size return Rectangle().set(bounds=bounds) def _path_changed(self, old, new): if self.renderer is None: # we can't sync up to the underlying path yet return print 'MARKER _path_changed', self.path._path.pathdata, self._marker.path.pathdata old.sync_trait('_path', self._marker, 'path', remove=True) new.sync_trait('_path', self._marker, 'path', mutual=False) def _update_marker(self): 'sync the Marker traits with the marker primitive' if self.renderer is None: # we can't sync up to the underlying path yet return # sync up the marker affine self.path.sync_trait('_path', self._marker, 'path', mutual=False) self._marker.affine.follow(self.affine.vec6) self.affine.on_trait_change(self._marker.affine.follow, 'vec6') self._update_locs() print 'MARKER _update_marker', self.path._path.pathdata, self._marker.path.pathdata def _update_locs(self): print 'MARKER: update markerdata' xy = self.locs if self.model is not None: xy = self.model(xy) self._marker.locs = xy def draw(self): if self.renderer is None or not self.visible: return Artist.draw(self) self.renderer.render_marker(self._markerid) def _renderer_changed(self, old, new): # we must make sure the contained artist gets the callback # first so we can update the path primitives properly self.path._renderer_changed(old, new) if old is not None: del old.markerd[self._markerid] if new is None: return print 'MARKER renderer_changed; updating' self._marker = renderer.new_marker_primitive() new.markerd[self._markerid] = self._marker self._update_marker() def _model_changed(self, old, new): self._update_locs() def _locs_changed(self, old, new): if len(new.shape)!=2: raise ValueError('new must be nx2 array') self._update_locs() class Line(Path): XY = Array('d') sequence = 'lines' def _facecolor_default(self): return None def _XY_default(self): return npy.array([[0,1],[0,1]], npy.float_) def _XY_changed(self): #print 'LINE: XY changed' codes = LINETO*npy.ones(len(self.XY), npy.uint8) codes[0] = MOVETO #print 'LINE shapes', codes.shape, self.XY.shape self.pathdata = codes, self.XY class Polygon(Path): zorder = Float(2.0) vertices = Array('d') sequence = 'polygons' def _vertices_default(self): return npy.array([[-1,0], [0,1], [1,0], [0,0]], npy.float_) def _vertices_changed(self, old, new): #print 'POLY: verts changed' N = len(new) codes = LINETO*npy.ones(N, npy.uint8) codes[0] = MOVETO codes[-1] = CLOSEPOLY pathdata = codes, new self.pathdata = pathdata self._pathdata_changed(None, pathdata) class Rectangle(Polygon, Box): sequence = 'rectangles' def __init__(self): Polygon.__init__(self) self._bounds_changed(None, self.bounds) def _bounds_changed(self, old, new): Box._bounds_changed(self, old, new) #print 'RECT: bounds changed' l,b,w,h = new t = b+h r = l+w self.vertices = npy.array([(l,b), (l,t), (r, t), (r, b), (0,0)], npy.float_) #XXX: do we need to otify traits change self._vertices_changed(None, self.vertices) class RegularPolygon(Polygon): center = Tuple(Float, Float) sides = Int(6) radius = Float(1.0) theta = Float(0.) # orientation in radians sequence = 'rectangles' def __init__(self): self._update_vertices() def _sides_changed(self, old, new): self._update_verts() def _theta_changed(self, old, new): self._update_verts() def _radius_changed(self, old, new): self._update_verts() def _update_verts(self): theta = 2*npy.pi/self.sides*npy.arange(self.sides) + self.theta x, y = self.center xy = npy.zeros((self.sides,2)) xy[:,0] = x + self.radius*npy.cos(theta) xy[:,1] = y + self.radius*npy.sin(theta) self.vertices = xy class Figure(ArtistContainer): rectangle = Instance(Rectangle, ()) sequence = None # figure is top level container def __init__(self): ArtistContainer.__init__(self) self.rectangle.zorder = 0 self.rectangle.facecolor = '0.75' self.rectangle.bounds = [0,0,1,1] self.add_artist(self.rectangle) class Axis(ArtistContainer): zorder = Float(1.5) tickmarker = Instance(Marker, ()) line = Instance(Line, ()) ticklocs = Array('d') ticksize = Float(7.0) loc = Float(0.) # the y location of the x-axis tickoffset = Float(0) # -1 for outer, -0.5 for centered, 0 for inner sequence = 'axes' def __init__(self): ArtistContainer.__init__(self) self.affine.on_trait_change(self._update_blended_affine, 'vec6') self.tickmarker.antialiased = False self.line.antialiased = False self.add_artist(self.line, followdata=False) self.add_artist(self.tickmarker, followdata=False) # XXX, do we have to manually call these or will they get # calle dautomagically in init self._update_tick_path() self._update_marker_locations() self._update_blended_affine() self._update_linepath() def _ticklocs_changed(self, old, new): self._update_marker_locations() def _loc_changed(self, old, new): self._update_blended_affine() def _ticksize_changed(self, old, new): self._update_tick_path() def _tickoffset_changed(self, old, new): self._update_tick_path() def _update_blended_affine(self): 'blend of xdata and y axis affine' raise NotImplementedError def _update_marker_locations(self): raise NotImplementedError def _update_tick_path(self): raise NotImplementedError def _update_linepath(self): raise NotImplementedError class XAxis(Axis): sequence = 'xaxes' def _update_blended_affine(self): 'blend of xdata and y axis affine' sx, b, tx = self.adata.data[0] a = Affine() a.vec6 = sx, b, 0, 1, tx, self.loc self.tickmarker.affine.vec6 = (self.aview * a).vec6 a = Affine() a.translate = 0, self.loc self.line.affine.vec6 = (self.aview * a).vec6 def _update_marker_locations(self): Nticks = len(self.ticklocs) locs = self.loc*npy.ones((Nticks,2)) locs[:,0] = self.ticklocs self.tickmarker.locs = locs def _update_tick_path(self): codes = MOVETO, LINETO verts = npy.array([[0., self.tickoffset], [0, self.tickoffset-1]])*self.ticksize pathdata = codes, verts self.tickmarker.path.pathdata = pathdata def _update_linepath(self): codes = MOVETO, LINETO X = npy.array([[0, 1], [0, 0]], npy.float_).T pathdata = codes, X self.line.pathdata = pathdata class YAxis(Axis): sequence = 'yaxes' def _update_blended_affine(self): 'blend of xdata and y axis affine' c, sy, ty = self.adata.data[1] a = Affine() a.vec6 = 1, 0, 0, sy, self.loc, ty self.tickmarker.affine.vec6 = (self.aview * a).vec6 a = Affine() a.translate = self.loc, 0 self.line.affine.vec6 = (self.aview * a).vec6 def _update_marker_locations(self): Nticks = len(self.ticklocs) locs = self.loc*npy.ones((Nticks,2)) locs[:,1] = self.ticklocs self.tickmarker.locs = locs def _update_tick_path(self): codes = MOVETO, LINETO verts = npy.array([[self.tickoffset,0], [self.tickoffset+1,0]])*self.ticksize pathdata = codes, verts self.tickmarker.path.pathdata = pathdata def _update_linepath(self): codes = MOVETO, LINETO X = npy.array([[0, 0], [0, 1]], npy.float_).T pathdata = codes, X self.line.pathdata = pathdata class FigurePane(ArtistContainer, Box): """ The figure pane conceptually like the matplotlib Axes, but now almost all of it's functionality is modular into the Axis and Affine instances. It is a shell of it's former self: it has a rectangle and a default x and y axis instance """ rectangle = Instance(Rectangle, ()) #gridabove = traits.false # TODO handle me xaxis = Instance(XAxis, ()) yaxis = Instance(YAxis, ()) sequence = 'panes' def __init__(self): ArtistContainer.__init__(self) self.rectangle.zorder = 0 self.rectangle.facecolor = 'white' self.rectangle.edgecolor = 'white' self.rectangle.linewidth = 0 self.rectangle.bounds = [0,0,1,1] self.add_artist(self.rectangle, followdata=False) self.add_artist(self.xaxis) self.add_artist(self.yaxis) def _bounds_changed(self, old, new): Box._bounds_changed(self, old, new) l,b,w,h = self.bounds self.aview.scale = w, h self.aview.translate = l, b ## begin examples def classic(fig): pane = FigurePane().set(bounds=[0.1, 0.1, 0.8, 0.8]) fig.add_artist(pane, followdata=False, followview=False) # update the view limits, all the affines should be automagically updated x = npy.arange(0, 10., 0.01) y = npy.sin(2*npy.pi*x) y = npy.exp(-x/2.) line1 = Line().set(XY=npy.array([x,y]).T, color='blue', linewidth=2.0, ) pane.add_artist(line1) pane.adata.xlim = 0, 10 pane.adata.ylim = -0.1, 1.1 # axis placement is still broken xax, yax = pane.xaxis, pane.yaxis xax.ticklocs = npy.arange(0., 11., 1) xax.tickoffset = 0.5 xax.loc = -0.05 xax.line.color = 'black' xax.tickmarker.path.color = 'black' yax.ticklocs = npy.arange(-1.0, 1.1, 0.2) yax.tickoffset = -0.5 yax.loc = -0.05 yax.line.color = 'black' yax.tickmarker.path.color = 'black' if 0: x = npy.arange(0, 10., 0.1) y = npy.sin(2*npy.pi*x) marker = Marker().set( locs=npy.array([x,y]).T, color='ref', linewidth=1.0, size=10) pane.add_artist(marker) if 0: xax, yax = pane.xaxis, pane.yaxis xax.ticklocs = npy.arange(0., 11., 1) xax.ticksize = 8 xax.line.color = 'black' xax.line.linewidth = 2.0 xax.tickoffset = .5 xax.tickmarker.path.color = 'black' xax.tickmarker.path.linewidth = 2 xax.loc = 0.5 xax.zorder = 10 yax.ticklocs = npy.arange(-1.0, 1.1, 0.2) yax.line.color = 'black' yax.line.linewidth = 2.0 yax.tickmarker.path.color = 'black' yax.ticksize = 8 yax.tickoffset = -0.5 yax.tickmarker.path.linewidth = 2 yax.loc = 0.5 yax.zorder = 10 if 0: # add a right and top axis; the markers are getting the loc # but the line path isn't... It appears all the line paths # are getting 0 xaxis2 = XAxis() xaxis2.loc = 0.6 xaxis2.tickoffset = -1 xaxis2.ticklocs = npy.arange(0., 10.1, 0.5) yaxis2 = YAxis().set(loc=0.6, tickoffset=-1) yaxis2.tickmarker.path.color = 'green' yaxis2.loc = 0.5 yaxis2.ticksize = 10 yaxis2.tickmarker.path.linewidth = 1 yaxis2.line.color = 'green' yaxis2.tickmarker.path.color = 'green' yaxis2.ticklocs = npy.arange(-1.0, 1.1, 0.1) pane.add_artist(xaxis2) pane.add_artist(yaxis2) # uncomment to change Axes wwidth #pane.width = 0.8 if 0: # XXX: axes lines and tick markes are placed in vastly # different locations depending on whether this is commented # or uncommented, suggesting that the problem is caused by # events not propogating unless lim are changed. If we set # these lim to be the same as the lim above (note they are # almost identical) then the graphs are the same regardless of # whether the lim are set pane.adata.xlim = 0.01, 10.01 pane.adata.ylim = -0.101, 1.101 def make_subplot_ll(fig): x1 = npy.arange(0, 10., 0.05) x2 = npy.arange(0, 10., 0.1) y1 = npy.cos(2*npy.pi*x1) y2 = 10*npy.exp(-x1) pane = FigurePane().set(bounds=[0.1, 0.1, 0.4, 0.4]) fig.add_artist(pane, followdata=False, followview=False) line1 = Line().set(X=npy.array([x1,y1]).T, color='blue', linewidth=2.0) pane.add_artist(line1) # update the view limits, all the affines should be automagically updated pane.adata.xlim = 0, 10 pane.adata.ylim = -1.1, 1.1 pane.xaxis.ticklocs = npy.arange(0., 11., 1.) pane.xaxis.loc = -0.1 pane.xaxis.tickoffset = -0.5 pane.xaxis.line.color = 'red' Pane.yaxis.ticklocs = npy.arange(-1.0, 1.1, 0.2) pane.yaxis.loc = -0.1 pane.xaxis.tickoffset = -0.5 pane.yaxis.line.color = 'blue' pane.yaxis.tickmarker.color = 'blue' # uncomment to change Axes wwidth #pane.width = 0.8 def make_subplot_ur(fig): axes2 = Axes() axes2.aview.scale = 0.4, 0.4 axes2.aview.translate = 0.55, 0.55 fig.add_artist(axes2, followdata=False, followview=False) r = npy.arange(0.0, 1.0, 0.01) theta = r*4*npy.pi line2 = Line().set(X=npy.array([r, theta]).T, model=Polar(), color='#ee8d18', linewidth=2.0) axes2.add_artist(line2) rect2 = Rectangle().set(bounds=[0,0,1,1], facecolor='#d5de9c') axes2.add_artist(rect2, followdata=False) axes2.adata.xlim = -1.1, 1.1 axes2.adata.ylim = -1.1, 1.1 if __name__=='__main__': renderer = RendererAgg() fig = Figure() fig.renderer = renderer classic(fig) #make_subplot_ll(fig) #make_subplot_ur(fig) fig.draw() renderer.show()