#
# Python SVG Library
# coded by Kenar (Kenji Arisawa)
# email: arisawa@aichi-u.ac.jp
#
import copy
import math
import re
import sys
from base import *
version = "0.4"
# style properties:
# font,fill,direction,letter-spacing,text-decoration,unicode-bidi,word-spacing,clip,color,
# cursor,overflow,visibility,
# clip-path,clip-rule,mask,opacy,enable-background,filter,flood-color,flood-opacity,lighting-color,
# stop-color,stop-opacity,pointing-events,color-interpolation,color-interpolation-filters,
# color-profile,color-rendering,fill-opacity,fill-rule,image-rendering,
# marker,marker-end,marker-start,shape-rendering,
# stroke,stroke-width,
# stroke-dasharray,stroke-dashoffset,stroke-line-cap,stroke-linejoin,stroke-miterlimit,stroke-opacity,
# text-anchor,writing-mode
# text-rendering,alignment-baseline,baseline-shift,dominant-baseline,
# glyph-orientation-horizontal,glyph-orientation-vertical,kerning
# ref: http://www.hcn.zaq.ne.jp/___/REC-SVG11-20030114/styling.html#SVGStylingProperties
# our default values: markerUnits="strokeWidth", fill="none", orient="auto"
markers = {
"triangle":
"""""",
"arrow":
"""""",
"circle":
"""""",
}
symbols = {
# symbols defined using is spinous
"circle":
'',
"box":
'',
"cross":
'',
"plus":
''
}
# example "MyCircle":''
# example "MyCircle":''
patterns = {
# NB: designe patterns is in box of width=100, height=100 so that we prevent jaggy
# stroke-width must be in
"cross":
"""""",
"grid":
"""""",
# diagonal is a bit buggy
"diagonal1":
"""""",
"diagonal2":
"""""",
"vertical":
"""""",
"horizontal":
"""""",
}
filters = {
"shadow": # ref: http://www.h2.dion.ne.jp/~defghi/svgMemo/svgMemo_11.htm
""" """
}
dic = {"marker-unit":"markerUnit", "marker-width":"markerWidth", "marker-height":"markerHeight"}
exceptions = {"font":"font-family","href":"xlink:href","base":"xml:base","lang":"xml:lang","space":"xml:space"}
gid = 0 # group ID for preventing duplicated gid
tag_p = None # tag pattern used in tagname()
opt_p = None
def str2dic(str):
# convert style string to a dictionary
# str is "XX1:YY1;XX2:YY2;" or "YY3;XX4:YY4;"
# then return {"XX1":"YY1","XX2":"YY2"} or {"":"YY3","XX4":"YY4"}
d = {}
u = str.split(";")
for x in u:
x = x.strip()
if x == "": continue
r = x.split(":")
if len(r) > 2:
raise Exception("str2dic: too many ':' in %s"%x)
r[0] = r[0].strip()
if len(r) == 1:
d[""] = r[0]
else:
d[r[0]] = r[1].strip()
return d
def str2ndic(str,attr):
# convert attr value to a normalized dictionary
# str is "XX1:YY1;XX2:YY2;" or "YY3;XX4:YY4;"
# attr is "AAA" (not "-" in attr)
# then return {"AAA-XX1":"YY1","AAA-XX2":"YY2"} or {"AAA":"YY3","AAA-XX4":"YY4"}
# this is inapplicable to style string
d = {}
u = str.split(";")
for x in u:
x = x.strip()
if x == "": continue
r = x.split(":")
if len(r) > 2:
raise Exception("str2dic: too many ':' in %s"%x)
r[0] = r[0].strip()
if len(r) == 1:
d[attr] = r[0]
else:
d[attr + "-" + r[0]] = r[1].strip()
return d
def keymatch(key,*keylist):
for k in keylist:
if k == key[0:len(k)]:
return True
return False
def dic2str(dic,*keylist):
# dictionary to string value for style
s = ""
for key in dic:
if keymatch(key,*keylist):
s = s + "%s:%s;"%(key,dic[key])
return s
def dic2opt(dic,*keylist):
# dictionary to string value for options (i.e, attr="value" style)
s = ""
for key in dic:
if keymatch(key,*keylist):
s = s + '%s="%s" '%(key,dic[key])
return s
def dic2ndic(dic):
# convert dic to normalized dictionary
# {"font":"Helvetica"} should be converted to {"font-family":"Helvetica"}
# {"animate":"<....>"} should be untouched
dd = {}
for key in dic:
d = dic[key]
if type(d) == type(""):
if key == "style":
d = str2dic(d)
elif key == "animate":
d = {key:d}
else:
d = str2ndic(d,key)
dd = merge_dic(dd,d)
else:
dd[key] = d
for key in exceptions:
if key in dd:
dd[exceptions[key]] = dd.pop(key)
return dd
def merge_dic(db1,db2):
# merge dictionary db2 into db1
# dictionary must be normalized
# db2 override db1
# produce normalized dictionary
for k in db2:
db1[k] = db2[k]
return db1
def mdelkey(dic,mkey):
# remove matched keys from the dictionary
m = len(mkey)
d = dic.copy()
for k in dic:
if k[0:m] == mkey:
del d[k]
return d
def default(db,de):
# db override de
ndb = dic2ndic(db)
nde = dic2ndic(de)
return merge_dic(nde,ndb)
def tagname(s):
global tag_p
if tag_p == None:
tag_p = re.compile("<([a-zA-Z]+)")
m = tag_p.match(s)
return m.group(1)
def opt2dic(s): # convert option part in tag to dictionary
global opt_p
dic = {}
def func(mo): # mo is match object
# print mo.group(0)
att = mo.group(1)
val = mo.group(2)
dic[att] = val
if opt_p == None:
opt_p = re.compile(' *([a-zA-Z][a-zA-Z-]*)="([^"]*)"')
opt_p.sub(func,s)
return dic
def dexml(s):
# decompose xml to (TAG,OPTS,STR)
# "STR" ---> (TAG,OPTS,STR)
# "" ---> (TAG,OPTS,None)
tag = tagname(s)
s = s[len(tag)+1:]
stop = s.find(">")
if s[stop-1] == "/":
return (tag,s[:stop-1],None)
opt = s[:stop]
s = s[stop+1:]
stop = s.find("%s>"%tag)
return (tag,opt,s[:stop])
def xmlesc(s):
# & to &
# < to <
# > to >
s = s.replace("&","&")
s = s.replace("<","<")
s = s.replace(">",">")
return s
class Canvas:
(oX,oY) = (0,0)
(sX,sY) = (1,1)
mapBox = viewBox = (0,0,None,None)
marker_no = 0
patt_no = 0
symbol_no = 0
filter_no = 0
gid = 0 # group id for the group. Canvas is root group, the id is 0
out = [] # output buffer. the elements are strings
child = [] # list of child group
parent = None
def __init__(self,x1,y1,x2,y2,**kd):
kd = default(kd,{"font-family":"HelveticaNeue","fill":"none","stroke":"black","stroke-width":1})
self.mapBox = self.viewBox = (x1,y1,x2,y2)
if kd and "map" in kd:
m = kd.pop("map")
self.map(m[0],m[1],m[2],m[3])
opt = dic2opt(kd,"")
self.svgout("""\n"""
+ """\n"""%version
+ """\n")
f.close()
def map(self, *t):
(x1,y1,x2,y2) = expand(*t)
self.mapBox = (x1,y1,x2,y2)
self.sX = 1.0*(self.viewBox[2] - self.viewBox[0])/(x2-x1)
self.sY = 1.0*(self.viewBox[3] - self.viewBox[1])/(y2-y1)
self.oX = self.viewBox[0] - self.sX*x1
self.oY = self.viewBox[1] - self.sY*y1
def xyconv(self,*t):
(x,y) = expand(*t)
if isnum(x): x = x*self.sX
if isnum(y): y = y*self.sY
return x,y
def svgxy(self,*t):
t = expand(*t)
r = ()
for n in range(0,len(t),2):
r = r + (self.oX+t[n]*self.sX,self.oY+t[n+1]*self.sY)
return r
def pathxyconv(self,*z):
# z[-1] must be a string
flag = None # move flag
if z[-1] in "ltmsqchva":
flag = True
if z[-1] in "LTMSQCHVA":
flag = False
z = z[0:-1]
if len(z) == 1:
z = z[0]
m = len(z)
u = [None]*m
for n in range(0,m,2):
x,y = self.xyconv(float(z[n]),float(z[n+1]))
u[n],u[n+1] = str(x),str(y)
return u
def svgpath(self,s):
# translate Python SVG path to SVG path
t = s.split()
m = len(t)
v = []
n = 0
while n < m:
key = t[n]
if key[0] in "-0123456789":
key = key0
else:
v = v + [key]
n = n + 1
if key in "LTMltm":
v = v + self.pathxyconv(t[n:n+2],key); n = n + 2
elif key in "SQsq":
v = v + self.pathxyconv(t[n:n+4],key); n = n + 4
elif key in "Cc":
v = v + self.pathxyconv(t[n:n+6],key); n = n + 6
elif key in "Hh":
v = v + [self.pathxyconv(t[n],0,key)[0]]; n = n + 1
elif key in "Vv":
v = v + [self.pathxyconv(0,t[n],key)[1]]; n = n + 1
elif key in "Aa":
v = v + self.pathxyconv(t[n:n+2],"a")
v = v + t[n+2:n+5]
v = v + self.pathxyconv(t[n+5:n+7],key)
n = n + 7
key0 = key
s = " ".join(v)
return s
def xtransform(self,**kd):
# transform the args such as translate=(x,y), rotate=a, scale=(sx,sy)
# to transform="translate=(x,y) rotate=a scale=(sx,sy)"
t = ""
if "x" in kd and "y" in kd:
t = t + "translate(%s,%s) "%(kd.pop("x"),kd.pop("y"))
if "rotate" in kd:
ro = kd.pop("rotate")
t = t + "rotate(%s) "%ro
if "scale" in kd:
sc = kd.pop("scale")
if isnum(sc):
sc = (sc,sc)
t = t + "scale(%s,%s) "%sc
if t != "":
if "transform" in kd:
kd["transform"] = t + kd["transform"]
else:
kd["transform"] = t
return kd
def view(self,*t,**kd):
(x1,y1,x2,y2) = expand(*t)
kd = dic2ndic(kd)
opt = dic2opt(kd,"")
if x2 < x1:
x1,x2 = x2,x1
if y2 < y1:
y1,y2 = y2,y1
self.svgout("view",'viewBox="%s %s %s %s" %s'%(x1,y1,x2-x1,y2-y1,opt))
# ===== drawing elements ======
def path(self,s,**kd):
kd = dic2ndic(kd)
kd = self.xtransform(**kd)
s = self.svgpath(s)
animate = None
if "animate" in kd:
animate = kd.pop("animate")
opt = dic2opt(kd,"")
self.svgout("path",'d="%s" %s'%(s,opt),animate)
def cplot2(self,P,DP,**kd):
path = "M %s %s"%tuple(P[0])
for n in range(0,len(DP)-1):
P0,Q0 = P[n],cal(P[n],DP[n],3,"/","+")
P1,Q1 = P[n+1],cal(P[n+1],DP[n+1],-3,"/","+")
path = path + " C %s %s %s %s %s %s"%(expand(Q0,Q1,P1))
self.path(path,**kd)
def interpolation(self,*q,**kd): # Overhauser (Catmull-Rom) Spline
P = points(*q)
kd.pop("smooth")
g = 1.0/2 # value by Catmull-Rom
if "smooth-looseness" in kd:
g = float(kd.pop("smooth-looseness"))/2
DP = [] # list of tangential vectors
for n in range(1,len(P)-1):
DP.append(cal(P[n+1],P[n-1],"-",g,"*"))
self.cplot2(P[1:],DP,**kd)
def interpolation2(self,*q,**kd): # Arisawa Spline
exclude = None
if "exclude" in kd:
exclude = kd.pop("exclude")
P = points(*q)
# R(t) is k0(t)*P0 + k1(t)*P1 + k2(t)*P2 + k3(t)*P3 (where t is in range [0,4])
# k0(t) = (1/6)*(1-t)*(2-t)*(3-t)
# k1(t) = (1/2)*t*(2-t)*(3-t)
# k2(t) = -(1/2)*t*(1-t)*(3-t)
# k3(t) = (1/6)*t*(1-t)*(2-t)
P0,P1,P2,P3 = tuple(P[0:4])
# D0,D1,D2,D3 are derivatives of R(t) at t=0,1,2,3
# D0 = P3/3-3*P2/2+3*P1-11*P0/6
D0 = cal(1.0/3,P3,"*",-3.0/2,P2,"*","+",3,P1,"*","+",-11.0/6,P0,"*","+")
# D1 = -P3/6+P2-P1/2-P0/3 # D1 is derivative of R(t) at t=1
D1 = cal(-1.0/6,P3,"*",P2,"+",1.0/2,P1,"*","-",1.0/3,P0,"*","-")
# D2 = P3/3+P2/2-P1+P0/6 # D2 is derivative of R(t) at t=2
D2 = cal(1.0/3,P3,"*",1.0/2,P2,"*","+",P1,"-",1.0/6,P0,"*","+")
DP = [D0,D1]
n = len(P)
P0,P1,P2,P3 = tuple(P[n-4:n])
# D3 = 11*P3/6-3*P2+3*P1/2-P0/3
# DL is derivative of the last point
DL = cal(11.0/6,P3,"*",-3,P2,"*","+",3.0/2,P1,"*","+",-1.0/3,P0,"*","+")
b = D2
for n in range(1,len(P)-3):
P0,P1,P2,P3 = tuple(P[n:n+4])
D1 = cal(-1.0/6,P3,"*",P2,"+",1.0/2,P1,"*","-",1.0/3,P0,"*","-")
D2 = cal(1.0/3,P3,"*",1.0/2,P2,"*","+",P1,"-",1.0/6,P0,"*","+")
DP.append(cal(b,D1,"+",2,"/"))
b = D2
DP.append(b)
DP.append(DL)
if exclude in ("start","both"):
P,DP = P[1:],DP[1:]
if exclude in ("end","both"):
P,DP = P[:-1],DP[:-1]
self.cplot2(P,DP,**kd)
def smoothline(self,*t,**kd): # internal use only
t = expand(*t)
sm = ["L","S","Q"]
c = sm[kd.pop("smooth")]
s = "M %s %s %s %s %s"%(t[0],t[1],c,t[2],t[3])
for n in range(4,len(t)-2,2):
s = s + " %s %s %s %s %s"%((t[n-2]+t[n])/2.0,(t[n-1]+t[n+1])/2.0,c,t[n],t[n+1])
if len(t)%2 == 0:
s = s + " %s %s"%(t[n+2],t[n+3])
else:
s = s + " %s %s"%(t[n],t[n+1])
self.path(s,**kd)
def mark(self,*t,**kd):
t = expand(*t)
kd = dic2ndic(kd)
kd = self.xtransform(**kd)
m = len(t)
label = None
if "label" in kd:
label = kd.pop("label")
if type(label) == type(""): label=(label,)
if "stroke" in kd: kd.pop("stroke")
anchor = pad = dx = dy = None
if "anchor" in kd:
anchor = tolist(kd.pop("anchor"))
if "pad" in kd:
pad = tolist(kd.pop("pad"))
if "dx" in kd:
dx = tolist(kd.pop("dx"))
if "dy" in kd:
dy = tolist(kd.pop("dy"))
kd0 = kd.copy()
for n in range(0,len(t),2):
if anchor: kd0["anchor"] = anchor[n//2 % len(anchor)]
if pad: kd0["pad"] = pad[n//2 % len(pad)]
if dx: kd0["dx"] = dx[n//2 % len(dx)]
if dy: kd0["dy"] = dy[n//2 % len(dy)]
self.text(t[n],t[n+1],text=label[n//2],**kd0)
kd = mdelkey(kd,"font")
for n in range(0,len(t),2):
self.use(t[n],t[n+1],**kd)
def line(self,*t,**kd):
t = expand(*t)
kd = dic2ndic(kd)
kd = self.xtransform(**kd)
m = len(t)
if "smooth" in kd:
smooth = kd["smooth"]
if smooth == "I":
self.interpolation(*t,**kd)
return
if smooth == "I2":
self.interpolation2(*t,**kd)
return
self.smoothline(*t,**kd)
return
s = ""
for n in range(0,len(t),2):
z = self.xyconv(t[n],t[n+1])
s = s + "%s,%s "%(z[0],z[1])
s = s[:-1]
animate = None
if "animate" in kd:
animate = kd.pop("animate")
opt = dic2opt(kd,"")
self.svgout("polyline", 'points="%s" %s'%(s,opt), animate)
def polygon(self,*t,**kd):
t = expand(*t)
kd = dic2ndic(kd)
kd = self.xtransform(**kd)
t = expand(*t)
m = len(t)
t = self.svgxy(t)
s = ""
for n in range(0,len(t),2):
s = s + "%s,%s "%(t[n],t[n + 1])
s = s[:-1]
animate = None
if "animate" in kd:
animate = kd.pop("animate")
opt = dic2opt(kd,"")
self.svgout("polygon", 'points="%s" %s'%(s,opt),animate)
def textpath(self,**kd):
kd = self.xtransform(**kd)
s = ('\n'%kd.pop("ref")
+ kd.pop("text") + '\n'
+ ''
)
opt = dic2opt(kd,"")
self.svgout("text", opt, s)
def textarea(self,*t,**kd): # internal use only
# currently, textArea support is very limited: only by Opera (2012/12/25)
# we assume *t is expanded
# borderline should be supported
# then pad should be also supported; (left,top,right,bottom) oder?
# In SVG Tiny 1.2 Specification, height="auto" is also possible. Look the Spec. for more details
text = kd.pop("text")
kd = default(kd,{"fill":"black","stroke":"none","baseline":"inherit"})
#kd = dic2ndic(kd)
kd = self.xtransform(**kd)
if len(t) == 4:
(x1,y1,x2,y2) = expand(*t)
(x,y,w,h) = self.xywh(x1,y1,x2,y2)
else:
(x1,y1) = expand(*t)
(x,y) = self.xyconv(x1,y1)
w = kd.pop("width")
h = kd.pop("height")
(w,h) = self.xyconv(w,h)
(kd['x'],kd["y"],kd["width"],kd["height"]) = (x,y,w,h)
opt = dic2opt(kd,"")
self.svgout("textArea",
'xml:space="preserve" %s'%opt,text)
def text(self,*t,**kd):
t = expand(*t)
text = kd.pop("text")
decoration = False
if "decoration" in kd:
decoration = kd.pop("decoration")
if decoration == False:
text = xmlesc(text)
kd = default(kd,{"fill":"black","stroke":"none","baseline":"inherit"})
if len(t) == 0:
kd = self.xtransform(**kd)
kd["text"] = text
self.textpath(**kd)
return
(kd["x"],kd["y"]) = self.xyconv(t[0],t[1])
kd = self.xtransform(**kd)
# it seems there are no way to handle " " in kd["text"]
# is not effective.
# " " is not allowed.
# "
....
" is not allowed.
# there are many issues on this problem in the net
d = {}
b = 0
fsize = 1.0 # ratio to default font height that is given by "em"
# we should not apply padding to baseline
padx = pady = 0
if "pad" in kd:
padx = pady = fsize * kd.pop("pad")
if "baseline" in kd:
d["baseline"] = kd["baseline"]
if "anchor" in kd:
anchor = kd.pop("anchor")
if "E" in anchor:
d["text-anchor"] = "end"
padx = -padx
if "W" in anchor:
d["text-anchor"] = "start"
if anchor in "CNS":
d["text-anchor"] = "middle"
padx = 0
if anchor in ("start","middle","end"):
d["text-anchor"] = anchor
if "N" in anchor:
b = fsize
if anchor in "CWE":
b = fsize/2
pady = 0
if "S" in anchor:
pady = -pady
if "baseline" in d:
d["alignment-baseline"] = d.pop("baseline")
s = text
if "animate" in kd:
s = s + kd.pop("animate")
if decoration and decoration != True:
s = ''%decoration + s + ''
# if we have transform data such as rotate, we must put x,y into transform="translate(x,y)"
# and remove x="x",y="y"
if "dx" in kd:
padx = float(kd.pop("dx"))
if "dy" in kd:
pady = float(kd.pop("dy"))
op = dic2opt(d,"") + " " + dic2opt(kd,"")
self.svgout("text",
'xml:space="preserve" dx="%sem" dy="%sem" %s'%(padx,0.7*b+pady,op),
s)
# we have alignment-baseline and dominant-baseline. I don't know these usage.
# alignment-baseline: auto,baseline,before-edge,text-before-edge,middle,central,after-edge,
# text-after-edge,ideographic,alphabetic,hanging,mathematical,inherit
# dominant-baseline: auto,use-script,no-change,reset-size,ideographic,alphabetic,hanging,
# mathematical,central,middle,text-after-edge,text-before-edge,inherit
# baseline is not effective to some browsers
#
# font style is
# style="font-family:Helvetica;font-size:20"
# note that style is not effective to some browsers
#
# http://nelsonslog.wordpress.com/2011/09/12/svgtext-baseline-considered-harmful/
# dominant-baseline="alphabetic"
# dy="0.0em" # S
# dy="0.35em" # C
# dy="0.7em" # N
def circle(self,*t,**kd):
(x,y) = expand(*t)
kd = dic2ndic(kd)
kd = self.xtransform(**kd)
r = kd.pop("r")
if isnum(r):
rx = ry = r
else:
(rx,ry) = (r[0],r[1])
(x,y) = self.xyconv(x,y)
(rx,ry) = self.xyconv(rx,ry)
animate = None
if "animate" in kd:
animate = kd.pop("animate")
opt = dic2opt(kd,"")
self.svgout("ellipse",'cx="%s" cy="%s" rx="%s" ry="%s" %s'%(
x,y,abs(rx),abs(ry),opt),animate)
def create_oval(self,*t,**kd): # for Tk
(x1,y1,x2,y2) = expand(*t)
kd = default(kd,{"outline":"black","fill":"none","width":1})
self.circle((x1+x2)/2.0,(y1+y2)/2.0,r=(abs((x2-x1)/2.0),abs((y2-y1)/2.0)),**kd)
def rect(self,*t,**kd):
(x1,y1,x2,y2) = expand(*t)
kd = dic2ndic(kd)
kd = self.xtransform(**kd)
(x,y,w,h) = self.xywh(x1,y1,x2,y2)
animate = None
if "animate" in kd:
animate = kd.pop("animate")
opt = dic2opt(kd,"")
self.svgout("rect",'x="%s" y="%s" width="%s" height="%s" %s'%(x,y,w,h,opt),animate)
def create_rectangle(self,*t,**kd): # for Tk
(x1,y1,x2,y2) = expand(*t)
kd = default(kd,{"fill":"none", "outline":"black", "width":1})
s = ""
if "fill" in kd:
s = s + 'fill:%s;'%kd["fill"]
if "outline" in kd:
s = s + 'stroke:%s;'%kd["outline"]
if "width" in kd:
s = s + 'stroke-width:%s;'%kd["width"]
self.rect(x1,y1,x2,y2,style=s)
def arc(self,*t,**kd):
(x,y) = expand(*t)
kd = default(kd,{"shape":"arc", "fill":"none"})
kd = self.xtransform(**kd)
# shape is one of "arc","chord","sector"; Tk employs "pieslice" for "sector".
# (x,y) is center
shape = kd.pop("shape")
r = kd["r"]
if isnum(r):
rx = ry = r
else:
(rx,ry) = (r[0],r[1])
c = math.pi/180
start = kd.pop("start")
extent = kd.pop("extent")
st = c * start
ex = c * extent
stop = st + ex
x1,y1 = (rx*math.cos(st)+x,ry*math.sin(st)+y)
if shape in ("sector"):
s = "M %s %s L %s %s"%(x,y,x1,y1)
else:
s = "M %s %s"%(x1,y1)
x2,y2 = (rx*math.cos(stop)+x,ry*math.sin(stop)+y)
flag1 = flag2 = 0
if abs(extent) > 180: flag1 = 1 # large arc flag
if extent < 0: flag2 = 1 # large arc flag
rotate = 0
if "rotate" in kd:
rotate = kd.pop("rotate")
s = s + " A %s %s %d %d %d %s %s"%(rx,ry,rotate,flag1,flag2,x2,y2)
if shape in ("chord","sector"):
s = s + " Z"
self.path(s,**kd)
def image(self,*t,**kd):
(x1,y1,x2,y2) = expand(*t)
kd = dic2ndic(kd)
kd = self.xtransform(**kd)
(x,y,w,h) = self.xywh(x1,y1,x2,y2)
animate = None
if "animate" in kd:
animate = kd.pop("animate")
opt = dic2opt(kd,"")
self.svgout("image",'x="%s" y="%s" width="%s" height="%s" %s'%(x,y,w,h,opt),animate)
def use(self,*t,**kd):
t = expand(*t)
kd = dic2ndic(kd)
kd["xlink:href"] = "#" + kd.pop("ref")
if len(t) > 1:
kd["x"],kd["y"] = self.xyconv(t[0],t[1])
kd = self.xtransform(**kd)
animate = None
if "animate" in kd:
animate = kd.pop("animate")
opt = dic2opt(kd,"")
self.svgout("use",opt,animate)
def grid(self,*t,**kd):
pitchX,pitchY = expand(*t)
"""=== usage example
c.grid(1,1,range=(-1.3,1.3,1.3,-1.3),stroke="cyan",
frame="green;width:5",axis="black;width:2",
font="Helvetica;size:16",label=("X","Y"))
# note that font is for the scale numbers, not for the axis label
# it's unable to make fine tuning to label. if you need, do manually!
# making too much is worse than do nothing.
==="""
# these attributes must be before default
frame = None
if "frame" in kd:
frame = kd.pop("frame")
axis = None
if "axis" in kd:
axis = kd.pop("axis")
xlabel = ylabel = None
if "label" in kd:
xlabel,ylabel = kd.pop("label")
kd = default(kd,{"stroke":"green;width:1"})
color = kd.pop("stroke")
width = float(kd["stroke-width"])
if "range" in kd:
range = kd.pop("range")
(mX1,mY1,mX2,mY2) = range
else:
(mX1,mY1,mX2,mY2) = self.mapBox
if mX1 > mX2: (mX1,mX2) = (mX2,mX1)
if mY1 > mY2: (mY1,mY2) = (mY2,mY1)
# x or y may be a small number, so ...
x0 = math.ceil(float(mX1)/pitchX)*pitchX
y0 = math.ceil(float(mY1)/pitchY)*pitchY
x = x0
while x <= mX2:
self.line(x,mY1,x,mY2,stroke="%s;width:%s"%(color,width)); x = x + pitchX
y = y0
while y <= mY2:
self.line(mX1,y,mX2,y,stroke="%s;width:%s"%(color,width)); y = y + pitchY
if xlabel:
x = x0
while x <= mX2:
self.text(x,0,text=int(x),anchor="NW",**kd); x = x + pitchX
self.text((mX1+mX2)/2,mY1,text="%s"%xlabel,font="size:24",dy="1",anchor="N",**kd)
if ylabel:
y = y0
while y <= mY2:
self.text(0,y,text=int(y),anchor="ES",**kd); y = y + pitchY
self.text(mX1,(mY1+mY2)/2,text="%s"%ylabel,font="size:24",dx="-1",anchor="E",**kd)
if not axis:
axis = "%s;width:%s"%(color,2*width)
self.line(mX1,0,mX2,0,stroke="%s"%axis)
self.line(0,mY1,0,mY2,stroke="%s"%axis)
if frame:
self.rect(mX1,mY1,mX2,mY2,stroke="%s"%frame)
# ======== group family ==============
def link(self,**kd):
kd = dic2ndic(kd)
if "ref" in kd:
kd["xlink:href"] = "#" + kd.pop("ref")
kd["gtype"] = "a"
return self.group(**kd)
def clip(self,**kd):
kd = default(kd, {"clippathUnits":"userSpaceOnUse"})
kd["gtype"] = "clipPath"
return self.group(**kd)
def mask(self,*t,**kd):
(x1,y1,x2,y2) = expand(*t)
kd = default(kd, {"maskUnits":"userSpaceOnUse"})
(kd["x"],kd["y"],kd["width"],kd["height"]) = self.xywh(x1,y1,x2,y2)
kd["gtype"] = "mask"
return self.group(**kd)
def define(self,**kd):
kd["gtype"] = "defs"
return self.group(**kd)
def group(self,**kd):
global gid
new = copy.deepcopy(self)
new.parent = self
gid = gid + 1
kd = self.xtransform(**kd)
kd = dic2ndic(kd)
t = "g"
if "gtype" in kd:
t = kd.pop("gtype")
opt = dic2opt(kd,"")
new.out = ['<%s %s>'%(t,opt)]
self.child.append(new)
new.child = []
new.gid = gid
return new
def end(self): # user's interface for flush
self.flush()
def flush(self):
# flush the out buffer (including children's) to it's parent out buffer
# and hide the 'self' from the parent' child list
# keep order of the 'for' statements
p = self.parent
out = p.out
for g in self.child:
if g:
g.flush()
for line in self.out:
out.append(line)
gtype = tagname(self.out[0]) # g,defs,symbol
out.append("%s>"%gtype)
for n in range(0,len(p.child)):
#print "##+",n,p.child[n],self
if p.child[n] == self:
p.child[n] = None
# === Plot Library
def func(self,f,**kd):
low,high = kd.pop("range")
low,high = float(low),float(high)
m = 100
if "n" in kd:
m = int(kd.pop("n"))
a = (high - low)/m
for n in range(0,m,1):
(x1,x2) = (a*n + low,a*(n+1) + low)
y1,y2 = f(x1),f(x2)
x0 = (x1+x2)/2
y0 = 2*f(x0) - (f(x1)+f(x2))/2
self.path("M %s %s Q %s %s %s %s"%(x1,y1,x0,y0,x2,y2))