Back to home page

Project CMSSW displayed by LXR

 
 

    


File indexing completed on 2024-09-26 05:05:38

0001 # svgfig.py copyright (C) 2008 Jim Pivarski <jpivarski@gmail.com>
0002 # 
0003 # This program is free software; you can redistribute it and/or
0004 # modify it under the terms of the GNU General Public License
0005 # as published by the Free Software Foundation; either version 2
0006 # of the License, or (at your option) any later version.
0007 # 
0008 # This program is distributed in the hope that it will be useful,
0009 # but WITHOUT ANY WARRANTY; without even the implied warranty of
0010 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0011 # GNU General Public License for more details.
0012 # 
0013 # You should have received a copy of the GNU General Public License
0014 # along with this program; if not, write to the Free Software
0015 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
0016 # 
0017 # Full licence is in the file COPYING and at http://www.gnu.org/copyleft/gpl.html
0018 
0019 from builtins import range
0020 import re, codecs, os, platform, copy, itertools, math, cmath, random, sys, copy
0021 _epsilon = 1e-5
0022 
0023 
0024 if re.search("windows", platform.system(), re.I):
0025   try:
0026     import _winreg
0027     _default_directory = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER, \
0028                        r"Software\Microsoft\Windows\Current Version\Explorer\Shell Folders"), "Desktop")[0]
0029 #   tmpdir = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Environment"), "TEMP")[0]
0030 #   if tmpdir[0:13] != "%USERPROFILE%":
0031 #     tmpdir = os.path.expanduser("~") + tmpdir[13:]
0032   except:
0033     _default_directory = os.path.expanduser("~") + os.sep + "Desktop"
0034 
0035 _default_fileName = "tmp.svg"
0036 
0037 _hacks = {}
0038 _hacks["inkscape-text-vertical-shift"] = False
0039 
0040 def rgb(r, g, b, maximum=1.):
0041   """Create an SVG color string "#xxyyzz" from r, g, and b.
0042 
0043   r,g,b = 0 is black and r,g,b = maximum is white.
0044   """
0045   return "#%02x%02x%02x" % (max(0, min(r*255./maximum, 255)), max(0, min(g*255./maximum, 255)), max(0, min(b*255./maximum, 255)))
0046 
0047 def attr_preprocess(attr):
0048   for name in attr.keys():
0049     name_colon = re.sub("__", ":", name)
0050     if name_colon != name:
0051       attr[name_colon] = attr[name]
0052       del attr[name]
0053       name = name_colon
0054 
0055     name_dash = re.sub("_", "-", name)
0056     if name_dash != name:
0057       attr[name_dash] = attr[name]
0058       del attr[name]
0059       name = name_dash
0060 
0061   return attr
0062 
0063 class SVG:
0064   """A tree representation of an SVG image or image fragment.
0065 
0066   SVG(t, sub, sub, sub..., attribute=value)
0067 
0068   t                       required             SVG type name
0069   sub                     optional list        nested SVG elements or text/Unicode
0070   attribute=value pairs   optional keywords    SVG attributes
0071 
0072   In attribute names, "__" becomes ":" and "_" becomes "-".
0073 
0074   SVG in XML
0075 
0076   <g id="mygroup" fill="blue">
0077       <rect x="1" y="1" width="2" height="2" />
0078       <rect x="3" y="3" width="2" height="2" />
0079   </g>
0080 
0081   SVG in Python
0082 
0083   >>> svg = SVG("g", SVG("rect", x=1, y=1, width=2, height=2), \\ 
0084   ...                SVG("rect", x=3, y=3, width=2, height=2), \\ 
0085   ...           id="mygroup", fill="blue")
0086 
0087   Sub-elements and attributes may be accessed through tree-indexing:
0088 
0089   >>> svg = SVG("text", SVG("tspan", "hello there"), stroke="none", fill="black")
0090   >>> svg[0]
0091   <tspan (1 sub) />
0092   >>> svg[0, 0]
0093   'hello there'
0094   >>> svg["fill"]
0095   'black'
0096 
0097   Iteration is depth-first:
0098 
0099   >>> svg = SVG("g", SVG("g", SVG("line", x1=0, y1=0, x2=1, y2=1)), \\
0100   ...                SVG("text", SVG("tspan", "hello again")))
0101   ... 
0102   >>> for ti, s in svg:
0103   ...     print ti, repr(s)
0104   ... 
0105   (0,) <g (1 sub) />
0106   (0, 0) <line x2=1 y1=0 x1=0 y2=1 />
0107   (0, 0, 'x2') 1
0108   (0, 0, 'y1') 0
0109   (0, 0, 'x1') 0
0110   (0, 0, 'y2') 1
0111   (1,) <text (1 sub) />
0112   (1, 0) <tspan (1 sub) />
0113   (1, 0, 0) 'hello again'
0114 
0115   Use "print" to navigate:
0116 
0117   >>> print svg
0118   None                 <g (2 sub) />
0119   [0]                      <g (1 sub) />
0120   [0, 0]                       <line x2=1 y1=0 x1=0 y2=1 />
0121   [1]                      <text (1 sub) />
0122   [1, 0]                       <tspan (1 sub) />
0123   """
0124   def __init__(self, *t_sub, **attr):
0125     if len(t_sub) == 0: raise TypeError("SVG element must have a t (SVG type)")
0126 
0127     # first argument is t (SVG type)
0128     self.t = t_sub[0]
0129     # the rest are sub-elements
0130     self.sub = list(t_sub[1:])
0131     
0132     # keyword arguments are attributes
0133     # need to preprocess to handle differences between SVG and Python syntax
0134     self.attr = attr_preprocess(attr)
0135 
0136   def __getitem__(self, ti):
0137     """Index is a list that descends tree, returning a sub-element if
0138     it ends with a number and an attribute if it ends with a string."""
0139     obj = self
0140     if isinstance(ti, (list, tuple)):
0141       for i in ti[:-1]: obj = obj[i]
0142       ti = ti[-1]
0143 
0144     if isinstance(ti, (int, long, slice)): return obj.sub[ti]
0145     else: return obj.attr[ti]
0146 
0147   def __setitem__(self, ti, value):
0148     """Index is a list that descends tree, returning a sub-element if
0149     it ends with a number and an attribute if it ends with a string."""
0150     obj = self
0151     if isinstance(ti, (list, tuple)):
0152       for i in ti[:-1]: obj = obj[i]
0153       ti = ti[-1]
0154 
0155     if isinstance(ti, (int, long, slice)): obj.sub[ti] = value
0156     else: obj.attr[ti] = value
0157 
0158   def __delitem__(self, ti):
0159     """Index is a list that descends tree, returning a sub-element if
0160     it ends with a number and an attribute if it ends with a string."""
0161     obj = self
0162     if isinstance(ti, (list, tuple)):
0163       for i in ti[:-1]: obj = obj[i]
0164       ti = ti[-1]
0165 
0166     if isinstance(ti, (int, long, slice)): del obj.sub[ti]
0167     else: del obj.attr[ti]
0168 
0169   def __contains__(self, value):
0170     """x in svg == True iff x is an attribute in svg."""
0171     return value in self.attr
0172 
0173   def __eq__(self, other):
0174     """x == y iff x represents the same SVG as y."""
0175     if id(self) == id(other): return True
0176     return isinstance(other, SVG) and self.t == other.t and self.sub == other.sub and self.attr == other.attr
0177 
0178   def __ne__(self, other):
0179     """x != y iff x does not represent the same SVG as y."""
0180     return not (self == other)
0181 
0182   def append(self, x):
0183     """Appends x to the list of sub-elements (drawn last, overlaps
0184     other primatives)."""
0185     self.sub.append(x)
0186 
0187   def prepend(self, x):
0188     """Prepends x to the list of sub-elements (drawn first may be
0189     overlapped by other primatives)."""
0190     self.sub[0:0] = [x]
0191 
0192   def extend(self, x):
0193     """Extends list of sub-elements by a list x."""
0194     self.sub.extend(x)
0195 
0196   def clone(self, shallow=False):
0197     """Deep copy of SVG tree.  Set shallow=True for a shallow copy."""
0198     if shallow:
0199       return copy.copy(self)
0200     else:
0201       return copy.deepcopy(self)
0202 
0203   ### nested class
0204   class SVGDepthIterator:
0205     """Manages SVG iteration."""
0206 
0207     def __init__(self, svg, ti, depth_limit):
0208       self.svg = svg
0209       self.ti = ti
0210       self.shown = False
0211       self.depth_limit = depth_limit
0212 
0213     def __iter__(self): return self
0214 
0215     def next(self):
0216       if not self.shown:
0217         self.shown = True
0218         if self.ti != ():
0219           return self.ti, self.svg
0220 
0221       if not isinstance(self.svg, SVG): raise StopIteration
0222       if self.depth_limit != None and len(self.ti) >= self.depth_limit: raise StopIteration
0223 
0224       if "iterators" not in self.__dict__:
0225         self.iterators = []
0226         for i, s in enumerate(self.svg.sub):
0227           self.iterators.append(self.__class__(s, self.ti + (i,), self.depth_limit))
0228         for k, s in self.svg.attr.items():
0229           self.iterators.append(self.__class__(s, self.ti + (k,), self.depth_limit))
0230         self.iterators = itertools.chain(*self.iterators)
0231 
0232       return next(self.iterators)
0233   ### end nested class
0234 
0235   def depth_first(self, depth_limit=None):
0236     """Returns a depth-first generator over the SVG.  If depth_limit
0237     is a number, stop recursion at that depth."""
0238     return self.SVGDepthIterator(self, (), depth_limit)
0239 
0240   def breadth_first(self, depth_limit=None):
0241     """Not implemented yet.  Any ideas on how to do it?
0242 
0243     Returns a breadth-first generator over the SVG.  If depth_limit
0244     is a number, stop recursion at that depth."""
0245     raise NotImplementedError("Got an algorithm for breadth-first searching a tree without effectively copying the tree?")
0246 
0247   def __iter__(self): return self.depth_first()
0248 
0249   def items(self, sub=True, attr=True, text=True):
0250     """Get a recursively-generated list of tree-index, sub-element/attribute pairs.
0251 
0252     If sub == False, do not show sub-elements.
0253     If attr == False, do not show attributes.
0254     If text == False, do not show text/Unicode sub-elements.
0255     """
0256     output = []
0257     for ti, s in self:
0258       show = False
0259       if isinstance(ti[-1], (int, long)):
0260         if isinstance(s, str): show = text
0261         else: show = sub
0262       else: show = attr
0263 
0264       if show: output.append((ti, s))
0265     return output
0266 
0267   def keys(self, sub=True, attr=True, text=True):
0268     """Get a recursively-generated list of tree-indexes.
0269 
0270     If sub == False, do not show sub-elements.
0271     If attr == False, do not show attributes.
0272     If text == False, do not show text/Unicode sub-elements.
0273     """
0274     return [ti for ti, s in self.items(sub, attr, text)]
0275 
0276   def values(self, sub=True, attr=True, text=True):
0277     """Get a recursively-generated list of sub-elements and attributes.
0278 
0279     If sub == False, do not show sub-elements.
0280     If attr == False, do not show attributes.
0281     If text == False, do not show text/Unicode sub-elements.
0282     """
0283     return [s for ti, s in self.items(sub, attr, text)]
0284 
0285   def __repr__(self): return self.xml(depth_limit=0)
0286 
0287   def __str__(self):
0288     """Print (actually, return a string of) the tree in a form useful for browsing."""
0289     return self.tree(sub=True, attr=False, text=False)
0290 
0291   def tree(self, depth_limit=None, sub=True, attr=True, text=True, tree_width=20, obj_width=80):
0292     """Print (actually, return a string of) the tree in a form useful for browsing.
0293 
0294     If depth_limit == a number, stop recursion at that depth.
0295     If sub == False, do not show sub-elements.
0296     If attr == False, do not show attributes.
0297     If text == False, do not show text/Unicode sub-elements.
0298     tree_width is the number of characters reserved for printing tree indexes.
0299     obj_width is the number of characters reserved for printing sub-elements/attributes.
0300     """
0301 
0302     output = []
0303 
0304     line = "%s %s" % (("%%-%ds" % tree_width) % repr(None), ("%%-%ds" % obj_width) % (repr(self))[0:obj_width])
0305     output.append(line)
0306 
0307     for ti, s in self.depth_first(depth_limit):
0308       show = False
0309       if isinstance(ti[-1], (int, long)):
0310         if isinstance(s, str): show = text
0311         else: show = sub
0312       else: show = attr
0313 
0314       if show:
0315         line = "%s %s" % (("%%-%ds" % tree_width) % repr(list(ti)), ("%%-%ds" % obj_width) % ("    "*len(ti) + repr(s))[0:obj_width])
0316         output.append(line)
0317 
0318     return "\n".join(output)
0319 
0320   def xml(self, indent="    ", newl="\n", depth_limit=None, depth=0):
0321     """Get an XML representation of the SVG.
0322 
0323     indent      string used for indenting
0324     newl        string used for newlines
0325     If depth_limit == a number, stop recursion at that depth.
0326     depth       starting depth (not useful for users)
0327 
0328     print svg.xml()
0329     """
0330 
0331     attrstr = []
0332     for n, v in self.attr.items():
0333       if isinstance(v, dict):
0334         v = "; ".join(["%s:%s" % (ni, vi) for ni, vi in v.items()])
0335       elif isinstance(v, (list, tuple)):
0336         v = ", ".join(v)
0337       attrstr.append(" %s=%s" % (n, repr(v)))
0338     attrstr = "".join(attrstr)
0339 
0340     if len(self.sub) == 0: return "%s<%s%s />" % (indent * depth, self.t, attrstr)
0341 
0342     if depth_limit == None or depth_limit > depth:
0343       substr = []
0344       for s in self.sub:
0345         if isinstance(s, SVG):
0346           substr.append(s.xml(indent, newl, depth_limit, depth + 1) + newl)
0347         elif isinstance(s, str):
0348           substr.append("%s%s%s" % (indent * (depth + 1), s, newl))
0349         else:
0350           substr.append("%s%s%s" % (indent * (depth + 1), repr(s), newl))
0351       substr = "".join(substr)
0352 
0353       return "%s<%s%s>%s%s%s</%s>" % (indent * depth, self.t, attrstr, newl, substr, indent * depth, self.t)
0354 
0355     else:
0356       return "%s<%s (%d sub)%s />" % (indent * depth, self.t, len(self.sub), attrstr)
0357 
0358   def standalone_xml(self, indent="    ", newl="\n"):
0359     """Get an XML representation of the SVG that can be saved/rendered.
0360 
0361     indent      string used for indenting
0362     newl        string used for newlines
0363     """
0364 
0365     if self.t == "svg": top = self
0366     else: top = canvas(self)
0367     return """\
0368 <?xml version="1.0" standalone="no"?>
0369 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
0370 
0371 """ + ("".join(top.__standalone_xml(indent, newl)))  # end of return statement
0372 
0373   def __standalone_xml(self, indent, newl):
0374     output = [u"<%s" % self.t]
0375 
0376     for n, v in self.attr.items():
0377       if isinstance(v, dict):
0378         v = "; ".join(["%s:%s" % (ni, vi) for ni, vi in v.items()])
0379       elif isinstance(v, (list, tuple)):
0380         v = ", ".join(v)
0381       output.append(u" %s=\"%s\"" % (n, v))
0382 
0383     if len(self.sub) == 0:
0384       output.append(u" />%s%s" % (newl, newl))
0385       return output
0386 
0387     elif self.t == "text" or self.t == "tspan" or self.t == "style":
0388       output.append(u">")
0389 
0390     else:
0391       output.append(u">%s%s" % (newl, newl))
0392 
0393     for s in self.sub:
0394       if isinstance(s, SVG): output.extend(s.__standalone_xml(indent, newl))
0395       else: output.append(unicode(s))
0396 
0397     if self.t == "tspan": output.append(u"</%s>" % self.t)
0398     else: output.append(u"</%s>%s%s" % (self.t, newl, newl))
0399 
0400     return output
0401 
0402   def interpret_fileName(self, fileName=None):
0403     if fileName == None:
0404       fileName = _default_fileName
0405     if re.search("windows", platform.system(), re.I) and not os.path.isabs(fileName):
0406       fileName = _default_directory + os.sep + fileName
0407     return fileName
0408 
0409   def save(self, fileName=None, encoding="utf-8", compresslevel=None):
0410     """Save to a file for viewing.  Note that svg.save() overwrites the file named _default_fileName.
0411 
0412     fileName        default=None            note that _default_fileName will be overwritten if
0413                                             no fileName is specified. If the extension
0414                                             is ".svgz" or ".gz", the output will be gzipped
0415     encoding        default="utf-8"       file encoding (default is Unicode)
0416     compresslevel   default=None            if a number, the output will be gzipped with that
0417                                             compression level (1-9, 1 being fastest and 9 most
0418                                             thorough)
0419     """
0420     fileName = self.interpret_fileName(fileName)
0421 
0422     if compresslevel != None or re.search("\\.svgz$", fileName, re.I) or re.search("\\.gz$", fileName, re.I):
0423       import gzip
0424       if compresslevel == None:
0425         f = gzip.GzipFile(fileName, "w")
0426       else:
0427         f = gzip.GzipFile(fileName, "w", compresslevel)
0428 
0429       f = codecs.EncodedFile(f, "utf-8", encoding)
0430       f.write(self.standalone_xml())
0431       f.close()
0432 
0433     else:
0434       f = codecs.open(fileName, "w", encoding=encoding)
0435       f.write(self.standalone_xml())
0436       f.close()
0437 
0438   def inkview(self, fileName=None, encoding="utf-8"):
0439     """View in "inkview", assuming that program is available on your system.
0440 
0441     fileName        default=None            note that any file named _default_fileName will be
0442                                             overwritten if no fileName is specified. If the extension
0443                                             is ".svgz" or ".gz", the output will be gzipped
0444     encoding        default="utf-8"       file encoding (default is Unicode)
0445     """
0446     fileName = self.interpret_fileName(fileName)
0447     self.save(fileName, encoding)
0448     os.spawnvp(os.P_NOWAIT, "inkview", ("inkview", fileName))
0449 
0450   def inkscape(self, fileName=None, encoding="utf-8"):
0451     """View in "inkscape", assuming that program is available on your system.
0452 
0453     fileName        default=None            note that any file named _default_fileName will be
0454                                             overwritten if no fileName is specified. If the extension
0455                                             is ".svgz" or ".gz", the output will be gzipped
0456     encoding        default="utf-8"       file encoding (default is Unicode)
0457     """
0458     fileName = self.interpret_fileName(fileName)
0459     self.save(fileName, encoding)
0460     os.spawnvp(os.P_NOWAIT, "inkscape", ("inkscape", fileName))
0461 
0462   def firefox(self, fileName=None, encoding="utf-8"):
0463     """View in "firefox", assuming that program is available on your system.
0464 
0465     fileName        default=None            note that any file named _default_fileName will be
0466                                             overwritten if no fileName is specified. If the extension
0467                                             is ".svgz" or ".gz", the output will be gzipped
0468     encoding        default="utf-8"       file encoding (default is Unicode)
0469     """
0470     fileName = self.interpret_fileName(fileName)
0471     self.save(fileName, encoding)
0472     os.spawnvp(os.P_NOWAIT, "firefox", ("firefox", fileName))
0473 
0474 ######################################################################
0475 
0476 _canvas_defaults = {"width": "400px", "height": "400px", "viewBox": "0 0 100 100", \
0477                    "xmlns": "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", "version":"1.1", \
0478                    "style": {"stroke":"black", "fill":"none", "stroke-width":"0.5pt", "stroke-linejoin":"round", "text-anchor":"middle"}, \
0479                    "font-family": ["Helvetica", "Arial", "FreeSans", "Sans", "sans", "sans-serif"], \
0480                    }
0481 
0482 def canvas(*sub, **attr):
0483   """Creates a top-level SVG object, allowing the user to control the
0484   image size and aspect ratio.
0485 
0486   canvas(sub, sub, sub..., attribute=value)
0487 
0488   sub                     optional list       nested SVG elements or text/Unicode
0489   attribute=value pairs   optional keywords   SVG attributes
0490 
0491   Default attribute values:
0492 
0493   width           "400px"
0494   height          "400px"
0495   viewBox         "0 0 100 100"
0496   xmlns           "http://www.w3.org/2000/svg"
0497   xmlns:xlink     "http://www.w3.org/1999/xlink"
0498   version         "1.1"
0499   style           "stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoin:round; text-anchor:middle"
0500   font-family     "Helvetica,Arial,FreeSans?,Sans,sans,sans-serif"
0501   """
0502   attributes = dict(_canvas_defaults)
0503   attributes.update(attr)
0504 
0505   if sub == None or sub == ():
0506     return SVG("svg", **attributes)
0507   else:
0508     return SVG("svg", *sub, **attributes)
0509 
0510 def canvas_outline(*sub, **attr):
0511   """Same as canvas(), but draws an outline around the drawable area,
0512   so that you know how close your image is to the edges."""
0513   svg = canvas(*sub, **attr)
0514   match = re.match("[, \t]*([0-9e.+\\-]+)[, \t]+([0-9e.+\\-]+)[, \t]+([0-9e.+\\-]+)[, \t]+([0-9e.+\\-]+)[, \t]*", svg["viewBox"])
0515   if match == None: raise ValueError("canvas viewBox is incorrectly formatted")
0516   x, y, width, height = [float(x) for x in match.groups()]
0517   svg.prepend(SVG("rect", x=x, y=y, width=width, height=height, stroke="none", fill="cornsilk"))
0518   svg.append(SVG("rect", x=x, y=y, width=width, height=height, stroke="black", fill="none"))
0519   return svg
0520 
0521 def template(fileName, svg, replaceme="REPLACEME"):
0522   """Loads an SVG image from a file, replacing instances of
0523   <REPLACEME /> with a given svg object.
0524 
0525   fileName         required                name of the template SVG
0526   svg              required                SVG object for replacement
0527   replaceme        default="REPLACEME"   fake SVG element to be replaced by the given object
0528 
0529   >>> print load("template.svg")
0530   None                 <svg (2 sub) style=u'stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoi
0531   [0]                      <rect height=u'100' width=u'100' stroke=u'none' y=u'0' x=u'0' fill=u'yellow'
0532   [1]                      <REPLACEME />
0533   >>> 
0534   >>> print template("template.svg", SVG("circle", cx=50, cy=50, r=30))
0535   None                 <svg (2 sub) style=u'stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoi
0536   [0]                      <rect height=u'100' width=u'100' stroke=u'none' y=u'0' x=u'0' fill=u'yellow'
0537   [1]                      <circle cy=50 cx=50 r=30 />
0538   """
0539   output = load(fileName)
0540   for ti, s in output:
0541     if isinstance(s, SVG) and s.t == replaceme:
0542       output[ti] = svg
0543   return output
0544 
0545 ######################################################################
0546 
0547 def load(fileName):
0548   """Loads an SVG image from a file."""
0549   return load_stream(file(fileName))
0550 
0551 def load_stream(stream):
0552   """Loads an SVG image from a stream (can be a string or a file object)."""
0553 
0554   from xml.sax import handler, make_parser
0555   from xml.sax.handler import feature_namespaces, feature_external_ges, feature_external_pes
0556 
0557   class ContentHandler(handler.ContentHandler):
0558     def __init__(self):
0559       self.stack = []
0560       self.output = None
0561       self.all_whitespace = re.compile("^\\s*$")
0562 
0563     def startElement(self, name, attr):
0564       s = SVG(name)
0565       s.attr = dict(attr.items())
0566       if len(self.stack) > 0:
0567         last = self.stack[-1]
0568         last.sub.append(s)
0569       self.stack.append(s)
0570 
0571     def characters(self, ch):
0572       if not isinstance(ch, str) or self.all_whitespace.match(ch) == None:
0573         if len(self.stack) > 0:
0574           last = self.stack[-1]
0575           if len(last.sub) > 0 and isinstance(last.sub[-1], str):
0576             last.sub[-1] = last.sub[-1] + "\n" + ch
0577           else:
0578             last.sub.append(ch)
0579 
0580     def endElement(self, name):
0581       if len(self.stack) > 0:
0582         last = self.stack[-1]
0583         if isinstance(last, SVG) and last.t == "style" and "type" in last.attr and last.attr["type"] == "text/css" and len(last.sub) == 1 and isinstance(last.sub[0], str):
0584           last.sub[0] = "<![CDATA[\n" + last.sub[0] + "]]>"
0585 
0586       self.output = self.stack.pop()
0587 
0588   ch = ContentHandler()
0589   parser = make_parser()
0590   parser.setContentHandler(ch)
0591   parser.setFeature(feature_namespaces, 0)
0592   parser.setFeature(feature_external_ges, 0)
0593   parser.parse(stream)
0594   return ch.output
0595 
0596 ######################################################################
0597 
0598 def totrans(expr, vars=("x", "y"), globals=None, locals=None):
0599   """Converts to a coordinate transformation (a function that accepts
0600   two arguments and returns two values).
0601 
0602   expr       required                  a string expression or a function
0603                                        of two real or one complex value
0604   vars       default=("x", "y")    independent variable names;
0605                                        a singleton ("z",) is interpreted
0606                                        as complex
0607   globals    default=None              dict of global variables
0608   locals     default=None              dict of local variables
0609   """
0610 
0611   if callable(expr):
0612     if expr.__code__.co_argcount == 2:
0613       return expr
0614 
0615     elif expr.__code__.co_argcount == 1:
0616       split = lambda z: (z.real, z.imag)
0617       output = lambda x, y: split(expr(x + y*1j))
0618       output.__name__ = expr.__name__
0619       return output
0620 
0621     else:
0622       raise TypeError("must be a function of 2 or 1 variables")
0623 
0624   if len(vars) == 2:
0625     g = math.__dict__
0626     if globals != None: g.update(globals)
0627     output = eval("lambda %s, %s: (%s)" % (vars[0], vars[1], expr), g, locals)
0628     output.__name__ = "%s,%s -> %s" % (vars[0], vars[1], expr)
0629     return output
0630 
0631   elif len(vars) == 1:
0632     g = cmath.__dict__
0633     if globals != None: g.update(globals)
0634     output = eval("lambda %s: (%s)" % (vars[0], expr), g, locals)
0635     split = lambda z: (z.real, z.imag)
0636     output2 = lambda x, y: split(output(x + y*1j))
0637     output2.__name__ = "%s -> %s" % (vars[0], expr)
0638     return output2
0639 
0640   else:
0641     raise TypeError("vars must have 2 or 1 elements")
0642 
0643 def window(xmin, xmax, ymin, ymax, x=0, y=0, width=100, height=100, xlogbase=None, ylogbase=None, minusInfinity=-1000, flipx=False, flipy=True):
0644   """Creates and returns a coordinate transformation (a function that
0645   accepts two arguments and returns two values) that transforms from
0646       (xmin, ymin), (xmax, ymax)
0647   to
0648       (x, y), (x + width, y + height).
0649 
0650   xlogbase, ylogbase    default=None, None     if a number, transform
0651                                                logarithmically with given base
0652   minusInfinity         default=-1000          what to return if
0653                                                log(0 or negative) is attempted
0654   flipx                 default=False          if true, reverse the direction of x
0655   flipy                 default=True           if true, reverse the direction of y
0656 
0657   (When composing windows, be sure to set flipy=False.)
0658   """
0659 
0660   if flipx:
0661     ox1 = x + width
0662     ox2 = x
0663   else:
0664     ox1 = x
0665     ox2 = x + width
0666   if flipy:
0667     oy1 = y + height
0668     oy2 = y
0669   else:
0670     oy1 = y
0671     oy2 = y + height
0672   ix1 = xmin
0673   iy1 = ymin
0674   ix2 = xmax
0675   iy2 = ymax
0676   
0677   if xlogbase != None and (ix1 <= 0. or ix2 <= 0.): raise ValueError("x range incompatible with log scaling: (%g, %g)" % (ix1, ix2))
0678 
0679   if ylogbase != None and (iy1 <= 0. or iy2 <= 0.): raise ValueError("y range incompatible with log scaling: (%g, %g)" % (iy1, iy2))
0680 
0681   def maybelog(t, it1, it2, ot1, ot2, logbase):
0682     if t <= 0.: return minusInfinity
0683     else:
0684       return ot1 + 1.*(math.log(t, logbase) - math.log(it1, logbase))/(math.log(it2, logbase) - math.log(it1, logbase)) * (ot2 - ot1)
0685 
0686   xlogstr, ylogstr = "", ""
0687 
0688   if xlogbase == None:
0689     xfunc = lambda x: ox1 + 1.*(x - ix1)/(ix2 - ix1) * (ox2 - ox1)
0690   else:
0691     xfunc = lambda x: maybelog(x, ix1, ix2, ox1, ox2, xlogbase)
0692     xlogstr = " xlog=%g" % xlogbase
0693 
0694   if ylogbase == None:
0695     yfunc = lambda y: oy1 + 1.*(y - iy1)/(iy2 - iy1) * (oy2 - oy1)
0696   else:
0697     yfunc = lambda y: maybelog(y, iy1, iy2, oy1, oy2, ylogbase)
0698     ylogstr = " ylog=%g" % ylogbase
0699 
0700   output = lambda x, y: (xfunc(x), yfunc(y))
0701 
0702   output.__name__ = "(%g, %g), (%g, %g) -> (%g, %g), (%g, %g)%s%s" % (ix1, ix2, iy1, iy2, ox1, ox2, oy1, oy2, xlogstr, ylogstr)
0703   return output
0704 
0705 def rotate(angle, cx=0, cy=0):
0706   """Creates and returns a coordinate transformation which rotates
0707   around (cx,cy) by "angle" degrees."""
0708   angle *= math.pi/180.
0709   return lambda x, y: (cx + math.cos(angle)*(x - cx) - math.sin(angle)*(y - cy), cy + math.sin(angle)*(x - cx) + math.cos(angle)*(y - cy))
0710 
0711 class Fig:
0712   """Stores graphics primitive objects and applies a single coordinate
0713   transformation to them. To compose coordinate systems, nest Fig
0714   objects.
0715 
0716   Fig(obj, obj, obj..., trans=function)
0717 
0718   obj     optional list    a list of drawing primatives
0719   trans   default=None     a coordinate transformation function
0720 
0721   >>> fig = Fig(Line(0,0,1,1), Rect(0.2,0.2,0.8,0.8), trans="2*x, 2*y")
0722   >>> print fig.SVG().xml()
0723   <g>
0724       <path d='M0 0L2 2' />
0725       <path d='M0.4 0.4L1.6 0.4ZL1.6 1.6ZL0.4 1.6ZL0.4 0.4ZZ' />
0726   </g>
0727   >>> print Fig(fig, trans="x/2., y/2.").SVG().xml()
0728   <g>
0729       <path d='M0 0L1 1' />
0730       <path d='M0.2 0.2L0.8 0.2ZL0.8 0.8ZL0.2 0.8ZL0.2 0.2ZZ' />
0731   </g>
0732   """
0733 
0734   def __repr__(self):
0735     if self.trans == None:
0736       return "<Fig (%d items)>" % len(self.d)
0737     elif isinstance(self.trans, str):
0738       return "<Fig (%d items) x,y -> %s>" % (len(self.d), self.trans)
0739     else:
0740       return "<Fig (%d items) %s>" % (len(self.d), self.trans.__name__)
0741 
0742   def __init__(self, *d, **kwds):
0743     self.d = list(d)
0744     defaults = {"trans":None}
0745     defaults.update(kwds)
0746     kwds = defaults
0747 
0748     self.trans = kwds["trans"]; del kwds["trans"]
0749     if len(kwds) != 0:
0750       raise TypeError("Fig() got unexpected keyword arguments %s" % kwds.keys())
0751 
0752   def SVG(self, trans=None):
0753     """Apply the transformation "trans" and return an SVG object.
0754 
0755     Coordinate transformations in nested Figs will be composed.
0756     """
0757 
0758     if trans == None: trans = self.trans
0759     if isinstance(trans, str): trans = totrans(trans)
0760 
0761     output = SVG("g")
0762     for s in self.d:
0763       if isinstance(s, SVG):
0764         output.append(s)
0765 
0766       elif isinstance(s, Fig):
0767         strans = s.trans
0768         if isinstance(strans, str): strans = totrans(strans)
0769 
0770         if trans == None: subtrans = strans
0771         elif strans == None: subtrans = trans
0772         else: subtrans = lambda x,y: trans(*strans(x, y))
0773 
0774         output.sub += s.SVG(subtrans).sub
0775 
0776       elif s == None: pass
0777 
0778       else:
0779         output.append(s.SVG(trans))
0780 
0781     return output
0782 
0783 class Plot:
0784   """Acts like Fig, but draws a coordinate axis. You also need to supply plot ranges.
0785 
0786   Plot(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...)
0787 
0788   xmin, xmax      required        minimum and maximum x values (in the objs' coordinates)
0789   ymin, ymax      required        minimum and maximum y values (in the objs' coordinates)
0790   obj             optional list   drawing primatives
0791   keyword options keyword list    options defined below
0792 
0793   The following are keyword options, with their default values:
0794 
0795   trans           None          transformation function
0796   x, y            5, 5          upper-left corner of the Plot in SVG coordinates
0797   width, height   90, 90        width and height of the Plot in SVG coordinates
0798   flipx, flipy    False, True   flip the sign of the coordinate axis
0799   minusInfinity   -1000         if an axis is logarithmic and an object is plotted at 0 or
0800                                 a negative value, -1000 will be used as a stand-in for NaN
0801   atx, aty        0, 0          the place where the coordinate axes cross
0802   xticks          -10           request ticks according to the standard tick specification
0803                                 (see help(Ticks))
0804   xminiticks      True          request miniticks according to the standard minitick
0805                                 specification
0806   xlabels         True          request tick labels according to the standard tick label
0807                                 specification
0808   xlogbase        None          if a number, the axis and transformation are logarithmic
0809                                 with ticks at the given base (10 being the most common)
0810   (same for y)
0811   arrows          None          if a new identifier, create arrow markers and draw them
0812                                 at the ends of the coordinate axes
0813   text_attr       {}            a dictionary of attributes for label text
0814   axis_attr       {}            a dictionary of attributes for the axis lines
0815   """
0816 
0817   def __repr__(self):
0818     if self.trans == None:
0819       return "<Plot (%d items)>" % len(self.d)
0820     else:
0821       return "<Plot (%d items) %s>" % (len(self.d), self.trans.__name__)
0822 
0823   def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds):
0824     self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
0825     self.d = list(d)
0826     defaults = {"trans":None, "x":5, "y":5, "width":90, "height":90, "flipx":False, "flipy":True, "minusInfinity":-1000, \
0827                 "atx":0, "xticks":-10, "xminiticks":True, "xlabels":True, "xlogbase":None, \
0828                 "aty":0, "yticks":-10, "yminiticks":True, "ylabels":True, "ylogbase":None, \
0829                 "arrows":None, "text_attr":{}, "axis_attr":{}}
0830     defaults.update(kwds)
0831     kwds = defaults
0832 
0833     self.trans = kwds["trans"]; del kwds["trans"]
0834     self.x = kwds["x"]; del kwds["x"]
0835     self.y = kwds["y"]; del kwds["y"]
0836     self.width = kwds["width"]; del kwds["width"]
0837     self.height = kwds["height"]; del kwds["height"]
0838     self.flipx = kwds["flipx"]; del kwds["flipx"]
0839     self.flipy = kwds["flipy"]; del kwds["flipy"]
0840     self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"]
0841     self.atx = kwds["atx"]; del kwds["atx"]
0842     self.xticks = kwds["xticks"]; del kwds["xticks"]
0843     self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"]
0844     self.xlabels = kwds["xlabels"]; del kwds["xlabels"]
0845     self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"]
0846     self.aty = kwds["aty"]; del kwds["aty"]
0847     self.yticks = kwds["yticks"]; del kwds["yticks"]
0848     self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"]
0849     self.ylabels = kwds["ylabels"]; del kwds["ylabels"]
0850     self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"]
0851     self.arrows = kwds["arrows"]; del kwds["arrows"]
0852     self.text_attr = kwds["text_attr"]; del kwds["text_attr"]
0853     self.axis_attr = kwds["axis_attr"]; del kwds["axis_attr"]
0854     if len(kwds) != 0:
0855       raise TypeError("Plot() got unexpected keyword arguments %s" % kwds.keys())
0856 
0857   def SVG(self, trans=None):
0858     """Apply the transformation "trans" and return an SVG object."""
0859     if trans == None: trans = self.trans
0860     if isinstance(trans, str): trans = totrans(trans)
0861 
0862     self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax, x=self.x, y=self.y, width=self.width, height=self.height, \
0863                               xlogbase=self.xlogbase, ylogbase=self.ylogbase, minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy)
0864 
0865     d = [Axes(self.xmin, self.xmax, self.ymin, self.ymax, self.atx, self.aty, \
0866               self.xticks, self.xminiticks, self.xlabels, self.xlogbase, \
0867               self.yticks, self.yminiticks, self.ylabels, self.ylogbase, \
0868               self.arrows, self.text_attr, **self.axis_attr)] \
0869         + self.d
0870 
0871     return Fig(Fig(*d, **{"trans":trans})).SVG(self.last_window)
0872     
0873 class Frame:
0874   text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
0875   axis_defaults = {}
0876 
0877   tick_length = 1.5
0878   minitick_length = 0.75
0879   text_xaxis_offset = 1.
0880   text_yaxis_offset = 2.
0881   text_xtitle_offset = 6.
0882   text_ytitle_offset = 12.
0883 
0884   def __repr__(self):
0885     return "<Frame (%d items)>" % len(self.d)
0886 
0887   def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds):
0888     """Acts like Fig, but draws a coordinate frame around the data. You also need to supply plot ranges.
0889 
0890     Frame(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...)
0891 
0892     xmin, xmax      required        minimum and maximum x values (in the objs' coordinates)
0893     ymin, ymax      required        minimum and maximum y values (in the objs' coordinates)
0894     obj             optional list   drawing primatives
0895     keyword options keyword list    options defined below
0896 
0897     The following are keyword options, with their default values:
0898 
0899     x, y            20, 5         upper-left corner of the Frame in SVG coordinates
0900     width, height   75, 80        width and height of the Frame in SVG coordinates
0901     flipx, flipy    False, True   flip the sign of the coordinate axis
0902     minusInfinity   -1000         if an axis is logarithmic and an object is plotted at 0 or
0903                                   a negative value, -1000 will be used as a stand-in for NaN
0904     xtitle          None          if a string, label the x axis
0905     xticks          -10           request ticks according to the standard tick specification
0906                                   (see help(Ticks))
0907     xminiticks      True          request miniticks according to the standard minitick
0908                                   specification
0909     xlabels         True          request tick labels according to the standard tick label
0910                                   specification
0911     xlogbase        None          if a number, the axis and transformation are logarithmic
0912                                   with ticks at the given base (10 being the most common)
0913     (same for y)
0914     text_attr       {}            a dictionary of attributes for label text
0915     axis_attr       {}            a dictionary of attributes for the axis lines
0916     """
0917 
0918     self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
0919     self.d = list(d)
0920     defaults = {"x":20, "y":5, "width":75, "height":80, "flipx":False, "flipy":True, "minusInfinity":-1000, \
0921                 "xtitle":None, "xticks":-10, "xminiticks":True, "xlabels":True, "x2labels":None, "xlogbase":None, \
0922                 "ytitle":None, "yticks":-10, "yminiticks":True, "ylabels":True, "y2labels":None, "ylogbase":None, \
0923                 "text_attr":{}, "axis_attr":{}}
0924     defaults.update(kwds)
0925     kwds = defaults
0926 
0927     self.x = kwds["x"]; del kwds["x"]
0928     self.y = kwds["y"]; del kwds["y"]
0929     self.width = kwds["width"]; del kwds["width"]
0930     self.height = kwds["height"]; del kwds["height"]
0931     self.flipx = kwds["flipx"]; del kwds["flipx"]
0932     self.flipy = kwds["flipy"]; del kwds["flipy"]
0933     self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"]
0934     self.xtitle = kwds["xtitle"]; del kwds["xtitle"]
0935     self.xticks = kwds["xticks"]; del kwds["xticks"]
0936     self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"]
0937     self.xlabels = kwds["xlabels"]; del kwds["xlabels"]
0938     self.x2labels = kwds["x2labels"]; del kwds["x2labels"]
0939     self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"]
0940     self.ytitle = kwds["ytitle"]; del kwds["ytitle"]
0941     self.yticks = kwds["yticks"]; del kwds["yticks"]
0942     self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"]
0943     self.ylabels = kwds["ylabels"]; del kwds["ylabels"]
0944     self.y2labels = kwds["y2labels"]; del kwds["y2labels"]
0945     self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"]
0946 
0947     self.text_attr = dict(self.text_defaults)
0948     self.text_attr.update(kwds["text_attr"]); del kwds["text_attr"]
0949 
0950     self.axis_attr = dict(self.axis_defaults)
0951     self.axis_attr.update(kwds["axis_attr"]); del kwds["axis_attr"]
0952 
0953     if len(kwds) != 0:
0954       raise TypeError("Frame() got unexpected keyword arguments %s" % kwds.keys())
0955 
0956   def SVG(self):
0957     """Apply the window transformation and return an SVG object."""
0958 
0959     self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax, x=self.x, y=self.y, width=self.width, height=self.height, \
0960                               xlogbase=self.xlogbase, ylogbase=self.ylogbase, minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy)
0961 
0962     left = YAxis(self.ymin, self.ymax, self.xmin, self.yticks, self.yminiticks, self.ylabels, self.ylogbase, None, None, None, self.text_attr, **self.axis_attr)
0963     right = YAxis(self.ymin, self.ymax, self.xmax, self.yticks, self.yminiticks, self.y2labels, self.ylogbase, None, None, None, self.text_attr, **self.axis_attr)
0964     bottom = XAxis(self.xmin, self.xmax, self.ymin, self.xticks, self.xminiticks, self.xlabels, self.xlogbase, None, None, None, self.text_attr, **self.axis_attr)
0965     top = XAxis(self.xmin, self.xmax, self.ymax, self.xticks, self.xminiticks, self.x2labels, self.xlogbase, None, None, None, self.text_attr, **self.axis_attr)
0966 
0967     left.tick_start = -self.tick_length
0968     left.tick_end = 0
0969     left.minitick_start = -self.minitick_length
0970     left.minitick_end = 0.
0971     left.text_start = self.text_yaxis_offset
0972 
0973     right.tick_start = 0.
0974     right.tick_end = self.tick_length
0975     right.minitick_start = 0.
0976     right.minitick_end = self.minitick_length
0977     right.text_start = -self.text_yaxis_offset
0978     right.text_attr["text-anchor"] = "start"
0979 
0980     bottom.tick_start = 0.
0981     bottom.tick_end = self.tick_length
0982     bottom.minitick_start = 0.
0983     bottom.minitick_end = self.minitick_length
0984     bottom.text_start = -self.text_xaxis_offset
0985 
0986     top.tick_start = -self.tick_length
0987     top.tick_end = 0.
0988     top.minitick_start = -self.minitick_length
0989     top.minitick_end = 0.
0990     top.text_start = self.text_xaxis_offset
0991     top.text_attr["dominant-baseline"] = "text-after-edge"
0992 
0993     output = Fig(*self.d).SVG(self.last_window)
0994     output.prepend(left.SVG(self.last_window))
0995     output.prepend(bottom.SVG(self.last_window))
0996     output.prepend(right.SVG(self.last_window))
0997     output.prepend(top.SVG(self.last_window))
0998 
0999     if self.xtitle != None:
1000       output.append(SVG("text", self.xtitle, transform="translate(%g, %g)" % ((self.x + self.width/2.), (self.y + self.height + self.text_xtitle_offset)), dominant_baseline="text-before-edge", **self.text_attr))
1001     if self.ytitle != None:
1002       output.append(SVG("text", self.ytitle, transform="translate(%g, %g) rotate(-90)" % ((self.x - self.text_ytitle_offset), (self.y + self.height/2.)), **self.text_attr))
1003     return output
1004     
1005 ######################################################################
1006 
1007 def pathtoPath(svg):
1008   """Converts SVG("path", d="...") into Path(d=[...])."""
1009   if not isinstance(svg, SVG) or svg.t != "path":
1010     raise TypeError("Only SVG <path /> objects can be converted into Paths")
1011   attr = dict(svg.attr)
1012   d = attr["d"]
1013   del attr["d"]
1014   for key in attr.keys():
1015     if not isinstance(key, str):
1016       value = attr[key]
1017       del attr[key]
1018       attr[str(key)] = value
1019   return Path(d, **attr)
1020 
1021 class Path:
1022   """Path represents an SVG path, an arbitrary set of curves and
1023   straight segments. Unlike SVG("path", d="..."), Path stores
1024   coordinates as a list of numbers, rather than a string, so that it is
1025   transformable in a Fig.
1026 
1027   Path(d, attribute=value)
1028 
1029   d                       required        path data
1030   attribute=value pairs   keyword list    SVG attributes
1031 
1032   See http://www.w3.org/TR/SVG/paths.html for specification of paths
1033   from text.
1034 
1035   Internally, Path data is a list of tuples with these definitions:
1036 
1037       * ("Z/z",): close the current path
1038       * ("H/h", x) or ("V/v", y): a horizontal or vertical line
1039         segment to x or y
1040       * ("M/m/L/l/T/t", x, y, global): moveto, lineto, or smooth
1041         quadratic curveto point (x, y). If global=True, (x, y) should
1042         not be transformed.
1043       * ("S/sQ/q", cx, cy, cglobal, x, y, global): polybezier or
1044         smooth quadratic curveto point (x, y) using (cx, cy) as a
1045         control point. If cglobal or global=True, (cx, cy) or (x, y)
1046         should not be transformed.
1047       * ("C/c", c1x, c1y, c1global, c2x, c2y, c2global, x, y, global):
1048         cubic curveto point (x, y) using (c1x, c1y) and (c2x, c2y) as
1049         control points. If c1global, c2global, or global=True, (c1x, c1y),
1050         (c2x, c2y), or (x, y) should not be transformed.
1051       * ("A/a", rx, ry, rglobal, x-axis-rotation, angle, large-arc-flag,
1052         sweep-flag, x, y, global): arcto point (x, y) using the
1053         aforementioned parameters.
1054       * (",/.", rx, ry, rglobal, angle, x, y, global): an ellipse at
1055         point (x, y) with radii (rx, ry). If angle is 0, the whole
1056         ellipse is drawn; otherwise, a partial ellipse is drawn.
1057   """
1058   defaults = {}
1059 
1060   def __repr__(self):
1061     return "<Path (%d nodes) %s>" % (len(self.d), self.attr)
1062 
1063   def __init__(self, d=[], **attr):
1064     if isinstance(d, str): self.d = self.parse(d)
1065     else: self.d = list(d)
1066 
1067     self.attr = dict(self.defaults)
1068     self.attr.update(attr)
1069 
1070   def parse_whitespace(self, index, pathdata):
1071     """Part of Path's text-command parsing algorithm; used internally."""
1072     while index < len(pathdata) and pathdata[index] in (" ", "\t", "\r", "\n", ","): index += 1
1073     return index, pathdata
1074 
1075   def parse_command(self, index, pathdata):
1076     """Part of Path's text-command parsing algorithm; used internally."""
1077     index, pathdata = self.parse_whitespace(index, pathdata)
1078 
1079     if index >= len(pathdata): return None, index, pathdata
1080     command = pathdata[index]
1081     if "A" <= command <= "Z" or "a" <= command <= "z":
1082       index += 1
1083       return command, index, pathdata
1084     else: 
1085       return None, index, pathdata
1086 
1087   def parse_number(self, index, pathdata):
1088     """Part of Path's text-command parsing algorithm; used internally."""
1089     index, pathdata = self.parse_whitespace(index, pathdata)
1090 
1091     if index >= len(pathdata): return None, index, pathdata
1092     first_digit = pathdata[index]
1093 
1094     if "0" <= first_digit <= "9" or first_digit in ("-", "+", "."):
1095       start = index
1096       while index < len(pathdata) and ("0" <= pathdata[index] <= "9" or pathdata[index] in ("-", "+", ".", "e", "E")):
1097         index += 1
1098       end = index
1099 
1100       index = end
1101       return float(pathdata[start:end]), index, pathdata
1102     else: 
1103       return None, index, pathdata
1104 
1105   def parse_boolean(self, index, pathdata):
1106     """Part of Path's text-command parsing algorithm; used internally."""
1107     index, pathdata = self.parse_whitespace(index, pathdata)
1108 
1109     if index >= len(pathdata): return None, index, pathdata
1110     first_digit = pathdata[index]
1111 
1112     if first_digit in ("0", "1"):
1113       index += 1
1114       return int(first_digit), index, pathdata
1115     else:
1116       return None, index, pathdata
1117 
1118   def parse(self, pathdata):
1119     """Parses text-commands, converting them into a list of tuples.
1120     Called by the constructor."""
1121     output = []
1122     index = 0
1123     while True:
1124       command, index, pathdata = self.parse_command(index, pathdata)
1125       index, pathdata = self.parse_whitespace(index, pathdata)
1126 
1127       if command == None and index == len(pathdata): break  # this is the normal way out of the loop
1128       if command in ("Z", "z"):
1129         output.append((command,))
1130 
1131       ######################
1132       elif command in ("H", "h", "V", "v"):
1133         errstring = "Path command \"%s\" requires a number at index %d" % (command, index)
1134         num1, index, pathdata = self.parse_number(index, pathdata)
1135         if num1 == None: raise ValueError(errstring)
1136 
1137         while num1 != None:
1138           output.append((command, num1))
1139           num1, index, pathdata = self.parse_number(index, pathdata)
1140 
1141       ######################
1142       elif command in ("M", "m", "L", "l", "T", "t"):
1143         errstring = "Path command \"%s\" requires an x,y pair at index %d" % (command, index)
1144         num1, index, pathdata = self.parse_number(index, pathdata)
1145         num2, index, pathdata = self.parse_number(index, pathdata)
1146 
1147         if num1 == None: raise ValueError(errstring)
1148 
1149         while num1 != None:
1150           if num2 == None: raise ValueError(errstring)
1151           output.append((command, num1, num2, False))
1152 
1153           num1, index, pathdata = self.parse_number(index, pathdata)
1154           num2, index, pathdata = self.parse_number(index, pathdata)
1155 
1156       ######################
1157       elif command in ("S", "s", "Q", "q"):
1158         errstring = "Path command \"%s\" requires a cx,cy,x,y quadruplet at index %d" % (command, index)
1159         num1, index, pathdata = self.parse_number(index, pathdata)
1160         num2, index, pathdata = self.parse_number(index, pathdata)
1161         num3, index, pathdata = self.parse_number(index, pathdata)
1162         num4, index, pathdata = self.parse_number(index, pathdata)
1163 
1164         if num1 == None: raise ValueError(errstring)
1165 
1166         while num1 != None:
1167           if num2 == None or num3 == None or num4 == None: raise ValueError(errstring)
1168           output.append((command, num1, num2, False, num3, num4, False))
1169 
1170           num1, index, pathdata = self.parse_number(index, pathdata)
1171           num2, index, pathdata = self.parse_number(index, pathdata)
1172           num3, index, pathdata = self.parse_number(index, pathdata)
1173           num4, index, pathdata = self.parse_number(index, pathdata)
1174           
1175       ######################
1176       elif command in ("C", "c"):
1177         errstring = "Path command \"%s\" requires a c1x,c1y,c2x,c2y,x,y sextuplet at index %d" % (command, index)
1178         num1, index, pathdata = self.parse_number(index, pathdata)
1179         num2, index, pathdata = self.parse_number(index, pathdata)
1180         num3, index, pathdata = self.parse_number(index, pathdata)
1181         num4, index, pathdata = self.parse_number(index, pathdata)
1182         num5, index, pathdata = self.parse_number(index, pathdata)
1183         num6, index, pathdata = self.parse_number(index, pathdata)
1184         
1185         if num1 == None: raise ValueError(errstring)
1186 
1187         while num1 != None:
1188           if num2 == None or num3 == None or num4 == None or num5 == None or num6 == None: raise ValueError(errstring)
1189 
1190           output.append((command, num1, num2, False, num3, num4, False, num5, num6, False))
1191 
1192           num1, index, pathdata = self.parse_number(index, pathdata)
1193           num2, index, pathdata = self.parse_number(index, pathdata)
1194           num3, index, pathdata = self.parse_number(index, pathdata)
1195           num4, index, pathdata = self.parse_number(index, pathdata)
1196           num5, index, pathdata = self.parse_number(index, pathdata)
1197           num6, index, pathdata = self.parse_number(index, pathdata)
1198 
1199       ######################
1200       elif command in ("A", "a"):
1201         errstring = "Path command \"%s\" requires a rx,ry,angle,large-arc-flag,sweep-flag,x,y septuplet at index %d" % (command, index)
1202         num1, index, pathdata = self.parse_number(index, pathdata)
1203         num2, index, pathdata = self.parse_number(index, pathdata)
1204         num3, index, pathdata = self.parse_number(index, pathdata)
1205         num4, index, pathdata = self.parse_boolean(index, pathdata)
1206         num5, index, pathdata = self.parse_boolean(index, pathdata)
1207         num6, index, pathdata = self.parse_number(index, pathdata)
1208         num7, index, pathdata = self.parse_number(index, pathdata)
1209 
1210         if num1 == None: raise ValueError(errstring)
1211 
1212         while num1 != None:
1213           if num2 == None or num3 == None or num4 == None or num5 == None or num6 == None or num7 == None: raise ValueError(errstring)
1214 
1215           output.append((command, num1, num2, False, num3, num4, num5, num6, num7, False))
1216 
1217           num1, index, pathdata = self.parse_number(index, pathdata)
1218           num2, index, pathdata = self.parse_number(index, pathdata)
1219           num3, index, pathdata = self.parse_number(index, pathdata)
1220           num4, index, pathdata = self.parse_boolean(index, pathdata)
1221           num5, index, pathdata = self.parse_boolean(index, pathdata)
1222           num6, index, pathdata = self.parse_number(index, pathdata)
1223           num7, index, pathdata = self.parse_number(index, pathdata)
1224 
1225     return output
1226 
1227   def SVG(self, trans=None):
1228     """Apply the transformation "trans" and return an SVG object."""
1229     if isinstance(trans, str): trans = totrans(trans)
1230 
1231     x, y, X, Y = None, None, None, None
1232     output = []
1233     for datum in self.d:
1234       if not isinstance(datum, (tuple, list)):
1235         raise TypeError("pathdata elements must be tuples/lists")
1236 
1237       command = datum[0]
1238 
1239       ######################
1240       if command in ("Z", "z"):
1241         x, y, X, Y = None, None, None, None
1242         output.append("Z")
1243 
1244       ######################
1245       elif command in ("H", "h", "V", "v"):
1246         command, num1 = datum
1247 
1248         if command == "H" or (command == "h" and x == None): x = num1
1249         elif command == "h": x += num1
1250         elif command == "V" or (command == "v" and y == None): y = num1
1251         elif command == "v": y += num1
1252 
1253         if trans == None: X, Y = x, y
1254         else: X, Y = trans(x, y)
1255 
1256         output.append("L%g %g" % (X, Y))
1257         
1258       ######################
1259       elif command in ("M", "m", "L", "l", "T", "t"):
1260         command, num1, num2, isglobal12 = datum
1261 
1262         if trans == None or isglobal12:
1263           if command.isupper() or X == None or Y == None:
1264             X, Y = num1, num2
1265           else:
1266             X += num1
1267             Y += num2
1268           x, y = X, Y
1269 
1270         else:
1271           if command.isupper() or x == None or y == None:
1272             x, y = num1, num2
1273           else:
1274             x += num1
1275             y += num2
1276           X, Y = trans(x, y)
1277 
1278         COMMAND = command.capitalize()
1279         output.append("%s%g %g" % (COMMAND, X, Y))
1280 
1281       ######################
1282       elif command in ("S", "s", "Q", "q"):
1283         command, num1, num2, isglobal12, num3, num4, isglobal34 = datum
1284 
1285         if trans == None or isglobal12:
1286           if command.isupper() or X == None or Y == None:
1287             CX, CY = num1, num2
1288           else:
1289             CX = X + num1
1290             CY = Y + num2
1291 
1292         else:
1293           if command.isupper() or x == None or y == None:
1294             cx, cy = num1, num2
1295           else:
1296             cx = x + num1
1297             cy = y + num2
1298           CX, CY = trans(cx, cy)
1299 
1300         if trans == None or isglobal34:
1301           if command.isupper() or X == None or Y == None:
1302             X, Y = num3, num4
1303           else:
1304             X += num3
1305             Y += num4
1306           x, y = X, Y
1307 
1308         else:
1309           if command.isupper() or x == None or y == None:
1310             x, y = num3, num4
1311           else:
1312             x += num3
1313             y += num4
1314           X, Y = trans(x, y)
1315 
1316         COMMAND = command.capitalize()
1317         output.append("%s%g %g %g %g" % (COMMAND, CX, CY, X, Y))
1318 
1319       ######################
1320       elif command in ("C", "c"):
1321         command, num1, num2, isglobal12, num3, num4, isglobal34, num5, num6, isglobal56 = datum
1322 
1323         if trans == None or isglobal12:
1324           if command.isupper() or X == None or Y == None:
1325             C1X, C1Y = num1, num2
1326           else:
1327             C1X = X + num1
1328             C1Y = Y + num2
1329 
1330         else:
1331           if command.isupper() or x == None or y == None:
1332             c1x, c1y = num1, num2
1333           else:
1334             c1x = x + num1
1335             c1y = y + num2
1336           C1X, C1Y = trans(c1x, c1y)
1337 
1338         if trans == None or isglobal34:
1339           if command.isupper() or X == None or Y == None:
1340             C2X, C2Y = num3, num4
1341           else:
1342             C2X = X + num3
1343             C2Y = Y + num4
1344 
1345         else:
1346           if command.isupper() or x == None or y == None:
1347             c2x, c2y = num3, num4
1348           else:
1349             c2x = x + num3
1350             c2y = y + num4
1351           C2X, C2Y = trans(c2x, c2y)
1352 
1353         if trans == None or isglobal56:
1354           if command.isupper() or X == None or Y == None:
1355             X, Y = num5, num6
1356           else:
1357             X += num5
1358             Y += num6
1359           x, y = X, Y
1360 
1361         else:
1362           if command.isupper() or x == None or y == None:
1363             x, y = num5, num6
1364           else:
1365             x += num5
1366             y += num6
1367           X, Y = trans(x, y)
1368 
1369         COMMAND = command.capitalize()
1370         output.append("%s%g %g %g %g %g %g" % (COMMAND, C1X, C1Y, C2X, C2Y, X, Y))
1371 
1372       ######################
1373       elif command in ("A", "a"):
1374         command, num1, num2, isglobal12, angle, large_arc_flag, sweep_flag, num3, num4, isglobal34 = datum
1375 
1376         oldx, oldy = x, y
1377         OLDX, OLDY = X, Y
1378 
1379         if trans == None or isglobal34:
1380           if command.isupper() or X == None or Y == None:
1381             X, Y = num3, num4
1382           else:
1383             X += num3
1384             Y += num4
1385           x, y = X, Y
1386 
1387         else:
1388           if command.isupper() or x == None or y == None:
1389             x, y = num3, num4
1390           else:
1391             x += num3
1392             y += num4
1393           X, Y = trans(x, y)
1394         
1395         if x != None and y != None:
1396           centerx, centery = (x + oldx)/2., (y + oldy)/2.
1397         CENTERX, CENTERY = (X + OLDX)/2., (Y + OLDY)/2.
1398 
1399         if trans == None or isglobal12:
1400           RX = CENTERX + num1
1401           RY = CENTERY + num2
1402 
1403         else:
1404           rx = centerx + num1
1405           ry = centery + num2
1406           RX, RY = trans(rx, ry)
1407 
1408         COMMAND = command.capitalize()
1409         output.append("%s%g %g %g %d %d %g %g" % (COMMAND, RX - CENTERX, RY - CENTERY, angle, large_arc_flag, sweep_flag, X, Y))
1410 
1411       elif command in (",", "."):
1412         command, num1, num2, isglobal12, angle, num3, num4, isglobal34 = datum
1413         if trans == None or isglobal34:
1414           if command == "." or X == None or Y == None:
1415             X, Y = num3, num4
1416           else:
1417             X += num3
1418             Y += num4
1419             x, y = None, None
1420 
1421         else:
1422           if command == "." or x == None or y == None:
1423             x, y = num3, num4
1424           else:
1425             x += num3
1426             y += num4
1427           X, Y = trans(x, y)
1428 
1429         if trans == None or isglobal12:
1430           RX = X + num1
1431           RY = Y + num2
1432 
1433         else:
1434           rx = x + num1
1435           ry = y + num2
1436           RX, RY = trans(rx, ry)
1437 
1438         RX, RY = RX - X, RY - Y
1439 
1440         X1, Y1 = X + RX * math.cos(angle*math.pi/180.), Y + RX * math.sin(angle*math.pi/180.)
1441         X2, Y2 = X + RY * math.sin(angle*math.pi/180.), Y - RY * math.cos(angle*math.pi/180.)
1442         X3, Y3 = X - RX * math.cos(angle*math.pi/180.), Y - RX * math.sin(angle*math.pi/180.)
1443         X4, Y4 = X - RY * math.sin(angle*math.pi/180.), Y + RY * math.cos(angle*math.pi/180.)
1444 
1445         output.append("M%g %gA%g %g %g 0 0 %g %gA%g %g %g 0 0 %g %gA%g %g %g 0 0 %g %gA%g %g %g 0 0 %g %g" \
1446                       % (X1, Y1, RX, RY, angle, X2, Y2, RX, RY, angle, X3, Y3, RX, RY, angle, X4, Y4, RX, RY, angle, X1, Y1))
1447 
1448     return SVG("path", d="".join(output), **self.attr)
1449 
1450 ######################################################################
1451 
1452 def funcRtoC(expr, var="t", globals=None, locals=None):
1453   """Converts a complex "z(t)" string to a function acceptable for Curve.
1454 
1455   expr    required        string in the form "z(t)"
1456   var     default="t"   name of the independent variable
1457   globals default=None    dict of global variables used in the expression;
1458                           you may want to use Python's builtin globals()
1459   locals  default=None    dict of local variables
1460   """
1461   g = cmath.__dict__
1462   if globals != None: g.update(globals)
1463   output = eval("lambda %s: (%s)" % (var, expr), g, locals)
1464   split = lambda z: (z.real, z.imag)
1465   output2 = lambda t: split(output(t))
1466   output2.__name__ = "%s -> %s" % (var, expr)
1467   return output2
1468 
1469 def funcRtoR2(expr, var="t", globals=None, locals=None):
1470   """Converts a "f(t), g(t)" string to a function acceptable for Curve.
1471 
1472   expr    required        string in the form "f(t), g(t)"
1473   var     default="t"   name of the independent variable
1474   globals default=None    dict of global variables used in the expression;
1475                           you may want to use Python's builtin globals()
1476   locals  default=None    dict of local variables
1477   """
1478   g = math.__dict__
1479   if globals != None: g.update(globals)
1480   output = eval("lambda %s: (%s)" % (var, expr), g, locals)
1481   output.__name__ = "%s -> %s" % (var, expr)
1482   return output
1483 
1484 def funcRtoR(expr, var="x", globals=None, locals=None):
1485   """Converts a "f(x)" string to a function acceptable for Curve.
1486 
1487   expr    required        string in the form "f(x)"
1488   var     default="x"   name of the independent variable
1489   globals default=None    dict of global variables used in the expression;
1490                           you may want to use Python's builtin globals()
1491   locals  default=None    dict of local variables
1492   """
1493   g = math.__dict__
1494   if globals != None: g.update(globals)
1495   output = eval("lambda %s: (%s, %s)" % (var, var, expr), g, locals)
1496   output.__name__ = "%s -> %s" % (var, expr)
1497   return output
1498 
1499 class Curve:
1500   """Draws a parametric function as a path.
1501 
1502   Curve(f, low, high, loop, attribute=value)
1503 
1504   f                      required         a Python callable or string in
1505                                           the form "f(t), g(t)"
1506   low, high              required         left and right endpoints
1507   loop                   default=False    if True, connect the endpoints
1508   attribute=value pairs  keyword list     SVG attributes
1509   """
1510   defaults = {}
1511   random_sampling = True
1512   recursion_limit = 15
1513   linearity_limit = 0.05
1514   discontinuity_limit = 5.
1515 
1516   def __repr__(self):
1517     return "<Curve %s [%s, %s] %s>" % (self.f, self.low, self.high, self.attr)
1518 
1519   def __init__(self, f, low, high, loop=False, **attr):
1520     self.f = f
1521     self.low = low
1522     self.high = high
1523     self.loop = loop
1524 
1525     self.attr = dict(self.defaults)
1526     self.attr.update(attr)
1527 
1528   ### nested class Sample
1529   class Sample:
1530     def __repr__(self):
1531       t, x, y, X, Y = self.t, self.x, self.y, self.X, self.Y
1532       if t != None: t = "%g" % t
1533       if x != None: x = "%g" % x
1534       if y != None: y = "%g" % y
1535       if X != None: X = "%g" % X
1536       if Y != None: Y = "%g" % Y
1537       return "<Curve.Sample t=%s x=%s y=%s X=%s Y=%s>" % (t, x, y, X, Y)
1538 
1539     def __init__(self, t): self.t = t
1540 
1541     def link(self, left, right): self.left, self.right = left, right
1542 
1543     def evaluate(self, f, trans):
1544       self.x, self.y = f(self.t)
1545       if trans == None:
1546         self.X, self.Y = self.x, self.y
1547       else:
1548         self.X, self.Y = trans(self.x, self.y)
1549   ### end Sample
1550 
1551   ### nested class Samples
1552   class Samples:
1553     def __repr__(self): return "<Curve.Samples (%d samples)>" % len(self)
1554 
1555     def __init__(self, left, right): self.left, self.right = left, right
1556 
1557     def __len__(self):
1558       count = 0
1559       current = self.left
1560       while current != None:
1561         count += 1
1562         current = current.right
1563       return count
1564 
1565     def __iter__(self):
1566       self.current = self.left
1567       return self
1568 
1569     def next(self):
1570       current = self.current
1571       if current == None: raise StopIteration
1572       self.current = self.current.right
1573       return current
1574   ### end nested class
1575 
1576   def sample(self, trans=None):
1577     """Adaptive-sampling algorithm that chooses the best sample points
1578     for a parametric curve between two endpoints and detects
1579     discontinuities.  Called by SVG()."""
1580     oldrecursionlimit = sys.getrecursionlimit()
1581     sys.setrecursionlimit(self.recursion_limit + 100)
1582     try:
1583       # the best way to keep all the information while sampling is to make a linked list
1584       if not (self.low < self.high): raise ValueError("low must be less than high")
1585       low, high = self.Sample(float(self.low)), self.Sample(float(self.high))
1586       low.link(None, high)
1587       high.link(low, None)
1588 
1589       low.evaluate(self.f, trans)
1590       high.evaluate(self.f, trans)
1591 
1592       # adaptive sampling between the low and high points
1593       self.subsample(low, high, 0, trans)
1594 
1595       # Prune excess points where the curve is nearly linear
1596       left = low
1597       while left.right != None:
1598         # increment mid and right
1599         mid = left.right
1600         right = mid.right
1601         if right != None and left.X != None and left.Y != None and mid.X != None and mid.Y != None and right.X != None and right.Y != None:
1602           numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y)
1603           denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2)
1604           if denom != 0. and abs(numer/denom) < self.linearity_limit:
1605             # drop mid (the garbage collector will get it)
1606             left.right = right
1607             right.left = left
1608           else:
1609             # increment left
1610             left = left.right
1611         else:
1612           left = left.right
1613 
1614       self.last_samples = self.Samples(low, high)
1615 
1616     finally:
1617       sys.setrecursionlimit(oldrecursionlimit)
1618 
1619   def subsample(self, left, right, depth, trans=None):
1620     """Part of the adaptive-sampling algorithm that chooses the best
1621     sample points.  Called by sample()."""
1622 
1623     if self.random_sampling:
1624       mid = self.Sample(left.t + random.uniform(0.3, 0.7) * (right.t - left.t))
1625     else:
1626       mid = self.Sample(left.t + 0.5 * (right.t - left.t))
1627 
1628     left.right = mid
1629     right.left = mid
1630     mid.link(left, right)
1631     mid.evaluate(self.f, trans)
1632 
1633     # calculate the distance of closest approach of mid to the line between left and right
1634     numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y)
1635     denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2)
1636 
1637     # if we haven't sampled enough or left fails to be close enough to right, or mid fails to be linear enough...
1638     if depth < 3 or (denom == 0 and left.t != right.t) or denom > self.discontinuity_limit or (denom != 0. and abs(numer/denom) > self.linearity_limit):
1639 
1640       # and we haven't sampled too many points
1641       if depth < self.recursion_limit:
1642         self.subsample(left, mid, depth+1, trans)
1643         self.subsample(mid, right, depth+1, trans)
1644 
1645       else:
1646         # We've sampled many points and yet it's still not a small linear gap.
1647         # Break the line: it's a discontinuity
1648         mid.y = mid.Y = None
1649 
1650   def SVG(self, trans=None):
1651     """Apply the transformation "trans" and return an SVG object."""
1652     return self.Path(trans).SVG()
1653 
1654   def Path(self, trans=None, local=False):
1655     """Apply the transformation "trans" and return a Path object in
1656     global coordinates.  If local=True, return a Path in local coordinates
1657     (which must be transformed again)."""
1658 
1659     if isinstance(trans, str): trans = totrans(trans)
1660     if isinstance(self.f, str): self.f = funcRtoR2(self.f)
1661 
1662     self.sample(trans)
1663 
1664     output = []
1665     for s in self.last_samples:
1666       if s.X != None and s.Y != None:
1667         if s.left == None or s.left.Y == None:
1668           command = "M"
1669         else:
1670           command = "L"
1671 
1672         if local: output.append((command, s.x, s.y, False))
1673         else: output.append((command, s.X, s.Y, True))
1674 
1675     if self.loop: output.append(("Z",))
1676     return Path(output, **self.attr)
1677 
1678 ######################################################################
1679 
1680 class Poly:
1681   """Draws a curve specified by a sequence of points. The curve may be
1682   piecewise linear, like a polygon, or a Bezier curve.
1683 
1684   Poly(d, mode, loop, attribute=value)
1685 
1686   d                       required        list of tuples representing points
1687                                           and possibly control points
1688   mode                    default="L"   "lines", "bezier", "velocity",
1689                                           "foreback", "smooth", or an abbreviation
1690   loop                    default=False   if True, connect the first and last
1691                                           point, closing the loop
1692   attribute=value pairs   keyword list    SVG attributes
1693 
1694   The format of the tuples in d depends on the mode.
1695 
1696   "lines"/"L"         d=[(x,y), (x,y), ...]
1697                                           piecewise-linear segments joining the (x,y) points
1698   "bezier"/"B"        d=[(x, y, c1x, c1y, c2x, c2y), ...]
1699                                           Bezier curve with two control points (control points
1700                                           preceed (x,y), as in SVG paths). If (c1x,c1y) and
1701                                           (c2x,c2y) both equal (x,y), you get a linear
1702                                           interpolation ("lines")
1703   "velocity"/"V"      d=[(x, y, vx, vy), ...]
1704                                           curve that passes through (x,y) with velocity (vx,vy)
1705                                           (one unit of arclength per unit time); in other words,
1706                                           (vx,vy) is the tangent vector at (x,y). If (vx,vy) is
1707                                           (0,0), you get a linear interpolation ("lines").
1708   "foreback"/"F"      d=[(x, y, bx, by, fx, fy), ...]
1709                                           like "velocity" except that there is a left derivative
1710                                           (bx,by) and a right derivative (fx,fy). If (bx,by)
1711                                           equals (fx,fy) (with no minus sign), you get a
1712                                           "velocity" curve
1713   "smooth"/"S"        d=[(x,y), (x,y), ...]
1714                                           a "velocity" interpolation with (vx,vy)[i] equal to
1715                                           ((x,y)[i+1] - (x,y)[i-1])/2: the minimal derivative
1716   """
1717   defaults = {}
1718 
1719   def __repr__(self):
1720     return "<Poly (%d nodes) mode=%s loop=%s %s>" % (len(self.d), self.mode, repr(self.loop), self.attr)
1721 
1722   def __init__(self, d=[], mode="L", loop=False, **attr):
1723     self.d = list(d)
1724     self.mode = mode
1725     self.loop = loop
1726 
1727     self.attr = dict(self.defaults)
1728     self.attr.update(attr)
1729 
1730   def SVG(self, trans=None):
1731     """Apply the transformation "trans" and return an SVG object."""
1732     return self.Path(trans).SVG()
1733 
1734   def Path(self, trans=None, local=False):
1735     """Apply the transformation "trans" and return a Path object in
1736     global coordinates.  If local=True, return a Path in local coordinates
1737     (which must be transformed again)."""
1738     if isinstance(trans, str): trans = totrans(trans)
1739 
1740     if self.mode[0] == "L" or self.mode[0] == "l": mode = "L"
1741     elif self.mode[0] == "B" or self.mode[0] == "b": mode = "B"
1742     elif self.mode[0] == "V" or self.mode[0] == "v": mode = "V"
1743     elif self.mode[0] == "F" or self.mode[0] == "f": mode = "F"
1744     elif self.mode[0] == "S" or self.mode[0] == "s":
1745       mode = "S"
1746 
1747       vx, vy = [0.]*len(self.d), [0.]*len(self.d)
1748       for i in range(len(self.d)):
1749         inext = (i+1) % len(self.d)
1750         iprev = (i-1) % len(self.d)
1751 
1752         vx[i] = (self.d[inext][0] - self.d[iprev][0])/2.
1753         vy[i] = (self.d[inext][1] - self.d[iprev][1])/2.
1754         if not self.loop and (i == 0 or i == len(self.d)-1):
1755           vx[i], vy[i] = 0., 0.
1756 
1757     else:
1758       raise ValueError("mode must be \"lines\", \"bezier\", \"velocity\", \"foreback\", \"smooth\", or an abbreviation")
1759 
1760     d = []
1761     indexes = list(range(len(self.d)))
1762     if self.loop and len(self.d) > 0: indexes.append(0)
1763 
1764     for i in indexes:
1765       inext = (i+1) % len(self.d)
1766       iprev = (i-1) % len(self.d)
1767 
1768       x, y = self.d[i][0], self.d[i][1]
1769 
1770       if trans == None: X, Y = x, y
1771       else: X, Y = trans(x, y)
1772 
1773       if d == []:
1774         if local: d.append(("M", x, y, False))
1775         else: d.append(("M", X, Y, True))
1776 
1777       elif mode == "L":
1778         if local: d.append(("L", x, y, False))
1779         else: d.append(("L", X, Y, True))
1780 
1781       elif mode == "B":
1782         c1x, c1y = self.d[i][2], self.d[i][3]
1783         if trans == None: C1X, C1Y = c1x, c1y
1784         else: C1X, C1Y = trans(c1x, c1y)
1785 
1786         c2x, c2y = self.d[i][4], self.d[i][5]
1787         if trans == None: C2X, C2Y = c2x, c2y
1788         else: C2X, C2Y = trans(c2x, c2y)
1789 
1790         if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
1791         else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
1792 
1793       elif mode == "V":
1794         c1x, c1y = self.d[iprev][2]/3. + self.d[iprev][0], self.d[iprev][3]/3. + self.d[iprev][1]
1795         c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y
1796 
1797         if trans == None: C1X, C1Y = c1x, c1y
1798         else: C1X, C1Y = trans(c1x, c1y)
1799         if trans == None: C2X, C2Y = c2x, c2y
1800         else: C2X, C2Y = trans(c2x, c2y)
1801 
1802         if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
1803         else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
1804 
1805       elif mode == "F":
1806         c1x, c1y = self.d[iprev][4]/3. + self.d[iprev][0], self.d[iprev][5]/3. + self.d[iprev][1]
1807         c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y
1808 
1809         if trans == None: C1X, C1Y = c1x, c1y
1810         else: C1X, C1Y = trans(c1x, c1y)
1811         if trans == None: C2X, C2Y = c2x, c2y
1812         else: C2X, C2Y = trans(c2x, c2y)
1813 
1814         if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
1815         else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
1816 
1817       elif mode == "S":
1818         c1x, c1y = vx[iprev]/3. + self.d[iprev][0], vy[iprev]/3. + self.d[iprev][1]
1819         c2x, c2y = vx[i]/-3. + x, vy[i]/-3. + y
1820 
1821         if trans == None: C1X, C1Y = c1x, c1y
1822         else: C1X, C1Y = trans(c1x, c1y)
1823         if trans == None: C2X, C2Y = c2x, c2y
1824         else: C2X, C2Y = trans(c2x, c2y)
1825 
1826         if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
1827         else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
1828 
1829     if self.loop and len(self.d) > 0: d.append(("Z",))
1830 
1831     return Path(d, **self.attr)
1832 
1833 ######################################################################
1834 
1835 class Text:
1836   """Draws at text string at a specified point in local coordinates.
1837 
1838   x, y                   required      location of the point in local coordinates
1839   d                      required      text/Unicode string
1840   attribute=value pairs  keyword list  SVG attributes 
1841   """
1842 
1843   defaults = {"stroke":"none", "fill":"black", "font-size":5}
1844 
1845   def __repr__(self):
1846     return "<Text %s at (%g, %g) %s>" % (repr(self.d), self.x, self.y, self.attr)
1847 
1848   def __init__(self, x, y, d, **attr):
1849     self.x = x
1850     self.y = y
1851     self.d = str(d)
1852     self.attr = dict(self.defaults)
1853     self.attr.update(attr)
1854 
1855   def SVG(self, trans=None):
1856     """Apply the transformation "trans" and return an SVG object."""
1857     if isinstance(trans, str): trans = totrans(trans)
1858 
1859     X, Y = self.x, self.y
1860     if trans != None: X, Y = trans(X, Y)
1861     return SVG("text", self.d, x=X, y=Y, **self.attr)
1862 
1863 class TextGlobal:
1864   """Draws at text string at a specified point in global coordinates.
1865 
1866   x, y                   required      location of the point in global coordinates
1867   d                      required      text/Unicode string
1868   attribute=value pairs  keyword list  SVG attributes 
1869   """
1870   defaults = {"stroke":"none", "fill":"black", "font-size":5}
1871 
1872   def __repr__(self):
1873     return "<TextGlobal %s at (%s, %s) %s>" % (repr(self.d), str(self.x), str(self.y), self.attr)
1874 
1875   def __init__(self, x, y, d, **attr):
1876     self.x = x
1877     self.y = y
1878     self.d = str(d)
1879     self.attr = dict(self.defaults)
1880     self.attr.update(attr)
1881 
1882   def SVG(self, trans=None):
1883     """Apply the transformation "trans" and return an SVG object."""
1884     return SVG("text", self.d, x=self.x, y=self.y, **self.attr)
1885 
1886 ######################################################################
1887 
1888 _symbol_templates = {"dot": SVG("symbol", SVG("circle", cx=0, cy=0, r=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"), \
1889                     "box": SVG("symbol", SVG("rect", x1=-1, y1=-1, x2=1, y2=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"), \
1890                     "uptri": SVG("symbol", SVG("path", d="M -1 0.866 L 1 0.866 L 0 -0.866 Z", stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"), \
1891                     "downtri": SVG("symbol", SVG("path", d="M -1 -0.866 L 1 -0.866 L 0 0.866 Z", stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"), \
1892                     }
1893 
1894 def make_symbol(id, shape="dot", **attr):
1895   """Creates a new instance of an SVG symbol to avoid cross-linking objects.
1896 
1897   id                    required         a new identifier (string/Unicode)
1898   shape                 default="dot"  the shape name from _symbol_templates
1899   attribute=value list  keyword list     modify the SVG attributes of the new symbol
1900   """
1901   output = copy.deepcopy(_symbol_templates[shape])
1902   for i in output.sub: i.attr.update(attr_preprocess(attr))
1903   output["id"] = id
1904   return output
1905 
1906 _circular_dot = make_symbol("circular_dot")
1907 
1908 class Dots:
1909   """Dots draws SVG symbols at a set of points.
1910 
1911   d                      required               list of (x,y) points
1912   symbol                 default=None           SVG symbol or a new identifier to
1913                                                 label an auto-generated symbol;
1914                                                 if None, use pre-defined _circular_dot
1915   width, height          default=1, 1           width and height of the symbols
1916                                                 in SVG coordinates
1917   attribute=value pairs  keyword list           SVG attributes
1918   """
1919   defaults = {}
1920 
1921   def __repr__(self):
1922     return "<Dots (%d nodes) %s>" % (len(self.d), self.attr)
1923 
1924   def __init__(self, d=[], symbol=None, width=1., height=1., **attr):
1925     self.d = list(d)
1926     self.width = width
1927     self.height = height
1928 
1929     self.attr = dict(self.defaults)
1930     self.attr.update(attr)
1931 
1932     if symbol == None:
1933       self.symbol = _circular_dot
1934     elif isinstance(symbol, SVG):
1935       self.symbol = symbol
1936     else:
1937       self.symbol = make_symbol(symbol)
1938 
1939   def SVG(self, trans=None):
1940     """Apply the transformation "trans" and return an SVG object."""
1941     if isinstance(trans, str): trans = totrans(trans)
1942 
1943     output = SVG("g", SVG("defs", self.symbol))
1944     id = "#%s" % self.symbol["id"]
1945 
1946     for p in self.d:
1947       x, y = p[0], p[1]
1948 
1949       if trans == None: X, Y = x, y
1950       else: X, Y = trans(x, y)
1951 
1952       item = SVG("use", x=X, y=Y, xlink__href=id)
1953       if self.width != None: item["width"] = self.width
1954       if self.height != None: item["height"] = self.height
1955       output.append(item)
1956       
1957     return output
1958 
1959 ######################################################################
1960 
1961 _marker_templates = {"arrow_start": SVG("marker", SVG("path", d="M 9 3.6 L 10.5 0 L 0 3.6 L 10.5 7.2 L 9 3.6 Z"), viewBox="0 0 10.5 7.2", refX="9", refY="3.6", markerWidth="10.5", markerHeight="7.2", markerUnits="strokeWidth", orient="auto", stroke="none", fill="black"), \
1962                     "arrow_end": SVG("marker", SVG("path", d="M 1.5 3.6 L 0 0 L 10.5 3.6 L 0 7.2 L 1.5 3.6 Z"), viewBox="0 0 10.5 7.2", refX="1.5", refY="3.6", markerWidth="10.5", markerHeight="7.2", markerUnits="strokeWidth", orient="auto", stroke="none", fill="black"), \
1963                     }
1964 
1965 def make_marker(id, shape, **attr):
1966   """Creates a new instance of an SVG marker to avoid cross-linking objects.
1967 
1968   id                     required         a new identifier (string/Unicode)
1969   shape                  required         the shape name from _marker_templates
1970   attribute=value list   keyword list     modify the SVG attributes of the new marker
1971   """
1972   output = copy.deepcopy(_marker_templates[shape])
1973   for i in output.sub: i.attr.update(attr_preprocess(attr))
1974   output["id"] = id
1975   return output
1976 
1977 class Line(Curve):
1978   """Draws a line between two points.
1979 
1980   Line(x1, y1, x2, y2, arrow_start, arrow_end, attribute=value)
1981 
1982   x1, y1                  required        the starting point
1983   x2, y2                  required        the ending point
1984   arrow_start             default=None    if an identifier string/Unicode,
1985                                           draw a new arrow object at the
1986                                           beginning of the line; if a marker,
1987                                           draw that marker instead
1988   arrow_end               default=None    same for the end of the line
1989   attribute=value pairs   keyword list    SVG attributes
1990   """
1991   defaults = {}
1992 
1993   def __repr__(self):
1994     return "<Line (%g, %g) to (%g, %g) %s>" % (self.x1, self.y1, self.x2, self.y2, self.attr)
1995 
1996   def __init__(self, x1, y1, x2, y2, arrow_start=None, arrow_end=None, **attr):
1997     self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
1998     self.arrow_start, self.arrow_end = arrow_start, arrow_end       
1999 
2000     self.attr = dict(self.defaults)
2001     self.attr.update(attr)
2002 
2003   def SVG(self, trans=None):
2004     """Apply the transformation "trans" and return an SVG object."""
2005 
2006     line = self.Path(trans).SVG()
2007 
2008     if (self.arrow_start != False and self.arrow_start != None) or (self.arrow_end != False and self.arrow_end != None):
2009       defs = SVG("defs")
2010 
2011       if self.arrow_start != False and self.arrow_start != None:
2012         if isinstance(self.arrow_start, SVG):
2013           defs.append(self.arrow_start)
2014           line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"]
2015         elif isinstance(self.arrow_start, str):
2016           defs.append(make_marker(self.arrow_start, "arrow_start"))
2017           line.attr["marker-start"] = "url(#%s)" % self.arrow_start
2018         else:
2019           raise TypeError("arrow_start must be False/None or an id string for the new marker")
2020 
2021       if self.arrow_end != False and self.arrow_end != None:
2022         if isinstance(self.arrow_end, SVG):
2023           defs.append(self.arrow_end)
2024           line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"]
2025         elif isinstance(self.arrow_end, str):
2026           defs.append(make_marker(self.arrow_end, "arrow_end"))
2027           line.attr["marker-end"] = "url(#%s)" % self.arrow_end
2028         else:
2029           raise TypeError("arrow_end must be False/None or an id string for the new marker")
2030 
2031       return SVG("g", defs, line)
2032 
2033     return line
2034 
2035   def Path(self, trans=None, local=False):
2036     """Apply the transformation "trans" and return a Path object in
2037     global coordinates.  If local=True, return a Path in local coordinates
2038     (which must be transformed again)."""
2039     self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1 + t*(self.y2 - self.y1))
2040     self.low = 0.
2041     self.high = 1.
2042     self.loop = False
2043 
2044     if trans == None:
2045       return Path([("M", self.x1, self.y1, not local), ("L", self.x2, self.y2, not local)], **self.attr)
2046     else:
2047       return Curve.Path(self, trans, local)
2048 
2049 class LineGlobal:
2050   """Draws a line between two points, one or both of which is in
2051   global coordinates.
2052 
2053   Line(x1, y1, x2, y2, lcoal1, local2, arrow_start, arrow_end, attribute=value)
2054 
2055   x1, y1                  required        the starting point
2056   x2, y2                  required        the ending point
2057   local1                  default=False   if True, interpret first point as a
2058                                           local coordinate (apply transform)
2059   local2                  default=False   if True, interpret second point as a
2060                                           local coordinate (apply transform)
2061   arrow_start             default=None    if an identifier string/Unicode,
2062                                           draw a new arrow object at the
2063                                           beginning of the line; if a marker,
2064                                           draw that marker instead
2065   arrow_end               default=None    same for the end of the line
2066   attribute=value pairs   keyword list    SVG attributes
2067   """
2068   defaults = {}
2069 
2070   def __repr__(self):
2071     local1, local2 = "", ""
2072     if self.local1: local1 = "L"
2073     if self.local2: local2 = "L"
2074 
2075     return "<LineGlobal %s(%s, %s) to %s(%s, %s) %s>" % (local1, str(self.x1), str(self.y1), local2, str(self.x2), str(self.y2), self.attr)
2076 
2077   def __init__(self, x1, y1, x2, y2, local1=False, local2=False, arrow_start=None, arrow_end=None, **attr):
2078     self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
2079     self.local1, self.local2 = local1, local2
2080     self.arrow_start, self.arrow_end = arrow_start, arrow_end
2081 
2082     self.attr = dict(self.defaults)
2083     self.attr.update(attr)
2084 
2085   def SVG(self, trans=None):
2086     """Apply the transformation "trans" and return an SVG object."""
2087     if isinstance(trans, str): trans = totrans(trans)
2088 
2089     X1, Y1, X2, Y2 = self.x1, self.y1, self.x2, self.y2
2090 
2091     if self.local1: X1, Y1 = trans(X1, Y1)
2092     if self.local2: X2, Y2 = trans(X2, Y2)
2093 
2094     line = SVG("path", d="M%s %s L%s %s" % (X1, Y1, X2, Y2), **self.attr)
2095 
2096     if (self.arrow_start != False and self.arrow_start != None) or (self.arrow_end != False and self.arrow_end != None):
2097       defs = SVG("defs")
2098 
2099       if self.arrow_start != False and self.arrow_start != None:
2100         if isinstance(self.arrow_start, SVG):
2101           defs.append(self.arrow_start)
2102           line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"]
2103         elif isinstance(self.arrow_start, str):
2104           defs.append(make_marker(self.arrow_start, "arrow_start"))
2105           line.attr["marker-start"] = "url(#%s)" % self.arrow_start
2106         else:
2107           raise TypeError("arrow_start must be False/None or an id string for the new marker")
2108 
2109       if self.arrow_end != False and self.arrow_end != None:
2110         if isinstance(self.arrow_end, SVG):
2111           defs.append(self.arrow_end)
2112           line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"]
2113         elif isinstance(self.arrow_end, str):
2114           defs.append(make_marker(self.arrow_end, "arrow_end"))
2115           line.attr["marker-end"] = "url(#%s)" % self.arrow_end
2116         else:
2117           raise TypeError("arrow_end must be False/None or an id string for the new marker")
2118 
2119       return SVG("g", defs, line)
2120 
2121     return line
2122 
2123 class VLine(Line):
2124   """Draws a vertical line.
2125 
2126   VLine(y1, y2, x, attribute=value)
2127 
2128   y1, y2                  required        y range
2129   x                       required        x position
2130   attribute=value pairs   keyword list    SVG attributes
2131   """
2132   defaults = {}
2133 
2134   def __repr__(self):
2135     return "<VLine (%g, %g) at x=%s %s>" % (self.y1, self.y2, self.x, self.attr)
2136 
2137   def __init__(self, y1, y2, x, **attr):
2138     self.x = x
2139     self.attr = dict(self.defaults)
2140     self.attr.update(attr)
2141     Line.__init__(self, x, y1, x, y2, **self.attr)
2142 
2143   def Path(self, trans=None, local=False):
2144     """Apply the transformation "trans" and return a Path object in
2145     global coordinates.  If local=True, return a Path in local coordinates
2146     (which must be transformed again)."""
2147     self.x1 = self.x
2148     self.x2 = self.x
2149     return Line.Path(self, trans, local)
2150 
2151 class HLine(Line):
2152   """Draws a horizontal line.
2153 
2154   HLine(x1, x2, y, attribute=value)
2155 
2156   x1, x2                  required        x range
2157   y                       required        y position
2158   attribute=value pairs   keyword list    SVG attributes
2159   """
2160   defaults = {}
2161 
2162   def __repr__(self):
2163     return "<HLine (%g, %g) at y=%s %s>" % (self.x1, self.x2, self.y, self.attr)
2164 
2165   def __init__(self, x1, x2, y, **attr):
2166     self.y = y
2167     self.attr = dict(self.defaults)
2168     self.attr.update(attr)
2169     Line.__init__(self, x1, y, x2, y, **self.attr)
2170 
2171   def Path(self, trans=None, local=False):
2172     """Apply the transformation "trans" and return a Path object in
2173     global coordinates.  If local=True, return a Path in local coordinates
2174     (which must be transformed again)."""
2175     self.y1 = self.y
2176     self.y2 = self.y
2177     return Line.Path(self, trans, local)
2178 
2179 ######################################################################
2180 
2181 class Rect(Curve):
2182   """Draws a rectangle.
2183 
2184   Rect(x1, y1, x2, y2, attribute=value)
2185 
2186   x1, y1                  required        the starting point
2187   x2, y2                  required        the ending point
2188   attribute=value pairs   keyword list    SVG attributes
2189   """
2190   defaults = {}
2191 
2192   def __repr__(self):
2193     return "<Rect (%g, %g), (%g, %g) %s>" % (self.x1, self.y1, self.x2, self.y2, self.attr)
2194 
2195   def __init__(self, x1, y1, x2, y2, **attr):
2196     self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
2197 
2198     self.attr = dict(self.defaults)
2199     self.attr.update(attr)
2200 
2201   def SVG(self, trans=None):
2202     """Apply the transformation "trans" and return an SVG object."""
2203     return self.Path(trans).SVG()
2204 
2205   def Path(self, trans=None, local=False):
2206     """Apply the transformation "trans" and return a Path object in
2207     global coordinates.  If local=True, return a Path in local coordinates
2208     (which must be transformed again)."""
2209     if trans == None:
2210       return Path([("M", self.x1, self.y1, not local), ("L", self.x2, self.y1, not local), ("L", self.x2, self.y2, not local), ("L", self.x1, self.y2, not local), ("Z",)], **self.attr)
2211 
2212     else:
2213       self.low = 0.
2214       self.high = 1.
2215       self.loop = False
2216 
2217       self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1)
2218       d1 = Curve.Path(self, trans, local).d
2219 
2220       self.f = lambda t: (self.x2, self.y1 + t*(self.y2 - self.y1))
2221       d2 = Curve.Path(self, trans, local).d
2222       del d2[0]
2223 
2224       self.f = lambda t: (self.x2 + t*(self.x1 - self.x2), self.y2)
2225       d3 = Curve.Path(self, trans, local).d
2226       del d3[0]
2227 
2228       self.f = lambda t: (self.x1, self.y2 + t*(self.y1 - self.y2))
2229       d4 = Curve.Path(self, trans, local).d
2230       del d4[0]
2231 
2232       return Path(d=(d1 + d2 + d3 + d4 + [("Z",)]), **self.attr)
2233 
2234 ######################################################################
2235 
2236 class Ellipse(Curve):
2237   """Draws an ellipse from a semimajor vector (ax,ay) and a semiminor
2238   length (b).
2239 
2240   Ellipse(x, y, ax, ay, b, attribute=value)
2241 
2242   x, y                    required        the center of the ellipse/circle
2243   ax, ay                  required        a vector indicating the length
2244                                           and direction of the semimajor axis
2245   b                       required        the length of the semiminor axis.
2246                                           If equal to sqrt(ax2 + ay2), the
2247                                           ellipse is a circle
2248   attribute=value pairs   keyword list    SVG attributes
2249 
2250   (If sqrt(ax**2 + ay**2) is less than b, then (ax,ay) is actually the
2251   semiminor axis.)
2252   """
2253   defaults = {}
2254 
2255   def __repr__(self):
2256     return "<Ellipse (%g, %g) a=(%g, %g), b=%g %s>" % (self.x, self.y, self.ax, self.ay, self.b, self.attr)
2257 
2258   def __init__(self, x, y, ax, ay, b, **attr):
2259     self.x, self.y, self.ax, self.ay, self.b = x, y, ax, ay, b
2260 
2261     self.attr = dict(self.defaults)
2262     self.attr.update(attr)
2263 
2264   def SVG(self, trans=None):
2265     """Apply the transformation "trans" and return an SVG object."""
2266     return self.Path(trans).SVG()
2267 
2268   def Path(self, trans=None, local=False):
2269     """Apply the transformation "trans" and return a Path object in
2270     global coordinates.  If local=True, return a Path in local coordinates
2271     (which must be transformed again)."""
2272     angle = math.atan2(self.ay, self.ax) + math.pi/2.
2273     bx = self.b * math.cos(angle)
2274     by = self.b * math.sin(angle)
2275 
2276     self.f = lambda t: (self.x + self.ax*math.cos(t) + bx*math.sin(t), self.y + self.ay*math.cos(t) + by*math.sin(t))
2277     self.low = -math.pi
2278     self.high = math.pi
2279     self.loop = True
2280     return Curve.Path(self, trans, local)
2281 
2282 ######################################################################
2283 
2284 def unumber(x):
2285   """Converts numbers to a Unicode string, taking advantage of special
2286   Unicode characters to make nice minus signs and scientific notation.
2287   """
2288   output = u"%g" % x
2289 
2290   if output[0] == u"-":
2291     output = u"\u2013" + output[1:]
2292 
2293   index = output.find(u"e")
2294   if index != -1:
2295     uniout = unicode(output[:index]) + u"\u00d710"
2296     saw_nonzero = False
2297     for n in output[index+1:]:
2298       if n == u"+": pass # uniout += u"\u207a"
2299       elif n == u"-": uniout += u"\u207b"
2300       elif n == u"0":
2301         if saw_nonzero: uniout += u"\u2070"
2302       elif n == u"1":
2303         saw_nonzero = True
2304         uniout += u"\u00b9"
2305       elif n == u"2":
2306         saw_nonzero = True
2307         uniout += u"\u00b2"
2308       elif n == u"3":
2309         saw_nonzero = True
2310         uniout += u"\u00b3"
2311       elif u"4" <= n <= u"9":
2312         saw_nonzero = True
2313         if saw_nonzero: uniout += eval("u\"\\u%x\"" % (0x2070 + ord(n) - ord(u"0")))
2314       else: uniout += n
2315 
2316     if uniout[:2] == u"1\u00d7": uniout = uniout[2:]
2317     return uniout
2318 
2319   return output
2320 
2321 class Ticks:
2322   """Superclass for all graphics primatives that draw ticks,
2323   miniticks, and tick labels.  This class only draws the ticks.
2324 
2325   Ticks(f, low, high, ticks, miniticks, labels, logbase, arrow_start,
2326         arrow_end, text_attr, attribute=value)
2327 
2328   f                       required        parametric function along which ticks
2329                                           will be drawn; has the same format as
2330                                           the function used in Curve
2331   low, high               required        range of the independent variable
2332   ticks                   default=-10     request ticks according to the standard
2333                                           tick specification (see below)
2334   miniticks               default=True    request miniticks according to the
2335                                           standard minitick specification (below)
2336   labels                  True            request tick labels according to the
2337                                           standard tick label specification (below)
2338   logbase                 default=None    if a number, the axis is logarithmic with
2339                                           ticks at the given base (usually 10)
2340   arrow_start             default=None    if a new string identifier, draw an arrow
2341                                           at the low-end of the axis, referenced by
2342                                           that identifier; if an SVG marker object,
2343                                           use that marker
2344   arrow_end               default=None    if a new string identifier, draw an arrow
2345                                           at the high-end of the axis, referenced by
2346                                           that identifier; if an SVG marker object,
2347                                           use that marker
2348   text_attr               default={}      SVG attributes for the text labels
2349   attribute=value pairs   keyword list    SVG attributes for the tick marks 
2350 
2351   Standard tick specification:
2352 
2353       * True: same as -10 (below).
2354       * Positive number N: draw exactly N ticks, including the endpoints. To
2355         subdivide an axis into 10 equal-sized segments, ask for 11 ticks.
2356       * Negative number -N: draw at least N ticks. Ticks will be chosen with
2357         "natural" values, multiples of 2 or 5.
2358       * List of values: draw a tick mark at each value.
2359       * Dict of value, label pairs: draw a tick mark at each value, labeling
2360         it with the given string. This lets you say things like {3.14159: "pi"}.
2361       * False or None: no ticks.
2362 
2363   Standard minitick specification:
2364 
2365       * True: draw miniticks with "natural" values, more closely spaced than
2366         the ticks.
2367       * Positive number N: draw exactly N miniticks, including the endpoints.
2368         To subdivide an axis into 100 equal-sized segments, ask for 101 miniticks.
2369       * Negative number -N: draw at least N miniticks.
2370       * List of values: draw a minitick mark at each value.
2371       * False or None: no miniticks. 
2372 
2373   Standard tick label specification:
2374 
2375       * True: use the unumber function (described below)
2376       * Format string: standard format strings, e.g. "%5.2f" for 12.34
2377       * Python callable: function that converts numbers to strings
2378       * False or None: no labels 
2379   """
2380   defaults = {"stroke-width":"0.25pt"}
2381   text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
2382   tick_start = -1.5
2383   tick_end = 1.5
2384   minitick_start = -0.75
2385   minitick_end = 0.75
2386   text_start = 2.5
2387   text_angle = 0.
2388 
2389   def __repr__(self):
2390     return "<Ticks %s from %s to %s ticks=%s labels=%s %s>" % (self.f, self.low, self.high, str(self.ticks), str(self.labels), self.attr)
2391 
2392   def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, text_attr={}, **attr):
2393     self.f = f
2394     self.low = low
2395     self.high = high
2396     self.ticks = ticks
2397     self.miniticks = miniticks
2398     self.labels = labels
2399     self.logbase = logbase
2400     self.arrow_start = arrow_start
2401     self.arrow_end = arrow_end
2402 
2403     self.attr = dict(self.defaults)
2404     self.attr.update(attr)
2405 
2406     self.text_attr = dict(self.text_defaults)
2407     self.text_attr.update(text_attr)
2408 
2409   def orient_tickmark(self, t, trans=None):
2410     """Return the position, normalized local x vector, normalized
2411     local y vector, and angle of a tick at position t.
2412 
2413     Normally only used internally.
2414     """
2415     if isinstance(trans, str): trans = totrans(trans)
2416     if trans == None:
2417       f = self.f
2418     else:
2419       f = lambda t: trans(*self.f(t))
2420 
2421     eps = _epsilon * abs(self.high - self.low)
2422 
2423     X, Y = f(t)
2424     Xprime, Yprime = f(t + eps)
2425     xhatx, xhaty = (Xprime - X)/eps, (Yprime - Y)/eps
2426 
2427     norm = math.sqrt(xhatx**2 + xhaty**2)
2428     if norm != 0: xhatx, xhaty = xhatx/norm, xhaty/norm
2429     else: xhatx, xhaty = 1., 0.
2430 
2431     angle = math.atan2(xhaty, xhatx) + math.pi/2.
2432     yhatx, yhaty = math.cos(angle), math.sin(angle)
2433 
2434     return (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle
2435 
2436   def SVG(self, trans=None):
2437     """Apply the transformation "trans" and return an SVG object."""
2438     if isinstance(trans, str): trans = totrans(trans)
2439 
2440     self.last_ticks, self.last_miniticks = self.interpret()
2441     tickmarks = Path([], **self.attr)
2442     minitickmarks = Path([], **self.attr)
2443     output = SVG("g")
2444 
2445     if (self.arrow_start != False and self.arrow_start != None) or (self.arrow_end != False and self.arrow_end != None):
2446       defs = SVG("defs")
2447 
2448       if self.arrow_start != False and self.arrow_start != None:
2449         if isinstance(self.arrow_start, SVG):
2450           defs.append(self.arrow_start)
2451         elif isinstance(self.arrow_start, str):
2452           defs.append(make_marker(self.arrow_start, "arrow_start"))
2453         else:
2454           raise TypeError("arrow_start must be False/None or an id string for the new marker")
2455 
2456       if self.arrow_end != False and self.arrow_end != None:
2457         if isinstance(self.arrow_end, SVG):
2458           defs.append(self.arrow_end)
2459         elif isinstance(self.arrow_end, str):
2460           defs.append(make_marker(self.arrow_end, "arrow_end"))
2461         else:
2462           raise TypeError("arrow_end must be False/None or an id string for the new marker")
2463 
2464       output.append(defs)
2465 
2466     eps = _epsilon * (self.high - self.low)
2467 
2468     for t, label in self.last_ticks.items():
2469       (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans)
2470       
2471       if (not self.arrow_start or abs(t - self.low) > eps) and (not self.arrow_end or abs(t - self.high) > eps):
2472         tickmarks.d.append(("M", X - yhatx*self.tick_start, Y - yhaty*self.tick_start, True))
2473         tickmarks.d.append(("L", X - yhatx*self.tick_end, Y - yhaty*self.tick_end, True))
2474 
2475       angle = (angle - math.pi/2.)*180./math.pi + self.text_angle
2476 
2477       ########### a HACK! ############ (to be removed when Inkscape handles baselines)
2478       if _hacks["inkscape-text-vertical-shift"]:
2479         if self.text_start > 0:
2480           X += math.cos(angle*math.pi/180. + math.pi/2.) * 2.
2481           Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2.
2482         else:
2483           X += math.cos(angle*math.pi/180. + math.pi/2.) * 2. * 2.5
2484           Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2. * 2.5
2485       ########### end hack ###########
2486 
2487       if label != "":
2488         output.append(SVG("text", label, transform="translate(%g, %g) rotate(%g)" % \
2489                           (X - yhatx*self.text_start, Y - yhaty*self.text_start, angle), **self.text_attr))
2490 
2491     for t in self.last_miniticks:
2492       skip = False
2493       for tt in self.last_ticks.keys():
2494         if abs(t - tt) < eps:
2495           skip = True
2496           break
2497       if not skip:
2498         (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans)
2499 
2500       if (not self.arrow_start or abs(t - self.low) > eps) and (not self.arrow_end or abs(t - self.high) > eps):
2501         minitickmarks.d.append(("M", X - yhatx*self.minitick_start, Y - yhaty*self.minitick_start, True))
2502         minitickmarks.d.append(("L", X - yhatx*self.minitick_end, Y - yhaty*self.minitick_end, True))
2503 
2504     output.prepend(tickmarks.SVG(trans))
2505     output.prepend(minitickmarks.SVG(trans))
2506     return output
2507 
2508   def interpret(self):
2509     """Evaluate and return optimal ticks and miniticks according to
2510     the standard minitick specification.
2511 
2512     Normally only used internally.
2513     """
2514 
2515     if self.labels == None or self.labels == False:
2516       format = lambda x: ""
2517 
2518     elif self.labels == True:
2519       format = unumber
2520 
2521     elif isinstance(self.labels, str):
2522       format = lambda x: (self.labels % x)
2523 
2524     elif callable(self.labels):
2525       format = self.labels
2526 
2527     else: raise TypeError("labels must be None/False, True, a format string, or a number->string function")
2528 
2529     # Now for the ticks
2530     ticks = self.ticks
2531 
2532     # Case 1: ticks is None/False
2533     if ticks == None or ticks == False: return {}, []
2534 
2535     # Case 2: ticks is the number of desired ticks
2536     elif isinstance(ticks, (int, long)):
2537       if ticks == True: ticks = -10
2538 
2539       if self.logbase == None:
2540         ticks = self.compute_ticks(ticks, format)
2541       else:
2542         ticks = self.compute_logticks(self.logbase, ticks, format)
2543 
2544       # Now for the miniticks
2545       if self.miniticks == True:
2546         if self.logbase == None:
2547           return ticks, self.compute_miniticks(ticks)
2548         else:
2549           return ticks, self.compute_logminiticks(self.logbase)
2550 
2551       elif isinstance(self.miniticks, (int, long)):
2552         return ticks, self.regular_miniticks(self.miniticks)
2553 
2554       elif getattr(self.miniticks, "__iter__", False):
2555         return ticks, self.miniticks
2556 
2557       elif self.miniticks == False or self.miniticks == None:
2558         return ticks, []
2559 
2560       else:
2561         raise TypeError("miniticks must be None/False, True, a number of desired miniticks, or a list of numbers")
2562         
2563     # Cases 3 & 4: ticks is iterable
2564     elif getattr(ticks, "__iter__", False):
2565 
2566       # Case 3: ticks is some kind of list
2567       if not isinstance(ticks, dict):
2568         output = {}
2569         eps = _epsilon * (self.high - self.low)
2570         for x in ticks:
2571           if format == unumber and abs(x) < eps:
2572             output[x] = u"0"
2573           else:
2574             output[x] = format(x)
2575         ticks = output
2576 
2577       # Case 4: ticks is a dict
2578       else: pass
2579 
2580       # Now for the miniticks
2581       if self.miniticks == True:
2582         if self.logbase == None:
2583           return ticks, self.compute_miniticks(ticks)
2584         else:
2585           return ticks, self.compute_logminiticks(self.logbase)
2586 
2587       elif isinstance(self.miniticks, (int, long)):
2588         return ticks, self.regular_miniticks(self.miniticks)
2589 
2590       elif getattr(self.miniticks, "__iter__", False):
2591         return ticks, self.miniticks
2592 
2593       elif self.miniticks == False or self.miniticks == None:
2594         return ticks, []
2595 
2596       else:
2597         raise TypeError("miniticks must be None/False, True, a number of desired miniticks, or a list of numbers")
2598         
2599     else:
2600       raise TypeError("ticks must be None/False, a number of desired ticks, a list of numbers, or a dictionary of explicit markers")
2601 
2602   def compute_ticks(self, N, format):
2603     """Return less than -N or exactly N optimal linear ticks.
2604 
2605     Normally only used internally.
2606     """
2607     if self.low >= self.high: raise ValueError("low must be less than high")
2608     if N == 1: raise ValueError("N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum")
2609 
2610     eps = _epsilon * (self.high - self.low)
2611 
2612     if N >= 0:
2613       output = {}
2614       x = self.low
2615       for i in range(N):
2616         if format == unumber and abs(x) < eps: label = u"0"
2617         else: label = format(x)
2618         output[x] = label
2619         x += (self.high - self.low)/(N-1.)
2620       return output
2621 
2622     N = -N
2623 
2624     counter = 0
2625     granularity = 10**math.ceil(math.log10(max(abs(self.low), abs(self.high))))
2626     lowN = math.ceil(1.*self.low / granularity)
2627     highN = math.floor(1.*self.high / granularity)
2628 
2629     while (lowN > highN):
2630       countermod3 = counter % 3
2631       if countermod3 == 0: granularity *= 0.5
2632       elif countermod3 == 1: granularity *= 0.4
2633       else: granularity *= 0.5
2634       counter += 1
2635       lowN = math.ceil(1.*self.low / granularity)
2636       highN = math.floor(1.*self.high / granularity)
2637 
2638     last_granularity = granularity
2639     last_trial = None
2640 
2641     while True:
2642       trial = {}
2643       for n in range(int(lowN), int(highN)+1):
2644         x = n * granularity
2645         if format == unumber and abs(x) < eps: label = u"0"
2646         else: label = format(x)
2647         trial[x] = label
2648 
2649       if int(highN)+1 - int(lowN) >= N:
2650         if last_trial == None:
2651           v1, v2 = self.low, self.high
2652           return {v1: format(v1), v2: format(v2)}
2653         else:
2654           low_in_ticks, high_in_ticks = False, False
2655           for t in last_trial.keys():
2656             if 1.*abs(t - self.low)/last_granularity < _epsilon: low_in_ticks = True
2657             if 1.*abs(t - self.high)/last_granularity < _epsilon: high_in_ticks = True
2658 
2659           lowN = 1.*self.low / last_granularity
2660           highN = 1.*self.high / last_granularity
2661           if abs(lowN - round(lowN)) < _epsilon and not low_in_ticks:
2662             last_trial[self.low] = format(self.low)
2663           if abs(highN - round(highN)) < _epsilon and not high_in_ticks:
2664             last_trial[self.high] = format(self.high)
2665           return last_trial
2666 
2667       last_granularity = granularity
2668       last_trial = trial
2669 
2670       countermod3 = counter % 3
2671       if countermod3 == 0: granularity *= 0.5
2672       elif countermod3 == 1: granularity *= 0.4
2673       else: granularity *= 0.5
2674       counter += 1
2675       lowN = math.ceil(1.*self.low / granularity)
2676       highN = math.floor(1.*self.high / granularity)
2677 
2678   def regular_miniticks(self, N):
2679     """Return exactly N linear ticks.
2680 
2681     Normally only used internally.
2682     """
2683     output = []
2684     x = self.low
2685     for i in range(N):
2686       output.append(x)
2687       x += (self.high - self.low)/(N-1.)
2688     return output
2689 
2690   def compute_miniticks(self, original_ticks):
2691     """Return optimal linear miniticks, given a set of ticks.
2692 
2693     Normally only used internally.
2694     """
2695     if len(original_ticks) < 2: original_ticks = ticks(self.low, self.high)
2696     original_ticks = sorted(original_ticks.keys())
2697 
2698     if self.low > original_ticks[0] + _epsilon or self.high < original_ticks[-1] - _epsilon:
2699       raise ValueError("original_ticks {%g...%g} extend beyond [%g, %g]" % (original_ticks[0], original_ticks[-1], self.low, self.high))
2700 
2701     granularities = []
2702     for i in range(len(original_ticks)-1):
2703       granularities.append(original_ticks[i+1] - original_ticks[i])
2704     spacing = 10**(math.ceil(math.log10(min(granularities)) - 1))
2705 
2706     output = []
2707     x = original_ticks[0] - math.ceil(1.*(original_ticks[0] - self.low) / spacing) * spacing
2708 
2709     while x <= self.high:
2710       if x >= self.low:
2711         already_in_ticks = False
2712         for t in original_ticks:
2713           if abs(x-t) < _epsilon * (self.high - self.low): already_in_ticks = True
2714         if not already_in_ticks: output.append(x)
2715       x += spacing
2716     return output
2717 
2718   def compute_logticks(self, base, N, format):
2719     """Return less than -N or exactly N optimal logarithmic ticks.
2720 
2721     Normally only used internally.
2722     """
2723     if self.low >= self.high: raise ValueError("low must be less than high")
2724     if N == 1: raise ValueError("N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum")
2725 
2726     eps = _epsilon * (self.high - self.low)
2727 
2728     if N >= 0:
2729       output = {}
2730       x = self.low
2731       for i in range(N):
2732         if format == unumber and abs(x) < eps: label = u"0"
2733         else: label = format(x)
2734         output[x] = label
2735         x += (self.high - self.low)/(N-1.)
2736       return output
2737 
2738     N = -N
2739 
2740     lowN = math.floor(math.log(self.low, base))
2741     highN = math.ceil(math.log(self.high, base))
2742     output = {}
2743     for n in range(int(lowN), int(highN)+1):
2744       x = base**n
2745       label = format(x)
2746       if self.low <= x <= self.high: output[x] = label
2747 
2748     for i in range(1, len(output)):
2749       keys = sorted(output.keys())
2750       keys = keys[::i]
2751       values = map(lambda k: output[k], keys)
2752       if len(values) <= N:
2753         for k in output.keys():
2754           if k not in keys:
2755             output[k] = ""
2756         break
2757 
2758     if len(output) <= 2:
2759       output2 = self.compute_ticks(N=-int(math.ceil(N/2.)), format=format)
2760       lowest = min(output2)
2761 
2762       for k in output:
2763         if k < lowest: output2[k] = output[k]
2764       output = output2
2765 
2766     return output
2767 
2768   def compute_logminiticks(self, base):
2769     """Return optimal logarithmic miniticks, given a set of ticks.
2770 
2771     Normally only used internally.
2772     """
2773     if self.low >= self.high: raise ValueError("low must be less than high")
2774 
2775     lowN = math.floor(math.log(self.low, base))
2776     highN = math.ceil(math.log(self.high, base))
2777     output = []
2778     num_ticks = 0
2779     for n in range(int(lowN), int(highN)+1):
2780       x = base**n
2781       if self.low <= x <= self.high: num_ticks += 1
2782       for m in range(2, int(math.ceil(base))):
2783         minix = m * x
2784         if self.low <= minix <= self.high: output.append(minix)
2785 
2786     if num_ticks <= 2: return []
2787     else: return output
2788 
2789 ######################################################################
2790 
2791 class CurveAxis(Curve, Ticks):
2792   """Draw an axis with tick marks along a parametric curve.
2793 
2794   CurveAxis(f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
2795   text_attr, attribute=value)
2796 
2797   f                      required         a Python callable or string in
2798                                           the form "f(t), g(t)", just like Curve
2799   low, high              required         left and right endpoints
2800   ticks                  default=-10      request ticks according to the standard
2801                                           tick specification (see help(Ticks))
2802   miniticks              default=True     request miniticks according to the
2803                                           standard minitick specification
2804   labels                 True             request tick labels according to the
2805                                           standard tick label specification
2806   logbase                default=None     if a number, the x axis is logarithmic
2807                                           with ticks at the given base (10 being
2808                                           the most common)
2809   arrow_start            default=None     if a new string identifier, draw an
2810                                           arrow at the low-end of the axis,
2811                                           referenced by that identifier; if an
2812                                           SVG marker object, use that marker
2813   arrow_end              default=None     if a new string identifier, draw an
2814                                           arrow at the high-end of the axis,
2815                                           referenced by that identifier; if an
2816                                           SVG marker object, use that marker
2817   text_attr              default={}       SVG attributes for the text labels
2818   attribute=value pairs  keyword list     SVG attributes
2819   """
2820   defaults = {"stroke-width":"0.25pt"}
2821   text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
2822 
2823   def __repr__(self):
2824     return "<CurveAxis %s [%s, %s] ticks=%s labels=%s %s>" % (self.f, self.low, self.high, str(self.ticks), str(self.labels), self.attr)
2825 
2826   def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, text_attr={}, **attr):
2827     tattr = dict(self.text_defaults)
2828     tattr.update(text_attr)
2829     Curve.__init__(self, f, low, high)
2830     Ticks.__init__(self, f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr)
2831 
2832   def SVG(self, trans=None):
2833     """Apply the transformation "trans" and return an SVG object."""
2834     func = Curve.SVG(self, trans)
2835     ticks = Ticks.SVG(self, trans) # returns a <g />
2836 
2837     if self.arrow_start != False and self.arrow_start != None:
2838       if isinstance(self.arrow_start, str):
2839         func.attr["marker-start"] = "url(#%s)" % self.arrow_start
2840       else:
2841         func.attr["marker-start"] = "url(#%s)" % self.arrow_start.id
2842 
2843     if self.arrow_end != False and self.arrow_end != None:
2844       if isinstance(self.arrow_end, str):
2845         func.attr["marker-end"] = "url(#%s)" % self.arrow_end
2846       else:
2847         func.attr["marker-end"] = "url(#%s)" % self.arrow_end.id
2848 
2849     ticks.append(func)
2850     return ticks
2851 
2852 class LineAxis(Line, Ticks):
2853   """Draws an axis with tick marks along a line.
2854 
2855   LineAxis(x1, y1, x2, y2, start, end, ticks, miniticks, labels, logbase,
2856   arrow_start, arrow_end, text_attr, attribute=value)
2857 
2858   x1, y1                  required        starting point
2859   x2, y2                  required        ending point
2860   start, end              default=0, 1    values to start and end labeling
2861   ticks                   default=-10     request ticks according to the standard
2862                                           tick specification (see help(Ticks))
2863   miniticks               default=True    request miniticks according to the
2864                                           standard minitick specification
2865   labels                  True            request tick labels according to the
2866                                           standard tick label specification
2867   logbase                 default=None    if a number, the x axis is logarithmic
2868                                           with ticks at the given base (usually 10)
2869   arrow_start             default=None    if a new string identifier, draw an arrow
2870                                           at the low-end of the axis, referenced by
2871                                           that identifier; if an SVG marker object,
2872                                           use that marker
2873   arrow_end               default=None    if a new string identifier, draw an arrow
2874                                           at the high-end of the axis, referenced by
2875                                           that identifier; if an SVG marker object,
2876                                           use that marker
2877   text_attr               default={}      SVG attributes for the text labels
2878   attribute=value pairs   keyword list    SVG attributes
2879   """
2880   defaults = {"stroke-width":"0.25pt"}
2881   text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
2882 
2883   def __repr__(self):
2884     return "<LineAxis (%g, %g) to (%g, %g) ticks=%s labels=%s %s>" % (self.x1, self.y1, self.x2, self.y2, str(self.ticks), str(self.labels), self.attr)
2885 
2886   def __init__(self, x1, y1, x2, y2, start=0., end=1., ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr):
2887     self.start = start
2888     self.end = end
2889     self.exclude = exclude
2890     tattr = dict(self.text_defaults)
2891     tattr.update(text_attr)
2892     Line.__init__(self, x1, y1, x2, y2, **attr)
2893     Ticks.__init__(self, None, None, None, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr)
2894 
2895   def interpret(self):
2896     if self.exclude != None and not (isinstance(self.exclude, (tuple, list)) and len(self.exclude) == 2 and \
2897                                      isinstance(self.exclude[0], (int, long, float)) and isinstance(self.exclude[1], (int, long, float))):
2898       raise TypeError("exclude must either be None or (low, high)")
2899 
2900     ticks, miniticks = Ticks.interpret(self)
2901     if self.exclude == None: return ticks, miniticks
2902 
2903     ticks2 = {}
2904     for loc, label in ticks.items():
2905       if self.exclude[0] <= loc <= self.exclude[1]:
2906         ticks2[loc] = ""
2907       else:
2908         ticks2[loc] = label
2909 
2910     return ticks2, miniticks
2911 
2912   def SVG(self, trans=None):
2913     """Apply the transformation "trans" and return an SVG object."""
2914     line = Line.SVG(self, trans) # must be evaluated first, to set self.f, self.low, self.high
2915 
2916     f01 = self.f
2917     self.f = lambda t: f01(1. * (t - self.start) / (self.end - self.start))
2918     self.low = self.start
2919     self.high = self.end
2920 
2921     if self.arrow_start != False and self.arrow_start != None:
2922       if isinstance(self.arrow_start, str):
2923         line.attr["marker-start"] = "url(#%s)" % self.arrow_start
2924       else:
2925         line.attr["marker-start"] = "url(#%s)" % self.arrow_start.id
2926 
2927     if self.arrow_end != False and self.arrow_end != None:
2928       if isinstance(self.arrow_end, str):
2929         line.attr["marker-end"] = "url(#%s)" % self.arrow_end
2930       else:
2931         line.attr["marker-end"] = "url(#%s)" % self.arrow_end.id
2932 
2933     ticks = Ticks.SVG(self, trans) # returns a <g />
2934     ticks.append(line)
2935     return ticks
2936   
2937 class XAxis(LineAxis):
2938   """Draws an x axis with tick marks.
2939 
2940   XAxis(xmin, xmax, aty, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
2941   exclude, text_attr, attribute=value)
2942 
2943   xmin, xmax              required        the x range
2944   aty                     default=0       y position to draw the axis
2945   ticks                   default=-10     request ticks according to the standard
2946                                           tick specification (see help(Ticks))
2947   miniticks               default=True    request miniticks according to the
2948                                           standard minitick specification
2949   labels                  True            request tick labels according to the
2950                                           standard tick label specification
2951   logbase                 default=None    if a number, the x axis is logarithmic
2952                                           with ticks at the given base (usually 10)
2953   arrow_start             default=None    if a new string identifier, draw an arrow
2954                                           at the low-end of the axis, referenced by
2955                                           that identifier; if an SVG marker object,
2956                                           use that marker
2957   arrow_end               default=None    if a new string identifier, draw an arrow
2958                                           at the high-end of the axis, referenced by
2959                                           that identifier; if an SVG marker object,
2960                                           use that marker
2961   exclude                 default=None    if a (low, high) pair, don't draw text
2962                                           labels within this range
2963   text_attr               default={}      SVG attributes for the text labels
2964   attribute=value pairs   keyword list    SVG attributes for all lines
2965 
2966   The exclude option is provided for Axes to keep text from overlapping
2967   where the axes cross. Normal users are not likely to need it.
2968   """
2969   defaults = {"stroke-width":"0.25pt"}
2970   text_defaults = {"stroke":"none", "fill":"black", "font-size":5, "dominant-baseline":"text-before-edge"}
2971   text_start = -1.
2972   text_angle = 0.
2973 
2974   def __repr__(self):
2975     return "<XAxis (%g, %g) at y=%g ticks=%s labels=%s %s>" % (self.xmin, self.xmax, self.aty, str(self.ticks), str(self.labels), self.attr)
2976 
2977   def __init__(self, xmin, xmax, aty=0, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr):
2978     self.aty = aty
2979     tattr = dict(self.text_defaults)
2980     tattr.update(text_attr)
2981     LineAxis.__init__(self, xmin, aty, xmax, aty, xmin, xmax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr)
2982 
2983   def SVG(self, trans=None):
2984     """Apply the transformation "trans" and return an SVG object."""
2985     self.y1 = self.aty
2986     self.y2 = self.aty
2987     return LineAxis.SVG(self, trans)
2988 
2989 class YAxis(LineAxis):
2990   """Draws a y axis with tick marks.
2991 
2992   YAxis(ymin, ymax, atx, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
2993   exclude, text_attr, attribute=value)
2994 
2995   ymin, ymax              required        the y range
2996   atx                     default=0       x position to draw the axis
2997   ticks                   default=-10     request ticks according to the standard
2998                                           tick specification (see help(Ticks))
2999   miniticks               default=True    request miniticks according to the
3000                                           standard minitick specification
3001   labels                  True            request tick labels according to the
3002                                           standard tick label specification
3003   logbase                 default=None    if a number, the y axis is logarithmic
3004                                           with ticks at the given base (usually 10)
3005   arrow_start             default=None    if a new string identifier, draw an arrow
3006                                           at the low-end of the axis, referenced by
3007                                           that identifier; if an SVG marker object,
3008                                           use that marker
3009   arrow_end               default=None    if a new string identifier, draw an arrow
3010                                           at the high-end of the axis, referenced by
3011                                           that identifier; if an SVG marker object,
3012                                           use that marker
3013   exclude                 default=None    if a (low, high) pair, don't draw text
3014                                           labels within this range
3015   text_attr               default={}      SVG attributes for the text labels
3016   attribute=value pairs   keyword list    SVG attributes for all lines
3017 
3018   The exclude option is provided for Axes to keep text from overlapping
3019   where the axes cross. Normal users are not likely to need it.
3020   """
3021   defaults = {"stroke-width":"0.25pt"}
3022   text_defaults = {"stroke":"none", "fill":"black", "font-size":5, "text-anchor":"end", "dominant-baseline":"middle"}
3023   text_start = 2.5
3024   text_angle = 90.
3025 
3026   def __repr__(self):
3027     return "<YAxis (%g, %g) at x=%g ticks=%s labels=%s %s>" % (self.ymin, self.ymax, self.atx, str(self.ticks), str(self.labels), self.attr)
3028 
3029   def __init__(self, ymin, ymax, atx=0, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr):
3030     self.atx = atx
3031     tattr = dict(self.text_defaults)
3032     tattr.update(text_attr)
3033     LineAxis.__init__(self, atx, ymin, atx, ymax, ymin, ymax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr)
3034 
3035   def SVG(self, trans=None):
3036     """Apply the transformation "trans" and return an SVG object."""
3037     self.x1 = self.atx
3038     self.x2 = self.atx
3039     return LineAxis.SVG(self, trans)
3040 
3041 class Axes:
3042   """Draw a pair of intersecting x-y axes.
3043 
3044   Axes(xmin, xmax, ymin, ymax, atx, aty, xticks, xminiticks, xlabels, xlogbase,
3045   yticks, yminiticks, ylabels, ylogbase, arrows, text_attr, attribute=value)
3046 
3047   xmin, xmax               required       the x range
3048   ymin, ymax               required       the y range
3049   atx, aty                 default=0, 0   point where the axes try to cross;
3050                                           if outside the range, the axes will
3051                                           cross at the closest corner
3052   xticks                   default=-10    request ticks according to the standard
3053                                           tick specification (see help(Ticks))
3054   xminiticks               default=True   request miniticks according to the
3055                                           standard minitick specification
3056   xlabels                  True           request tick labels according to the
3057                                           standard tick label specification
3058   xlogbase                 default=None   if a number, the x axis is logarithmic
3059                                           with ticks at the given base (usually 10)
3060   yticks                   default=-10    request ticks according to the standard
3061                                           tick specification
3062   yminiticks               default=True   request miniticks according to the
3063                                           standard minitick specification
3064   ylabels                  True           request tick labels according to the
3065                                           standard tick label specification
3066   ylogbase                 default=None   if a number, the y axis is logarithmic
3067                                           with ticks at the given base (usually 10)
3068   arrows                   default=None   if a new string identifier, draw arrows
3069                                           referenced by that identifier
3070   text_attr                default={}     SVG attributes for the text labels
3071   attribute=value pairs    keyword list   SVG attributes for all lines
3072   """
3073   defaults = {"stroke-width":"0.25pt"}
3074   text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
3075 
3076   def __repr__(self):
3077     return "<Axes x=(%g, %g) y=(%g, %g) at (%g, %g) %s>" % (self.xmin, self.xmax, self.ymin, self.ymax, self.atx, self.aty, self.attr)
3078 
3079   def __init__(self, xmin, xmax, ymin, ymax, atx=0, aty=0, xticks=-10, xminiticks=True, xlabels=True, xlogbase=None, yticks=-10, yminiticks=True, ylabels=True, ylogbase=None, arrows=None, text_attr={}, **attr):
3080     self.xmin, self.xmax = xmin, xmax
3081     self.ymin, self.ymax = ymin, ymax
3082     self.atx, self.aty = atx, aty
3083     self.xticks, self.xminiticks, self.xlabels, self.xlogbase = xticks, xminiticks, xlabels, xlogbase
3084     self.yticks, self.yminiticks, self.ylabels, self.ylogbase = yticks, yminiticks, ylabels, ylogbase
3085     self.arrows = arrows
3086 
3087     self.text_attr = dict(self.text_defaults)
3088     self.text_attr.update(text_attr)
3089 
3090     self.attr = dict(self.defaults)
3091     self.attr.update(attr)
3092 
3093   def SVG(self, trans=None):
3094     """Apply the transformation "trans" and return an SVG object."""
3095     atx, aty = self.atx, self.aty
3096     if atx < self.xmin: atx = self.xmin
3097     if atx > self.xmax: atx = self.xmax
3098     if aty < self.ymin: aty = self.ymin
3099     if aty > self.ymax: aty = self.ymax
3100 
3101     xmargin = 0.1 * abs(self.ymin - self.ymax)
3102     xexclude = atx - xmargin, atx + xmargin
3103     
3104     ymargin = 0.1 * abs(self.xmin - self.xmax)
3105     yexclude = aty - ymargin, aty + ymargin
3106 
3107     if self.arrows != None and self.arrows != False:
3108       xarrow_start = self.arrows + ".xstart"
3109       xarrow_end = self.arrows + ".xend"
3110       yarrow_start = self.arrows + ".ystart"
3111       yarrow_end = self.arrows + ".yend"
3112     else:
3113       xarrow_start = xarrow_end = yarrow_start = yarrow_end = None
3114 
3115     xaxis = XAxis(self.xmin, self.xmax, aty, self.xticks, self.xminiticks, self.xlabels, self.xlogbase, xarrow_start, xarrow_end, exclude=xexclude, text_attr=self.text_attr, **self.attr).SVG(trans)
3116     yaxis = YAxis(self.ymin, self.ymax, atx, self.yticks, self.yminiticks, self.ylabels, self.ylogbase, yarrow_start, yarrow_end, exclude=yexclude, text_attr=self.text_attr, **self.attr).SVG(trans)
3117     return SVG("g", *(xaxis.sub + yaxis.sub))
3118 
3119 ######################################################################
3120 
3121 class HGrid(Ticks):
3122   """Draws the horizontal lines of a grid over a specified region
3123   using the standard tick specification (see help(Ticks)) to place the
3124   grid lines.
3125 
3126   HGrid(xmin, xmax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value)
3127 
3128   xmin, xmax              required        the x range
3129   low, high               required        the y range
3130   ticks                   default=-10     request ticks according to the standard
3131                                           tick specification (see help(Ticks))
3132   miniticks               default=False   request miniticks according to the
3133                                           standard minitick specification
3134   logbase                 default=None    if a number, the axis is logarithmic
3135                                           with ticks at the given base (usually 10)
3136   mini_attr               default={}      SVG attributes for the minitick-lines
3137                                           (if miniticks != False)
3138   attribute=value pairs   keyword list    SVG attributes for the major tick lines
3139   """
3140   defaults = {"stroke-width":"0.25pt", "stroke":"gray"}
3141   mini_defaults = {"stroke-width":"0.25pt", "stroke":"lightgray", "stroke-dasharray":"1,1"}
3142 
3143   def __repr__(self):
3144     return "<HGrid x=(%g, %g) %g <= y <= %g ticks=%s miniticks=%s %s>" % (self.xmin, self.xmax, self.low, self.high, str(self.ticks), str(self.miniticks), self.attr)
3145 
3146   def __init__(self, xmin, xmax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
3147     self.xmin, self.xmax = xmin, xmax
3148 
3149     self.mini_attr = dict(self.mini_defaults)
3150     self.mini_attr.update(mini_attr)
3151 
3152     Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase)
3153 
3154     self.attr = dict(self.defaults)
3155     self.attr.update(attr)
3156 
3157   def SVG(self, trans=None):
3158     """Apply the transformation "trans" and return an SVG object."""
3159     self.last_ticks, self.last_miniticks = Ticks.interpret(self)
3160 
3161     ticksd = []
3162     for t in self.last_ticks.keys():
3163       ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
3164 
3165     miniticksd = []
3166     for t in self.last_miniticks:
3167       miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
3168 
3169     return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
3170     
3171 class VGrid(Ticks):
3172   """Draws the vertical lines of a grid over a specified region
3173   using the standard tick specification (see help(Ticks)) to place the
3174   grid lines.
3175 
3176   HGrid(ymin, ymax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value)
3177 
3178   ymin, ymax              required        the y range
3179   low, high               required        the x range
3180   ticks                   default=-10     request ticks according to the standard
3181                                           tick specification (see help(Ticks))
3182   miniticks               default=False   request miniticks according to the
3183                                           standard minitick specification
3184   logbase                 default=None    if a number, the axis is logarithmic
3185                                           with ticks at the given base (usually 10)
3186   mini_attr               default={}      SVG attributes for the minitick-lines
3187                                           (if miniticks != False)
3188   attribute=value pairs   keyword list    SVG attributes for the major tick lines
3189   """
3190   defaults = {"stroke-width":"0.25pt", "stroke":"gray"}
3191   mini_defaults = {"stroke-width":"0.25pt", "stroke":"lightgray", "stroke-dasharray":"1,1"}
3192 
3193   def __repr__(self):
3194     return "<VGrid y=(%g, %g) %g <= x <= %g ticks=%s miniticks=%s %s>" % (self.ymin, self.ymax, self.low, self.high, str(self.ticks), str(self.miniticks), self.attr)
3195 
3196   def __init__(self, ymin, ymax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
3197     self.ymin, self.ymax = ymin, ymax
3198 
3199     self.mini_attr = dict(self.mini_defaults)
3200     self.mini_attr.update(mini_attr)
3201 
3202     Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase)
3203 
3204     self.attr = dict(self.defaults)
3205     self.attr.update(attr)
3206 
3207   def SVG(self, trans=None):
3208     """Apply the transformation "trans" and return an SVG object."""
3209     self.last_ticks, self.last_miniticks = Ticks.interpret(self)
3210 
3211     ticksd = []
3212     for t in self.last_ticks.keys():
3213       ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
3214 
3215     miniticksd = []
3216     for t in self.last_miniticks:
3217       miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
3218 
3219     return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
3220 
3221 class Grid(Ticks):
3222   """Draws a grid over a specified region using the standard tick
3223   specification (see help(Ticks)) to place the grid lines.
3224 
3225   Grid(xmin, xmax, ymin, ymax, ticks, miniticks, logbase, mini_attr, attribute=value)
3226 
3227   xmin, xmax              required        the x range
3228   ymin, ymax              required        the y range
3229   ticks                   default=-10     request ticks according to the standard
3230                                           tick specification (see help(Ticks))
3231   miniticks               default=False   request miniticks according to the
3232                                           standard minitick specification
3233   logbase                 default=None    if a number, the axis is logarithmic
3234                                           with ticks at the given base (usually 10)
3235   mini_attr               default={}      SVG attributes for the minitick-lines
3236                                           (if miniticks != False)
3237   attribute=value pairs   keyword list    SVG attributes for the major tick lines
3238   """
3239   defaults = {"stroke-width":"0.25pt", "stroke":"gray"}
3240   mini_defaults = {"stroke-width":"0.25pt", "stroke":"lightgray", "stroke-dasharray":"1,1"}
3241 
3242   def __repr__(self):
3243     return "<Grid x=(%g, %g) y=(%g, %g) ticks=%s miniticks=%s %s>" % (self.xmin, self.xmax, self.ymin, self.ymax, str(self.ticks), str(self.miniticks), self.attr)
3244 
3245   def __init__(self, xmin, xmax, ymin, ymax, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
3246     self.xmin, self.xmax = xmin, xmax
3247     self.ymin, self.ymax = ymin, ymax
3248 
3249     self.mini_attr = dict(self.mini_defaults)
3250     self.mini_attr.update(mini_attr)
3251 
3252     Ticks.__init__(self, None, None, None, ticks, miniticks, None, logbase)
3253 
3254     self.attr = dict(self.defaults)
3255     self.attr.update(attr)
3256 
3257   def SVG(self, trans=None):
3258     """Apply the transformation "trans" and return an SVG object."""
3259     self.low, self.high = self.xmin, self.xmax
3260     self.last_xticks, self.last_xminiticks = Ticks.interpret(self)
3261     self.low, self.high = self.ymin, self.ymax
3262     self.last_yticks, self.last_yminiticks = Ticks.interpret(self)
3263 
3264     ticksd = []
3265     for t in self.last_xticks.keys():
3266       ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
3267     for t in self.last_yticks.keys():
3268       ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
3269 
3270     miniticksd = []
3271     for t in self.last_xminiticks:
3272       miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
3273     for t in self.last_yminiticks:
3274       miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
3275 
3276     return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
3277 
3278 ######################################################################
3279 
3280 class XErrorBars:
3281   """Draws x error bars at a set of points. This is usually used
3282   before (under) a set of Dots at the same points.
3283 
3284   XErrorBars(d, attribute=value)
3285 
3286   d                       required        list of (x,y,xerr...) points
3287   attribute=value pairs   keyword list    SVG attributes
3288 
3289   If points in d have
3290 
3291       * 3 elements, the third is the symmetric error bar
3292       * 4 elements, the third and fourth are the asymmetric lower and
3293         upper error bar. The third element should be negative,
3294         e.g. (5, 5, -1, 2) is a bar from 4 to 7.
3295       * more than 4, a tick mark is placed at each value. This lets
3296         you nest errors from different sources, correlated and
3297         uncorrelated, statistical and systematic, etc.
3298   """
3299   defaults = {"stroke-width":"0.25pt"}
3300 
3301   def __repr__(self):
3302     return "<XErrorBars (%d nodes)>" % len(self.d)
3303 
3304   def __init__(self, d=[], **attr):
3305     self.d = list(d)
3306 
3307     self.attr = dict(self.defaults)
3308     self.attr.update(attr)
3309     
3310   def SVG(self, trans=None):
3311     """Apply the transformation "trans" and return an SVG object."""
3312     if isinstance(trans, str): trans = totrans(trans) # only once
3313 
3314     output = SVG("g")
3315     for p in self.d:
3316       x, y = p[0], p[1]
3317 
3318       if len(p) == 3: bars = [x - p[2], x + p[2]]
3319       else: bars = [x + pi for pi in p[2:]]
3320       
3321       start, end = min(bars), max(bars)
3322       output.append(LineAxis(start, y, end, y, start, end, bars, False, False, **self.attr).SVG(trans))
3323 
3324     return output
3325 
3326 class YErrorBars:
3327   """Draws y error bars at a set of points. This is usually used
3328   before (under) a set of Dots at the same points.
3329 
3330   YErrorBars(d, attribute=value)
3331 
3332   d                       required        list of (x,y,yerr...) points
3333   attribute=value pairs   keyword list    SVG attributes
3334 
3335   If points in d have
3336 
3337       * 3 elements, the third is the symmetric error bar
3338       * 4 elements, the third and fourth are the asymmetric lower and
3339         upper error bar. The third element should be negative,
3340         e.g. (5, 5, -1, 2) is a bar from 4 to 7.
3341       * more than 4, a tick mark is placed at each value. This lets
3342         you nest errors from different sources, correlated and
3343         uncorrelated, statistical and systematic, etc.
3344   """
3345   defaults = {"stroke-width":"0.25pt"}
3346 
3347   def __repr__(self):
3348     return "<YErrorBars (%d nodes)>" % len(self.d)
3349 
3350   def __init__(self, d=[], **attr):
3351     self.d = list(d)
3352 
3353     self.attr = dict(self.defaults)
3354     self.attr.update(attr)
3355     
3356   def SVG(self, trans=None):
3357     """Apply the transformation "trans" and return an SVG object."""
3358     if isinstance(trans, str): trans = totrans(trans) # only once
3359 
3360     output = SVG("g")
3361     for p in self.d:
3362       x, y = p[0], p[1]
3363 
3364       if len(p) == 3: bars = [y - p[2], y + p[2]]
3365       else: bars = [y + pi for pi in p[2:]]
3366       
3367       start, end = min(bars), max(bars)
3368       output.append(LineAxis(x, start, x, end, start, end, bars, False, False, **self.attr).SVG(trans))
3369 
3370     return output