# vplot.py is a python module for writing vector graphic commands into # postscript files # v0.9 January 8, 2004 # Recommend you make a directory "python" in your home directory, and # put vplot.py in there. To access anywhere, set PYTHONPATH in your .bashrc,: # PYTHONPATH=$HOME/python # export PYTHONPATH import math #because we need sqrt in one of the methods below import os, sys # When a vplot.eps_class() object is created, the __init__ method is invoked. # The Bounding Box is set, an output file is opened, a header is written. # Default scale factors are then set by invoking "scale". class eps_class: def __init__(self,fname='/tmp/temp.eps',bbx=512,bby=512): self.bbx=bbx self.bby=bby if fname=="-": self.fname="STDOUT" self.eps=sys.stdout else: self.fname=fname self.eps=open(self.fname,'w') self.scale() #scale method is invoked, setting defaults, but it can be called again #Next we write out the header, with our values inserted in the BoundingBox statement #Notice the """ surrounding multi-line strings. #The `int(bbx)` converts bbx into an integer and then the ` ` converts it into a string self.eps.write("""%!PS-Adobe-2.0 EPSF-2.0 %%Creator: vplot.py %%DocumentFonts: Geneva %%BoundingBox: 0 0 """+`int(bbx)`+' '+`int(bby)`+""" %%EndComments /fontsize 12 def /csize {1 mul} def /Geneva fontsize selectfont /cshift fontsize neg def /vshift fontsize -2 div def /L {lineto} bind def /M {moveto} bind def /S {stroke} bind def /CS {closepath stroke} bind def /RP {reversepath} bind def /F {closepath fill} bind def /X {currentpoint stroke moveto} bind def /N {newpath} bind def /C {setrgbcolor} bind def /R {rmoveto} bind def /V {rlineto} bind def /C {setrgbcolor} bind def /Cshow { currentpoint S M dup stringwidth pop -2 div cshift R 0 -2 R show } def /Rshow { dup stringwidth pop neg vshift R -4 2 R show } def 1 setlinewidth %%EndProlog """) def close(self): #print "The file ",self.fname," was successfully written and closed by vplot" sys.stderr.write("The file %s was successfully written and closed by vplot\n" % (self.fname)) self.eps.close() # METHODS FOR COORDINATES AND COORDINATE CONVERSION. The postscript eps files will # use coordinates (think "i" and "j") measured in units of "points", or pts, # which is 1/72 of an inch. The origin for the postscript files is the extreme lower left. # User coordinates, (think "x" and "y") are linearly related to postscript coordinates, # The origin starts at the lower-left margins. It is expected that things are easier to # plot in user coordinates. A coordinate passed in as a real number is interpreted # as a user coordinate. A coordinate passed in as an integer is interpreted as a # postscript coordinate. But there are two other forms of "user" coordinates that can also # be passed in. Imajinary numbers are fractions of the bounding box, the acceptable # being 0.0j to 1.0j. Long integers are hi-resolution postscript coordinates, # essentially the postscript coordinates multiplied by 100. def scale(self,xmin=0.,xmax=1.,ymin=0.,ymax=1., #sets the user coordinates leftmarg=50,rightmarg=50,botmarg=50,topmarg=50): self.xmin=xmin self.xmax=xmax self.ymin=ymin self.ymax=ymax self.leftmarg=leftmarg self.rightmarg=rightmarg self.botmarg=botmarg self.topmarg=topmarg self.xscale=float(self.bbx-self.leftmarg-self.rightmarg)/(self.xmax-self.xmin) self.yscale=float(self.bby-self.botmarg -self.topmarg )/(self.ymax-self.ymin) #the following are generally called just before writing the coordinate to the file: def ix(self,x): #postscript "i" coordinate as function of various types of user "x" if isinstance(x,float): return self.leftmarg+(x-self.xmin)*self.xscale elif isinstance(x,complex): return x.imag*self.bbx elif isinstance(x,long): return x*.01 else: return x def jy(self,y): #postscript "j" coordinate as function of various types of user "y" if isinstance(y,float): return self.botmarg+(y-self.ymin)*self.yscale elif isinstance(y,complex): return y.imag*self.bby elif isinstance(y,long): return y*.01 else: return y #sizes of things are scaled a bit differently from a postion of a thing. def sx(self,x): #pt size for fonts, ticks, radius, etc., as function of user "x" size if isinstance(x,float): return x*self.xscale elif isinstance(x,complex): return x.imag*self.bbx elif isinstance(x,long): return x*.01 else: return x def sy(self,y): #pt size for fonts, ticks, radius, etc., as function of user "y" size if isinstance(y,float): return y*self.yscale elif isinstance(y,complex): return y.imag*self.bby elif isinstance(y,long): return y*.01 else: return y def ijxy(self,a): #converts an x,y pair to postscript i,j pairs return self.ix(a[0]),self.iy(a[1]) #COLOR AND WIDTH METHODS # def color(self,red,green,blue): # self.eps.write( "%6.3f %6.3f %6.3f C\n" % (red,green,blue) ) def color(self,*colors): if bool(colors): f=colors[0] if isinstance(f,list) or isinstance(f,tuple): r,g,b=f elif isinstance(f,str): if f=="red": r,g,b=1,0,0 elif f=="green": r,g,b=0,1,0 elif f=="blue": r,g,b=0,0,1 else: r,g,b=0,0,0 else: r,g,b=colors else: r,g,b=0,0,0 self.eps.write( "%6.3f %6.3f %6.3f C\n" % (r,g,b) ) def linewidth(self,width=1): self.eps.write("%6.2f setlinewidth\n" % (self.sx(width))) #SIMPLE DRAWING #Some simple, and useful drawing methods. def moveto(self,x,y): #move to, without drawing self.eps.write("N %8.2f %8.2f M\n"% (self.ix(x),self.jy(y))) def lineto(self,x,y): #draw line to, from current point self.eps.write("%8.2f %8.2f L X\n"% (self.ix(x),self.jy(y))) def line(self,x1,y1,x2,y2): #draw line between points self.moveto(x1,y1) self.eps.write("%8.2f %8.2f L S\n"% (self.ix(x2),self.jy(y2))) def linetos(self,alist): #connects a list of points #alist may be a list of tuples x,y=pop2(alist) self.moveto(x,y) while len(alist) > 0: x,y=pop2(alist) self.eps.write("%8.2f %8.2f L\n" % (self.ix(x),self.jy(y))) #note the stroke, connect, or fill command is not yet called def draw(self,alist):#draws a path connecting the list of points self.linetos(alist) self.eps.write("S\n") def dashdraw(self,alist): #like draw, but dashed self.eps.write("[3 2] 0 setdash\n") self.draw(alist) self.eps.write("[] 0 setdash\n") #the followings accept an optional trailing argument 'F', to fill def poly(self,alist,*tags): #like draw, but closes path self.linetos(alist) self.eps.write("%s\n" % (detag('CS',tags))) def rect(self,x1,y1,x2,y2,*tags): self.poly([x1,y1,x2,y1,x2,y2,x1,y2],detag('',tags)) #CIRCLES, both accept optional trailing argument 'F', for fill #circle with center at x,y with radius r : def circle(self,x,y,r,*tags): self.eps.write("N %8.2f %8.2f %8.2f csize 0 360 arc %s\n" % (self.ix(x), self.jy(y), self.sx(r), detag('CS',tags))) #sector with center at user (x,y), but radius r1 and r2 are in pts: def sector(self,x,y,r1,r2,a1,a2,*tags): self.eps.write("N %8.2f %8.2f %8.2f csize %8.2f %8.2f arc RP\n" % (self.ix(x),self.jy(y),self.sx(r1),a1,a2)) self.eps.write(" %8.2f %8.2f %8.2f csize %8.2f %8.2f arc %s\n" % (self.ix(x),self.jy(y),self.sx(r2),a1,a2,detag('CS',tags))) #TEXT PLACEMENT def text(self,x,y,angle,size,text): self.moveto(x,y) self.eps.write( "gsave /Geneva %d selectfont\n" % self.sx(size)) self.eps.write( "%7.2f rotate\n" % angle ) self.eps.write( "("+text+") show grestore\n") #COMPOSITE DRAWING def arrow(self,x1,y1,x2,y2,headsize): #headsize is in pts i1,j1,i2,j2=self.ix(x1),self.jy(y1),self.ix(x2),self.jy(y2) headsize=self.sx(headsize) self.line(x1,y1,x2,y2) r=math.sqrt((i2-i1)**2+(j2-j1)**2) u=(i2-i1)/r v=(j2-j1)/r ai=-.8*u-.6*v aj=.6*u-.8*v self.line(lng(i2),lng(j2),lng(i2+headsize*ai),lng(j2+headsize*aj)) ai=-.8*u+.6*v aj=-.6*u-.8*v self.line(lng(i2),lng(j2),lng(i2+headsize*ai),lng(j2+headsize*aj)) def fatarrow(self,x1,y1,x2,y2,asize): #asize is the half-width of the fat arrow i1,j1,i2,j2=self.ix(x1),self.jy(y1),self.ix(x2),self.jy(y2) asize=self.sx(asize) r=math.sqrt((i2-i1)**2+(j2-j1)**2) u=asize*(i2-i1)/r v=asize*(j2-j1)/r self.poly(map(lng,[ i1+v,j1-u, i2+v-u,j2-u-v, i2,j2, i2-v-u,j2+u-v, i1-v,j1+u]),'F') #AXES DRAWING #If you don't use the derfaults, you should call these using your user coordinates only, #except for ticklen which can be passed as an integer def xaxis(self, y="", #where to intersect the y-axis x1="", #smallest x dx="", #increment for tick marks x2="", #largest x ticklen=10, #length of ticks, in pts form='%5.1f'): #format string for numerical labels if y=="": y=self.ymin if x1=="": x1=self.xmin if x2=="": x2=self.xmax if dx=="": dx=(self.xmax-self.xmin)*.1 y,x1,x2,dx=map(float,[y,x1,x2,dx]) ticklen=self.sy(ticklen) self.line(x1,y,x2,y) x=x1 while x < x2*1.00001: #make tick marks str=form % x self.line(x,y,x,lng(self.jy(y)+ticklen)) self.cshow(x,y,str) #label tick marks x=x+dx def yaxis(self, x="", #where to intersect the x-axis y1="", #smallest y dy="", #increment for tick marks y2="", #largest y ticklen=10, #length of ticks, in pts form='%5.1f'): #format for numerical labels if x=="": x=self.xmin if y1=="": y1=self.ymin if y2=="": y2=self.ymax if dy=="": dy=(self.ymax-self.ymin)*.1 x,y1,y2,dy=map(float,[x,y1,y2,dy]) ticklen=self.sx(ticklen) self.line(x,y1,x,y2) y=y1 while y < y2*1.00001: #make tick marks str=form % y self.line(x,y,lng(self.ix(x)+ticklen),y) self.rshow(x,y,str) #label tick marks y=y+dy def cshow(self,x,y,text): #used for numerical labels on x-axis tick marks self.moveto(x,y) self.eps.write("("+text+") Cshow\n") def rshow(self,x,y,text): #used for numerical labels on y-axis tick marks self.moveto(x,y) self.eps.write("("+text+") Rshow\n") ### some functions independent of eps_class, used internally def detag(default,tags): #overides default tag tag=default if tags and tags[0]: tag=tags[0] return tag def lng(x): #converts postscript (pts) coordinates to hi-res coordinate type return long(100*x) def pop2(alist): #gets next 2 elements in list, even if they are found in a tuple x=alist.pop(0) if isinstance(x,tuple) or isinstance(x,list): x,y=x else: y=alist.pop(0) return x,y ############## # A simple test program, which can be invoked as vplot.tester() from your # python command line: def tester(): import vplot print "A sample plot will be output as tester.eps" #this example makes a Japanese flag #the next line creates or "instantiates" an object vplot.eps_class a=vplot.eps_class(fname="tester.eps") # argument fname is passed, overides default 'temp.eps' #next we invoke the methods of our object "a". The methods write out postscript commands. a.color('red') #changes colors to red a.circle(.5,.5,100,'F') #filled circle at user coordinate (.5,.5) with radius=100 pts a.color() #change color back to black (default) a.rect(0.,.15,1.,.85) #draws a rectangle a.close() #close the output file # The python tradition is that the test program is called if this module is invoked by itself, # meaning if you type on your command line: python vplot.py if __name__ == '__main__': tester()